artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs

This commit is contained in:
2026-06-16 15:39:42 +00:00
parent 64bc360fc0
commit bb85aa9f11
728 changed files with 34148 additions and 0 deletions

View File

@ -0,0 +1,16 @@
# BACKLOG — phase: eval (shared)
## Build backlog
- [x] Write `calc/evaluator.py` with `evaluate(node)` and `EvalError`
- [x] Write `calc/test_evaluator.py` (unittest, 15 cases, D1D3)
- [x] Write `calc.py` CLI (D4)
- [x] Verify 56 tests green (D5, no regression)
- [x] Write STATUS-eval.md with cold-verify commands
- [ ] Await Adversary PASS on all gates; write ## DONE
## Adversary findings
<!-- Adversary-owned — filed when breaking probes find issues -->
No findings yet.

View File

@ -0,0 +1,15 @@
# BACKLOG — phase: lex
## Build backlog (Builder-owned)
- [x] Create machine-docs phase files
- [x] Create calc/__init__.py
- [x] Create calc/lexer.py with Token, LexError, tokenize()
- [x] Create calc/test_lexer.py with unittest suite
- [x] Run tests locally and verify all pass (22/22)
- [x] Claim D1-D4
- [ ] Wait for Adversary PASS on all gates
- [ ] Write ## DONE to STATUS-lex.md
## Adversary findings
(Adversary-owned — do not edit)

View File

@ -0,0 +1,12 @@
# BACKLOG — phase: parse (Builder)
## Build backlog
- [x] Implement `calc/parser.py` with ParseError, Num, BinOp, Unary, parse()
- [x] Implement `calc/test_parser.py` with 19 tests covering D1D5
- [x] Run full test suite (41 tests green)
- [x] Write STATUS-parse.md with WHAT/HOW/EXPECTED/WHERE
- [x] Claim D1D6
## Adversary findings
(Read-only — Adversary writes here)

View File

@ -0,0 +1,11 @@
# DECISIONS — shared (append-only)
## 2026-06-15 — Token.value for non-numeric tokens
Decision: `Token.value` is `None` for operator/paren/EOF tokens, and `int` or `float` for NUMBER tokens.
Rationale: Parser phases only need the numeric value; operator/paren tokens carry no useful payload beyond their kind.
## 2026-06-15 — Float parsing strategy
Decision: Use Python's built-in `float()` for converting float literals. Detect float vs int by presence of `.` in the matched string.
Rationale: Handles edge cases like `.5` and `10.` correctly via stdlib, avoids manual parsing bugs.

View File

@ -0,0 +1,39 @@
# JOURNAL — phase: eval (Builder)
## 2026-06-15 — implementation
### Design
AST nodes from parse phase: `Num(value)`, `BinOp(op, left, right)`, `Unary(op, operand)`.
Evaluator is a recursive tree walk. Division uses Python's `/` (true division), then normalises
whole-valued floats to `int` (avoids trailing `.0` in CLI output without needing special fmt logic).
### Runs
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 56 tests in 0.002s
OK
```
```
$ python calc.py "2+3*4"
14
$ python calc.py "(2+3)*4"
20
$ python calc.py "7/2"
3.5
$ python calc.py "4/2"
2
$ python calc.py "1/0"
error: division by zero (stderr, exit 1)
$ python calc.py "1 +"
error: unexpected token 'EOF' (stderr, exit 1)
```
### Type normalisation choice
`int(result) if result == int(result) else result` — chose this over a `fmt` conversion in the CLI
so `evaluate` itself is honest about the value type and tests can assert `isinstance(result, int)`.

View File

@ -0,0 +1,57 @@
# JOURNAL — phase: lex
## 2026-06-15 — Start
Read phase plan. Mission: build `calc/lexer.py` with `tokenize()`, Token type, LexError, and unittest suite.
Plan:
- Token: dataclass with `kind` (str) and `value` (int | float | None)
- Kinds: NUMBER, PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN, EOF
- LexError: custom Exception with char + position
- tokenize(): iterate over chars, match numbers (int/float), operators, parens; skip whitespace; raise LexError on unknown char; append EOF at end
## 2026-06-15 — Implementation
Created:
- `calc/__init__.py` (empty package marker)
- `calc/lexer.py` — token scanner using manual char-by-char iteration
- `calc/test_lexer.py` — 22 unittest cases
Float parsing design: detect float vs int by presence of `.` in scanned substring. Handle `.5` (leading dot) and `10.` (trailing dot) correctly by scanning digits before/after the dot separately.
## 2026-06-15 — Test run
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 22 tests in 0.001s
OK
```
## 2026-06-15 — Verify commands
```
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
[('NUMBER', 42), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
[('NUMBER', 3.14), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
[('NUMBER', 0.5), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
[('NUMBER', 10.0), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
calc.lexer.LexError: unexpected character '@' at position 2
```
All plan verify commands produce expected output. Claiming D1D4.

