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