Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions expression_parser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Mathematical Expression Parser and Evaluator

A zero-external-dependency mathematical expression parser, tokenizer, AST builder, and evaluator built from scratch in Python.

## Features

- **Tokenizer**: Converts input text strings into tokens (Numbers, Operators, Identifiers, Functions, Parentheses). Supports implicit multiplication (e.g. `2(3+4)` or `3pi`).
- **AST Parser**: Recursive-descent parser building Abstract Syntax Trees respecting operator precedence and associativity (`^`, `*`, `/`, `%`, `+`, `-`).
- **Evaluator**: Walks AST nodes safely to calculate results with variable binding support and builtin math functions (`sin`, `cos`, `tan`, `sqrt`, `abs`, `log`, `exp`, `min`, `max`, `floor`, `ceil`).
- **Interactive REPL & CLI**: Support for interactive session and CLI expression evaluation.

## Usage

### CLI REPL

```bash
python expression_parser/main.py
```

### Direct Expression Evaluation

```bash
python expression_parser/main.py "2 + 3 * (4 - 1)^2"
# Output: 29.0
```

### Python API

```python
from expression_parser import evaluate_expression

result = evaluate_expression("sin(pi / 2) + cos(0)")
print(result) # 2.0

vars_dict = {"x": 5, "y": 2}
res = evaluate_expression("x^2 + 2*x*y + y^2", vars_dict)
print(res) # 49.0
```
13 changes: 13 additions & 0 deletions expression_parser/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
__init__.py for expression_parser module.
"""

from expression_parser.tokenizer import Tokenizer, Token, TokenType, TokenizerError
from expression_parser.parser import Parser, ParseError, ASTNode
from expression_parser.evaluator import Evaluator, EvaluationError, evaluate_expression

__all__ = [
"Tokenizer", "Token", "TokenType", "TokenizerError",
"Parser", "ParseError", "ASTNode",
"Evaluator", "EvaluationError", "evaluate_expression"
]
119 changes: 119 additions & 0 deletions expression_parser/evaluator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Evaluator for AST nodes generated by Parser.
Evaluates math expressions with variable and function support.
"""

import math
from typing import Dict, Any, Union
from expression_parser.parser import ASTNode, NumberNode, VariableNode, UnaryOpNode, BinaryOpNode, FunctionCallNode


class EvaluationError(Exception):
pass


class Evaluator:
BUILTIN_FUNCTIONS = {
"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"asin": math.asin,
"acos": math.acos,
"atan": math.atan,
"sqrt": math.sqrt,
"abs": abs,
"log": math.log,
"exp": math.exp,
"floor": math.floor,
"ceil": math.ceil,
"min": min,
"max": max,
}

BUILTIN_CONSTANTS = {
"pi": math.pi,
"e": math.e,
"tau": math.tau,
}

def __init__(self, variables: Dict[str, Union[int, float]] = None):
self.variables = self.BUILTIN_CONSTANTS.copy()
if variables:
self.variables.update(variables)

def evaluate(self, node: ASTNode) -> Union[int, float]:
if isinstance(node, NumberNode):
return node.value

if isinstance(node, VariableNode):
if node.name in self.variables:
return self.variables[node.name]
raise EvaluationError(f"Undefined variable or constant: '{node.name}'")

if isinstance(node, UnaryOpNode):
val = self.evaluate(node.operand)
if node.operator == '+':
return +val
elif node.operator == '-':
return -val
raise EvaluationError(f"Unsupported unary operator: '{node.operator}'")

if isinstance(node, BinaryOpNode):
left_val = self.evaluate(node.left)
right_val = self.evaluate(node.right)

if node.operator == '+':
return left_val + right_val
elif node.operator == '-':
return left_val - right_val
elif node.operator == '*':
return left_val * right_val
elif node.operator == '/':
if right_val == 0:
raise EvaluationError("Division by zero")
return left_val / right_val
elif node.operator == '%':
if right_val == 0:
raise EvaluationError("Modulo by zero")
return left_val % right_val
elif node.operator in ('^', '**'):
try:
res = left_val ** right_val
if isinstance(res, complex):
raise EvaluationError("Complex numbers result not supported in real domain")
return res
except OverflowError:
raise EvaluationError("Numerical overflow in exponentiation")

raise EvaluationError(f"Unsupported binary operator: '{node.operator}'")

if isinstance(node, FunctionCallNode):
fn_name = node.name.lower()
if fn_name not in self.BUILTIN_FUNCTIONS:
raise EvaluationError(f"Unknown or unsupported function: '{node.name}'")

args = [self.evaluate(arg) for arg in node.args]
func = self.BUILTIN_FUNCTIONS[fn_name]

try:
result = func(*args)
return result
except TypeError as e:
raise EvaluationError(f"Invalid arguments for function '{node.name}': {e}")
except ValueError as e:
raise EvaluationError(f"Mathematical domain error in function '{node.name}': {e}")

raise EvaluationError(f"Unknown AST node type: {type(node).__name__}")


def evaluate_expression(expression: str, variables: Dict[str, Union[int, float]] = None) -> Union[int, float]:
"""Helper function to tokenize, parse, and evaluate a math expression string."""
from expression_parser.tokenizer import Tokenizer
from expression_parser.parser import Parser

tokenizer = Tokenizer(expression)
tokens = tokenizer.tokenize()
parser = Parser(tokens)
ast = parser.parse()
evaluator = Evaluator(variables)
return evaluator.evaluate(ast)
98 changes: 98 additions & 0 deletions expression_parser/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Interactive CLI REPL and runner for Mathematical Expression Parser & Evaluator.
"""

import sys
import os

# Ensure project root is in sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from typing import Dict, Union
from expression_parser import evaluate_expression, TokenizerError, ParseError, EvaluationError


def run_repl():
print("=" * 60)
print(" Mathematical Expression Parser & Evaluator (CLI REPL)")
print("=" * 60)
print("Commands:")
print(" exit / quit : Exit REPL")
print(" vars : Show current active variables")
print(" let x = val : Set variable value (e.g. let x = 10)")
print(" clear : Reset custom variables")
print("-" * 60)

user_vars: Dict[str, Union[int, float]] = {}

while True:
try:
inp = input("math> ").strip()
except (KeyboardInterrupt, EOFError):
print("\nExiting REPL. Goodbye!")
break

if not inp:
continue

if inp.lower() in ("exit", "quit"):
print("Goodbye!")
break

if inp.lower() == "vars":
if not user_vars:
print("No custom variables set. Builtins available: pi, e, tau.")
else:
print("Active variables:", user_vars)
continue

if inp.lower() == "clear":
user_vars.clear()
print("Custom variables cleared.")
continue

if inp.lower().startswith("let "):
parts = inp[4:].split("=", 1)
if len(parts) == 2:
var_name = parts[0].strip()
val_expr = parts[1].strip()

if not var_name.isidentifier():
print(f"Error: Invalid variable name '{var_name}'")
continue

try:
val = evaluate_expression(val_expr, user_vars)
user_vars[var_name] = val
print(f"Set {var_name} = {val}")
except Exception as e:
print(f"Error evaluating variable value: {e}")
continue
else:
print("Usage for assignment: let <var_name> = <expression>")
continue

try:
result = evaluate_expression(inp, user_vars)
print(f"Result: {result}")
except (TokenizerError, ParseError, EvaluationError) as e:
print(f"Error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")


def main():
if len(sys.argv) > 1:
expr = " ".join(sys.argv[1:])
try:
res = evaluate_expression(expr)
print(res)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
else:
run_repl()


if __name__ == "__main__":
main()
Loading
Loading