View File

@ -0,0 +1,46 @@
# JOURNAL — phase: parse (Builder)
## 2026-06-15
### Design decisions
Chose a classic recursive-descent parser with separate grammar levels for precedence:
- `_expr` handles `+`/`-` (low precedence, left-associative via while-loop)
- `_term` handles `*`/`/` (high precedence, left-associative via while-loop)
- `_unary` handles prefix `-` (right-associative via recursion)
- `_primary` handles `NUMBER` and `(expr)`
Left-associativity comes naturally from the iterative loop pattern (each iteration wraps the accumulating `left` node deeper).
### Verification runs
```
$ python -m unittest -q
Ran 41 tests in 0.001s
OK
```
Key shape outputs verified:
```
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓ D1
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓ D2
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓ D2
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓ D3
-5 → Unary(MINUS, Num(5)) ✓ D4
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓ D4
3*-2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓ D4
```
D5 errors all raise `ParseError` (not `SyntaxError`, `ValueError`, etc.):
- `"1 +"` → ParseError: unexpected token 'EOF'
- `"(1"` → ParseError: expected ')', got 'EOF'
- `"1 2"` → ParseError: unexpected token 'NUMBER' after expression
- `")("` → ParseError: unexpected token 'RPAREN'
- `""` → ParseError: empty input
### Empty-string handling
`tokenize('')` returns `[Token('EOF')]`. The `parse()` function checks the first token;
if it's `EOF`, raises `ParseError("empty input")` immediately, avoiding an ambiguous
"unexpected token 'EOF'" message.

View File

@ -0,0 +1,52 @@
# REVIEW — phase: eval (Adversary)
## Status
Initialized @2026-06-15T01:43:40Z
Verdicts written @2026-06-15T01:58:00Z — ALL PASS
## Gate verdicts
### eval/D1: PASS @2026-06-15T01:58:00Z
All 5 required expressions correct from cold run:
- `2+3*4``14` (precedence: * before +) ✓
- `(2+3)*4``20` (parens override precedence) ✓
- `8-3-2``3` (left-associativity) ✓
- `-2+5``3` (unary minus leading) ✓
- `2*-3``-6` (unary minus in mul) ✓
Additional probes: `2*(3+4)-1`→13, `-(3+4)`→-7, `--5`→5 all correct.
### eval/D2: PASS @2026-06-15T01:58:00Z
- `7/2``3.5` (true division, not floor) ✓
- `1/0` raises `EvalError("division by zero")` — not bare `ZeroDivisionError`
- `0/0` also raises `EvalError` (edge case probed) ✓
- Error goes to stderr, exit code 1 ✓
- API-level check confirmed: `try/except EvalError` catches it, `ZeroDivisionError` does not ✓
### eval/D3: PASS @2026-06-15T01:58:00Z
- `4/2``2` (int, no trailing `.0`) ✓
- `7/2``3.5` (float) ✓
- `6/3``2`, `9/3``3`, `10/5``2` (whole-valued normalisation consistent) ✓
- Integer arithmetic stays `int`: `2+3` → int, `3*4` → int, `-5` → int ✓
- Type assertions: `assertIsInstance(calc("4/2"), int)` and `assertIsInstance(calc("7/2"), float)` both pass ✓
### eval/D4: PASS @2026-06-15T01:58:00Z
- `python calc.py "2+3*4"``14`, exit 0 ✓
- `python calc.py "(2+3)*4"``20`, exit 0 ✓
- `python calc.py "7/2"``3.5`, exit 0 ✓
- `python calc.py "4/2"``2`, exit 0 ✓
- `python calc.py "1/0"` → error to stderr, exit 1 ✓
- `python calc.py "1 +"` → error to stderr, exit 1 ✓
- `python calc.py ""``error: empty input`, exit 1 ✓
- `python calc.py "abc"` → exit 1 (LexError caught) ✓
- No tracebacks leak to stderr (grep for 'traceback' found nothing) ✓
- Exact output format confirmed: `[14]`, `[3.5]` — no extra whitespace ✓
### eval/D5: PASS @2026-06-15T01:58:00Z
- `python -m unittest -q``Ran 56 tests in 0.001s / OK`
- 41 lex+parse tests: still all green (no regression) ✓
- 15 new evaluator tests: all green ✓
- Test classes: TestArithmetic (8), TestDivision (4), TestResultType (3) ✓
## No findings, no VETO
All five gates PASS. No defects found. Builder may write ## DONE.

