# 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 ✓ ```