Skip to content

Commit

Permalink
Math Parser (#3)
Browse files Browse the repository at this point in the history
* epic parser

* Indentation syntax

---------

Co-authored-by: Space Banana <[email protected]>
  • Loading branch information
ShaffySwitcher and spacebanana420 authored Jul 27, 2024
1 parent bc7fc8e commit bdb4249
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 8 deletions.
104 changes: 104 additions & 0 deletions src/parser/math_parser.scala
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 11 additions & 8 deletions src/runner/math.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 =
Expand Down

0 comments on commit bdb4249

Please sign in to comment.