View File

@ -0,0 +1,56 @@
# REVIEW — phase `lex` (Adversary)
## Status
All 4 gates verified. Phase COMPLETE.
## Verdicts
### lex/D1: PASS @2026-06-15T01:35:56Z
Cold-run evidence:
```
tokenize('42') → [('NUMBER', 42), ('EOF', None)] — int ✓
tokenize('3.14') → [('NUMBER', 3.14), ('EOF', None)] — float ✓
tokenize('.5') → [('NUMBER', 0.5), ('EOF', None)] — float ✓
tokenize('10.') → [('NUMBER', 10.0), ('EOF', None)] — float ✓
type(tokenize('42')[0].value) == int ✓
type(tokenize('3.14')[0].value) == float ✓
```
Value is int for integers, float for floats. DoD fully met.
### lex/D2: PASS @2026-06-15T01:35:56Z
Cold-run evidence:
```
tokenize('1+2*3') → ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'] ✓
tokenize('+-*/()') → ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'] ✓
tokenize('3.5*(1-2)') → correct kind/value pairs ✓
```
All 6 operator/paren kinds present and correct.
### lex/D3: PASS @2026-06-15T01:35:56Z
Cold-run evidence:
```
tokenize(' 12 + 3 ') → NUMBER(12) PLUS NUMBER(3) EOF — spaces skipped ✓
tokenize('1\t+\t2') → NUMBER PLUS NUMBER EOF — tabs skipped ✓
tokenize('1 @ 2') → LexError: unexpected character '@' at position 2 ✓
tokenize('$5') → LexError: unexpected character '$' at position 0 ✓
tokenize('x') → LexError: unexpected character 'x' at position 0 ✓
```
Offending char and position both present in message. DoD fully met.
### lex/D4: PASS @2026-06-15T01:35:56Z
```
python -m unittest -q
Ran 22 tests in 0.001s
OK
```
Zero failures, zero errors. All 22 tests cover D1D3 including required cases:
`" 12 + 3 "`, `"3.5*(1-2)"`, and `"1 @ 2"` raising LexError.
## Informational finding (non-blocking)
**F1 — lone `.` raises `ValueError` not `LexError`:** `tokenize('.')` crashes with
`ValueError: could not convert string to float: '.'` instead of a `LexError`. The
DoD does not require this case to be handled, so this is NOT a blocker for any gate.
Noted for future phases that may extend the lexer.
No VETO. All DoD items independently verified.

View File

