Educational calculator teaching FSMs (explicit transition table tokenizer) and DAGs (recursive descent parser with AST evaluation). Includes CLI with REPL, graphviz visualization, and 61 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
2.7 KiB
Python
121 lines
2.7 KiB
Python
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import pytest
|
|
from tokenizer import tokenize
|
|
from parser import Parser
|
|
from evaluator import evaluate, evaluate_traced, EvalError
|
|
|
|
|
|
def eval_expr(expr):
|
|
"""Helper: tokenize -> parse -> evaluate in one step."""
|
|
tokens = tokenize(expr)
|
|
ast = Parser(tokens).parse()
|
|
return evaluate(ast)
|
|
|
|
|
|
# ---------- Basic arithmetic ----------
|
|
|
|
def test_addition():
|
|
assert eval_expr("3 + 4") == 7.0
|
|
|
|
def test_subtraction():
|
|
assert eval_expr("10 - 3") == 7.0
|
|
|
|
def test_multiplication():
|
|
assert eval_expr("3 * 4") == 12.0
|
|
|
|
def test_division():
|
|
assert eval_expr("10 / 4") == 2.5
|
|
|
|
def test_power():
|
|
assert eval_expr("2 ^ 10") == 1024.0
|
|
|
|
|
|
# ---------- Precedence ----------
|
|
|
|
def test_standard_precedence():
|
|
assert eval_expr("3 + 4 * 2") == 11.0
|
|
|
|
def test_parentheses():
|
|
assert eval_expr("(3 + 4) * 2") == 14.0
|
|
|
|
def test_power_precedence():
|
|
assert eval_expr("2 * 3 ^ 2") == 18.0
|
|
|
|
def test_right_associative_power():
|
|
# 2^(2^3) = 2^8 = 256
|
|
assert eval_expr("2 ^ 2 ^ 3") == 256.0
|
|
|
|
|
|
# ---------- Unary minus ----------
|
|
|
|
def test_negation():
|
|
assert eval_expr("-5") == -5.0
|
|
|
|
def test_double_negation():
|
|
assert eval_expr("--5") == 5.0
|
|
|
|
def test_negation_with_power():
|
|
# -(3^2) = -9, not (-3)^2 = 9
|
|
assert eval_expr("-3 ^ 2") == -9.0
|
|
|
|
def test_negation_in_parens():
|
|
assert eval_expr("(-3) ^ 2") == 9.0
|
|
|
|
|
|
# ---------- Decimals ----------
|
|
|
|
def test_decimal_addition():
|
|
assert eval_expr("0.1 + 0.2") == pytest.approx(0.3)
|
|
|
|
def test_leading_dot():
|
|
assert eval_expr(".5 + .5") == 1.0
|
|
|
|
|
|
# ---------- Edge cases ----------
|
|
|
|
def test_nested_parens():
|
|
assert eval_expr("((((3))))") == 3.0
|
|
|
|
def test_complex_expression():
|
|
assert eval_expr("(2 + 3) * (7 - 2) / 5 ^ 1") == 5.0
|
|
|
|
def test_long_chain():
|
|
assert eval_expr("1 + 2 + 3 + 4 + 5") == 15.0
|
|
|
|
def test_mixed_operations():
|
|
assert eval_expr("2 + 3 * 4 - 6 / 2") == 11.0
|
|
|
|
|
|
# ---------- Division by zero ----------
|
|
|
|
def test_division_by_zero():
|
|
with pytest.raises(EvalError):
|
|
eval_expr("1 / 0")
|
|
|
|
def test_division_by_zero_in_expression():
|
|
with pytest.raises(EvalError):
|
|
eval_expr("5 + 3 / (2 - 2)")
|
|
|
|
|
|
# ---------- Traced evaluation ----------
|
|
|
|
def test_traced_returns_correct_result():
|
|
tokens = tokenize("3 + 4 * 2")
|
|
ast = Parser(tokens).parse()
|
|
result, steps = evaluate_traced(ast)
|
|
assert result == 11.0
|
|
assert len(steps) > 0
|
|
|
|
def test_traced_step_count():
|
|
"""A simple binary op has 3 evaluation events: left, right, combine."""
|
|
tokens = tokenize("3 + 4")
|
|
ast = Parser(tokens).parse()
|
|
result, steps = evaluate_traced(ast)
|
|
assert result == 7.0
|
|
# NumberNode(3), NumberNode(4), BinOp(+)
|
|
assert len(steps) == 3
|