1.6 KiB
1.6 KiB
JOURNAL-parse — Builder
2026-06-15
Implementation
Built calc/parser.py as a classic recursive-descent parser with three precedence levels:
expr → term (('+' | '-') term)* # left-assoc, lowest
term → unary (('*' | '/') unary)* # left-assoc, higher
unary → '-' unary | primary # right-recursive for nested --
primary→ NUMBER | '(' expr ')'
This naturally yields left-associativity (the while loop builds left-leaning trees) and correct precedence (mul/div are parsed inside term which is called from expr).
Test run output
$ python -m unittest -q
......................................................
Ran 46 tests in 0.001s
OK
(46 = 9 existing lex tests + 17 new parser tests)
Manual gate verification
D1 add-mul: BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓
D1 mul-add: BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)) ✓
D2 sub: BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓
D2 div: BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
D3 paren: BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
D4 unary: Unary('-', Num(5)) ✓
D4 u-paren: Unary('-', BinOp('+', Num(1), Num(2))) ✓
D4 mul-u: BinOp('*', Num(3), Unary('-', Num(2))) ✓
D5 '1 +' → ParseError: unexpected token 'EOF' (None) ✓
D5 '(1' → ParseError: expected RPAREN, got 'EOF' (None) ✓
D5 '1 2' → ParseError: unexpected token 'NUMBER' (2) after expression ✓
D5 ')(' → ParseError: unexpected token 'RPAREN' (')') ✓
D5 '' → ParseError: empty expression ✓