@ -0,0 +1,106 @@
# REVIEW — phase: parse (Adversary)
## Verdict: ALL GATES PASS @2026-06-15T01:42:00Z
Cold-verified from commit `23d0ae9` (work-adv clone, fresh shell, no cached state).
---
### D6 (tests green): PASS @2026-06-15T01:42:00Z
```
python -m unittest -q
Ran 41 tests in 0.001s
OK
```
41 tests (22 lexer + 19 parser), 0 failures. Tests assert on `repr()`/tree structure, not evaluation — satisfies the plan requirement.
---
### D1 (precedence): PASS @2026-06-15T01:42:00Z
```
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓
6-4/2 → BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2))) ✓
```
Re-derived independently: grammar `expr: term((+|-)term)*` / `term: unary((*|/)unary)*` guarantees `*`/`/` subtrees sit inside `+`/`-` nodes. Observed matches.
Extra adversarial probe:
```
1+2*3+4 → BinOp(PLUS, BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))), Num(4)) ✓
```
---
### D2 (left associativity): PASS @2026-06-15T01:42:00Z
```
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓
```
Extra probes:
```
1+2+3 → BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
2*3*4 → BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4)) ✓
4*3/2 → BinOp(SLASH, BinOp(STAR, Num(4), Num(3)), Num(2)) ✓
```
Left-associativity correctly implemented via iterative while-loop (not recursion).
---
### D3 (parentheses): PASS @2026-06-15T01:42:00Z
```
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
((2)) → Num(2) ✓
(-5) → Unary(MINUS, Num(5)) ✓
```
---
### D4 (unary minus): PASS @2026-06-15T01:42:00Z
```
-5 → Unary(MINUS, Num(5)) ✓
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓
3 * -2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓
--5 → Unary(MINUS, Unary(MINUS, Num(5))) ✓ (double unary)
2+-3 → BinOp(PLUS, Num(2), Unary(MINUS, Num(3))) ✓ (unary after binary)
```
Right-recursive `_unary` correctly handles chained negation.
---
### D5 (errors raise ParseError): PASS @2026-06-15T01:42:00Z
All five required cases verified — each raises `ParseError`, not `ValueError`, `IndexError`, or any other exception:
```
'1 +' → ParseError: unexpected token 'EOF' ✓
'(1' → ParseError: expected ')', got 'EOF' ✓
'1 2' → ParseError: unexpected token 'NUMBER' after expression ✓
')(' → ParseError: unexpected token 'RPAREN' ✓
'' → ParseError: empty input ✓
```
Extra adversarial error probes (all raise ParseError):
```
'*5' → ParseError: unexpected token 'STAR' ✓
'5*' → ParseError: unexpected token 'EOF' ✓
'()' → ParseError: unexpected token 'RPAREN' ✓
'(+5)' → ParseError: unexpected token 'PLUS' ✓
'1++2' → ParseError: unexpected token 'PLUS' ✓
```
---
## Summary
All six DoD gates D1D6 independently verified cold. No defects found. The grammar in STATUS exactly matches the implementation (`_expr`/`_term`/`_unary`/`_primary`). Node shape is stable and documented in the `parse()` docstring. No veto.
Builder may write `## DONE` to STATUS-parse.md.

View File

@ -0,0 +1,44 @@
# STATUS — phase: eval (Builder)
## DONE
Gate: D1 D2 D3 D4 D5 — Adversary PASS @2026-06-15T01:58:00Z. Phase complete.
## What was built
- `calc/evaluator.py``evaluate(node) -> int | float`; `EvalError` for bad nodes / division by zero
- `calc/test_evaluator.py` — 15 unittest cases covering D1D3
- `calc.py` (repo root) — CLI: `python calc.py "<expr>"`
## Commit SHA
See `git log --oneline -1` on origin/main after this push.
## Verify commands (cold — run from repo root)
```bash
python -m unittest -q
# Expected: Ran 56 tests in <N>s / OK
python calc.py "2+3*4" # Expected: 14
python calc.py "(2+3)*4" # Expected: 20
python calc.py "7/2" # Expected: 3.5
python calc.py "4/2" # Expected: 2
python calc.py "1/0" # Expected: prints error to stderr, exits 1
python calc.py "1 +" # Expected: prints error to stderr, exits 1
```
## Gate mapping
| Gate | DoD | Verify |
|------|-----|--------|
| D1 | arithmetic: `+ - * /`, precedence, parens, unary minus | `python -m unittest -q` (TestArithmetic) |
| D2 | true division; `EvalError` on div-by-zero (not bare `ZeroDivisionError`) | `python -m unittest -q` (TestDivision) |
| D3 | whole-valued → int (no `.0`); non-whole → float | `python -m unittest -q` (TestResultType) |
| D4 | `python calc.py "2+3*4"` → 14 exit 0; error → stderr + exit 1 | manual CLI commands above |
| D5 | 56 tests total (lex+parse+eval), 0 failures | `python -m unittest -q` |
## Result type rule (D3)
Division result is normalised: `result = left / right; return int(result) if result == int(result) else result`.
The CLI's `_fmt` calls `str(result)`, so `int` prints as `"2"` and `float` prints as `"3.5"`.

View File

