Add expression-evaluator: DAGs & state machines tutorial project
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>
This commit is contained in:
136
python/expression-evaluator/tests/test_parser.py
Normal file
136
python/expression-evaluator/tests/test_parser.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import pytest
|
||||
from tokenizer import tokenize, TokenType
|
||||
from parser import Parser, ParseError, NumberNode, BinOpNode, UnaryOpNode
|
||||
|
||||
|
||||
def parse(expr):
|
||||
"""Helper: tokenize and parse in one step."""
|
||||
return Parser(tokenize(expr)).parse()
|
||||
|
||||
|
||||
# ---------- Basic parsing ----------
|
||||
|
||||
def test_parse_number():
|
||||
ast = parse("42")
|
||||
assert isinstance(ast, NumberNode)
|
||||
assert ast.value == 42.0
|
||||
|
||||
def test_parse_decimal():
|
||||
ast = parse("3.14")
|
||||
assert isinstance(ast, NumberNode)
|
||||
assert ast.value == 3.14
|
||||
|
||||
def test_parse_addition():
|
||||
ast = parse("3 + 4")
|
||||
assert isinstance(ast, BinOpNode)
|
||||
assert ast.op == TokenType.PLUS
|
||||
assert isinstance(ast.left, NumberNode)
|
||||
assert isinstance(ast.right, NumberNode)
|
||||
|
||||
|
||||
# ---------- Precedence ----------
|
||||
|
||||
def test_multiply_before_add():
|
||||
"""3 + 4 * 2 should parse as 3 + (4 * 2)."""
|
||||
ast = parse("3 + 4 * 2")
|
||||
assert ast.op == TokenType.PLUS
|
||||
assert isinstance(ast.right, BinOpNode)
|
||||
assert ast.right.op == TokenType.MULTIPLY
|
||||
|
||||
def test_power_before_multiply():
|
||||
"""2 * 3 ^ 4 should parse as 2 * (3 ^ 4)."""
|
||||
ast = parse("2 * 3 ^ 4")
|
||||
assert ast.op == TokenType.MULTIPLY
|
||||
assert isinstance(ast.right, BinOpNode)
|
||||
assert ast.right.op == TokenType.POWER
|
||||
|
||||
def test_parentheses_override_precedence():
|
||||
"""(3 + 4) * 2 should parse as (3 + 4) * 2."""
|
||||
ast = parse("(3 + 4) * 2")
|
||||
assert ast.op == TokenType.MULTIPLY
|
||||
assert isinstance(ast.left, BinOpNode)
|
||||
assert ast.left.op == TokenType.PLUS
|
||||
|
||||
|
||||
# ---------- Associativity ----------
|
||||
|
||||
def test_left_associative_subtraction():
|
||||
"""10 - 3 - 2 should parse as (10 - 3) - 2."""
|
||||
ast = parse("10 - 3 - 2")
|
||||
assert ast.op == TokenType.MINUS
|
||||
assert isinstance(ast.left, BinOpNode)
|
||||
assert ast.left.op == TokenType.MINUS
|
||||
assert isinstance(ast.right, NumberNode)
|
||||
|
||||
def test_power_right_associative():
|
||||
"""2 ^ 3 ^ 4 should parse as 2 ^ (3 ^ 4)."""
|
||||
ast = parse("2 ^ 3 ^ 4")
|
||||
assert ast.op == TokenType.POWER
|
||||
assert isinstance(ast.left, NumberNode)
|
||||
assert isinstance(ast.right, BinOpNode)
|
||||
assert ast.right.op == TokenType.POWER
|
||||
|
||||
|
||||
# ---------- Unary minus ----------
|
||||
|
||||
def test_unary_minus():
|
||||
ast = parse("-3")
|
||||
assert isinstance(ast, UnaryOpNode)
|
||||
assert ast.operand.value == 3.0
|
||||
|
||||
def test_double_negation():
|
||||
ast = parse("--3")
|
||||
assert isinstance(ast, UnaryOpNode)
|
||||
assert isinstance(ast.operand, UnaryOpNode)
|
||||
assert ast.operand.operand.value == 3.0
|
||||
|
||||
def test_unary_minus_precedence():
|
||||
"""-3^2 should parse as -(3^2), not (-3)^2."""
|
||||
ast = parse("-3 ^ 2")
|
||||
assert isinstance(ast, UnaryOpNode)
|
||||
assert isinstance(ast.operand, BinOpNode)
|
||||
assert ast.operand.op == TokenType.POWER
|
||||
|
||||
def test_unary_minus_in_expression():
|
||||
"""2 * -3 should parse as 2 * (-(3))."""
|
||||
ast = parse("2 * -3")
|
||||
assert ast.op == TokenType.MULTIPLY
|
||||
assert isinstance(ast.right, UnaryOpNode)
|
||||
|
||||
|
||||
# ---------- Nested parentheses ----------
|
||||
|
||||
def test_nested_parens():
|
||||
ast = parse("((3))")
|
||||
assert isinstance(ast, NumberNode)
|
||||
assert ast.value == 3.0
|
||||
|
||||
def test_complex_nesting():
|
||||
"""((2 + 3) * (7 - 2))"""
|
||||
ast = parse("((2 + 3) * (7 - 2))")
|
||||
assert isinstance(ast, BinOpNode)
|
||||
assert ast.op == TokenType.MULTIPLY
|
||||
|
||||
|
||||
# ---------- Errors ----------
|
||||
|
||||
def test_missing_rparen():
|
||||
with pytest.raises(ParseError):
|
||||
parse("(3 + 4")
|
||||
|
||||
def test_empty_expression():
|
||||
with pytest.raises(ParseError):
|
||||
parse("")
|
||||
|
||||
def test_trailing_operator():
|
||||
with pytest.raises(ParseError):
|
||||
parse("3 +")
|
||||
|
||||
def test_empty_parens():
|
||||
with pytest.raises(ParseError):
|
||||
parse("()")
|
||||
Reference in New Issue
Block a user