From bdb4249a91f9790880fb781d64cb9558049e350b Mon Sep 17 00:00:00 2001 From: Shaffy Date: Sat, 27 Jul 2024 22:27:11 +0200 Subject: [PATCH] Math Parser (#3) * epic parser * Indentation syntax --------- Co-authored-by: Space Banana --- src/parser/math_parser.scala | 104 +++++++++++++++++++++++++++++++++++ src/runner/math.scala | 19 ++++--- 2 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 src/parser/math_parser.scala diff --git a/src/parser/math_parser.scala b/src/parser/math_parser.scala new file mode 100644 index 0000000..4cb1f4f --- /dev/null +++ b/src/parser/math_parser.scala @@ -0,0 +1,104 @@ +package tofu.math_parser + +import tofu.{debugMessage, debug_printSeq} +import tofu.variables.* + +// TO-DO BITWISE: +// AND (&) -> XOR (^) -> OR (|) + +// Defines a datatype Token with all subclasses defined +sealed trait Token +case class NumberToken(value: Int) extends Token // 1,2,3,4 +case class VariableToken(name: String) extends Token // variable +case class OperatorToken(op: Char) extends Token // +,-,etc... +case class ParenToken(paren: Char) extends Token // ( & ) + +// "($variable+1)" -> (()+(variable)+(+)+(1)+()) +def tokenize(expr: String): List[Token] = + // main function (recurssive holy shit) + def tokenizeHelper(remaining: List[Char], current: String, acc: List[Token]): List[Token] = remaining match + case Nil => //last character of the string + if (current.nonEmpty) tokenizeHelper(Nil, "", acc :+ parseToken(current)) // parse last token + else acc // all done! + case head :: tail if head.isWhitespace => // if current character is whitespace + if (current.nonEmpty) tokenizeHelper(tail, "", acc :+ parseToken(current)) // parse current token + else tokenizeHelper(tail, "", acc) // continue to next token + case head :: tail if "+-*/()".contains(head) => // if current character is operator + val newAcc = if (current.nonEmpty) acc :+ parseToken(current) else acc // parse current token if available + tokenizeHelper(tail, "", newAcc :+ parseToken(head.toString)) // continue to next token + case head :: tail => tokenizeHelper(tail, current + head, acc) // add to current string (maybe number or variable) + + def parseToken(s: String): Token = s match + case "+" | "-" | "*" | "/" => OperatorToken(s.charAt(0)) + case "(" => ParenToken('(') + case ")" => ParenToken(')') + case s if s.startsWith("$") => VariableToken(s) + case s => NumberToken(s.toInt) + + tokenizeHelper(expr.toList, "", Nil) + +// evaluates token to an int, follows PEMDAS! +def parseExpression(tokens: List[Token]): Int = + def parseExpr(remaining: List[Token]): (Int, List[Token]) = parseAddSub(remaining) + + def parseAddSub(remaining: List[Token]): (Int, List[Token]) = + // PEDMAS logic + var (left, newRemaining) = parseMulDiv(remaining) + var currentRemaining = newRemaining + + // while still parsing add/sub + while (currentRemaining.headOption.exists { + case OperatorToken('+') | OperatorToken('-') => true + case _ => false + }) do + val op = currentRemaining.head.asInstanceOf[OperatorToken].op + val (right, nextRemaining) = parseMulDiv(currentRemaining.tail) + left = if (op == '+') left + right else left - right + currentRemaining = nextRemaining + (left, currentRemaining) + + def parseMulDiv(remaining: List[Token]): (Int, List[Token]) = + var (left, newRemaining) = parseFactor(remaining) + var currentRemaining = newRemaining + + // while still parsing mul/div + while (currentRemaining.headOption.exists { + case OperatorToken('*') | OperatorToken('/') => true + case _ => false + }) do + val op = currentRemaining.head.asInstanceOf[OperatorToken].op + val (right, nextRemaining) = parseFactor(currentRemaining.tail) + left = if (op == '*') left * right else left / right + currentRemaining = nextRemaining + (left, currentRemaining) + + // parse variable or immediate value + def parseFactor(remaining: List[Token]): (Int, List[Token]) = + remaining match + case NumberToken(n) :: tail => + debugMessage(s"Parsed number: $n") + (n, tail) + case VariableToken(name) :: tail => + val value = readVariable_int_safe(name) + debugMessage(s"Parsed variable: $name with value $value") + (value, tail) + case ParenToken('(') :: tail => + debugMessage(s"Start of expression in parentheses") + // if parenthesis, parse new expression + val (result, afterExpr) = parseExpr(tail) + afterExpr.headOption match + case Some(ParenToken(')')) => + debugMessage(s"End of expression in parentheses") + (result, afterExpr.tail) + case _ => + throw new RuntimeException("Mismatched parentheses: no closing parenthesis found") + case _ => + throw new RuntimeException(s"Unexpected token: ${remaining.headOption.getOrElse("None")}") + + // returns the int + parseExpr(tokens)._1 + +def evaluateExpression(expr: String): Int = + val tokens = tokenize(expr) + debugMessage(s"Tokenized expression: $tokens") + parseExpression(tokens) diff --git a/src/runner/math.scala b/src/runner/math.scala index ecb1547..a983233 100644 --- a/src/runner/math.scala +++ b/src/runner/math.scala @@ -4,15 +4,15 @@ import tofu.{debugMessage, debug_printSeq, closeTofu} import tofu.variables.* import tofu.parser.* import tofu.reader.findLineStart +import tofu.math_parser.evaluateExpression import scala.sys.process.* def calculate(strs: Seq[String], line: String): Int = debug_printSeq(s"Math string elements (length ${strs.length}):", strs) - if strs.length < 3 then closeTofu(s"Operator error! Calculation in line\n$line\nRequires at least 2 elements and 1 operator!") - if strs.length % 2 != 1 then closeTofu(s"Operator error! Calculation in line\n$line\nIs missing an element or operator") - val classes = strs.map(x => readVariable_class_safe(x)) - calculateSeq(classes) + if strs.length < 2 then closeTofu(s"Operator error! Calculation in line\n$line\nRequires at least a variable name and an expression!") + val expression = strs.tail.mkString(" ") + evaluateExpression(expression) def calc_operator(e0: Int, e1: Int, o: String): Int = debugMessage(s"Calculating: $e0 $o $e1") @@ -62,10 +62,13 @@ private def getMathStr(line: String, i: Int, math: String = "", copystr: Boolean def calculateInt(line: String) = val start = findLineStart(line, 7) - val name = getName_variable(line, i = start) - val mathstr = getMathStr(line, start) - - val result = calculate(mkstr(mathstr), line) + val parts = line.substring(start).split(",", 2).map(_.trim) + if parts.length != 2 then + closeTofu(s"Syntax error! Calculation in line\n$line\nRequires a variable name and an expression separated by a comma!") + val name = parts(0) + val expression = parts(1) + + val result = evaluateExpression(expression) declareInt(name, result) private def math_mkInt(num: String): Int =