@ -0,0 +1,70 @@
# STATUS — phase: lex (Builder)
## DONE
All gates verified PASS by Adversary @2026-06-15T01:35:56Z.
## Gate: D1, D2, D3, D4 — PASS
### WHAT is claimed
All four gates D1D4 are implemented and tested.
### HOW to verify (run from a fresh clone)
```bash
cd <repo>
python -m unittest -q
```
Expected: 22 tests, 0 failures, 0 errors.
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
```
Expected: `[('NUMBER', 42), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
```
Expected: `[('NUMBER', 3.14), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
```
Expected: `[('NUMBER', 0.5), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
```
Expected: `[('NUMBER', 10.0), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
```
Expected: `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
```
Expected: `[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
Expected: raises `calc.lexer.LexError: unexpected character '@' at position 2`
### WHERE (commit sha)
`7022854acf94b35ebc79cb48315e91b82c7cc4ec`
### Files
- `calc/__init__.py` — empty package marker
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
- `calc/test_lexer.py` — 22 unittest cases covering D1D3
### Gate checklist
- D1 (numbers): PASS @2026-06-15T01:35:56Z
- D2 (operators & parens): PASS @2026-06-15T01:35:56Z
- D3 (whitespace & errors): PASS @2026-06-15T01:35:56Z
- D4 (tests green): PASS @2026-06-15T01:35:56Z
## Informational (non-blocking)
- F1: lone `.` raises `ValueError` not `LexError` — not required by DoD, noted for future phases

View File

@ -0,0 +1,115 @@
# STATUS — phase: parse (Builder)
## DONE
## Gate: D1D6 — ALL PASS (Adversary-verified @2026-06-15T01:42:00Z)
### WHAT is claimed
All six gates D1D6 are implemented and tested.
- **D1 (precedence):** `*`/`/` bind tighter than `+`/`-`
- **D2 (left associativity):** Same-precedence ops associate left
- **D3 (parentheses):** Parens override precedence
- **D4 (unary minus):** Leading and nested unary minus
- **D5 (errors):** Five malformed inputs each raise `ParseError`
- **D6 (tests green):** 19 parser tests + 22 lexer tests = 41 total, 0 failures
### HOW to verify (run from a fresh clone)
**D6 — all tests green:**
```bash
python -m unittest -q
```
Expected output: `Ran 41 tests in 0.00xs` / `OK`
**D1 — precedence (structural check):**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
```
Expected: `BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('6-4/2')))"
```
Expected: `BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))`
**D2 — left associativity:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
```
Expected: `BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
```
Expected: `BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))`
**D3 — parentheses:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
```
Expected: `BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))`
**D4 — unary minus:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
```
Expected: `Unary(MINUS, Num(5))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
```
Expected: `Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
```
Expected: `BinOp(STAR, Num(3), Unary(MINUS, Num(2)))`
**D5 — errors (each must raise `ParseError`, not a different exception):**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 +'))"
# raises: ParseError: unexpected token 'EOF'
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('(1'))"
# raises: ParseError: expected ')', got 'EOF'
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 2'))"
# raises: ParseError: unexpected token 'NUMBER' after expression
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(')('))"
# raises: ParseError: unexpected token 'RPAREN'
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(''))"
# raises: ParseError: empty input
```
### EXPECTED AST shapes (re-derivable from grammar)
AST node types:
- `Num(value)` — numeric literal; value is int or float
- `BinOp(op, left, right)` — op in `{'PLUS','MINUS','STAR','SLASH'}`
- `Unary(op, operand)` — op == `'MINUS'`
Grammar used (drives precedence + associativity):
```
expr : term (('+' | '-') term)* ← left-assoc, low precedence
term : unary (('*' | '/') unary)* ← left-assoc, high precedence
unary : '-' unary | primary ← right-assoc chain, prefix only
primary : NUMBER | '(' expr ')'
```
### WHERE (commit sha)
`23d0ae9` (claim(D1,D2,D3,D4,D5,D6): parser complete, all 41 tests green)
### Files
- `calc/parser.py` — ParseError, Num, BinOp, Unary, _Parser, parse()
- `calc/test_parser.py` — 19 unittest cases covering D1D5
### Gate checklist
- D1 (precedence): CLAIMED
- D2 (left associativity): CLAIMED
- D3 (parentheses): CLAIMED
- D4 (unary minus): CLAIMED
- D5 (errors): CLAIMED
- D6 (tests green): CLAIMED