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,7 @@
# BACKLOG — eval phase
## Build backlog
<!-- Builder maintains this section -->
## Adversary findings
<!-- Adversary-only section. Items opened here; only Adversary closes them. -->

View File

@ -0,0 +1,15 @@
# BACKLOG — lex phase
## Build backlog
- [x] Implement `calc/lexer.py` with Token, LexError, tokenize()
- [x] Implement `calc/test_lexer.py` with full coverage
- [x] Claim D1 (numbers)
- [x] Claim D2 (operators & parens)
- [x] Claim D3 (whitespace & errors)
- [x] Claim D4 (tests green)
- [ ] Await Adversary PASS on all gates → write ## DONE to STATUS
## Adversary findings
<!-- Adversary owns this section -->

View File

@ -0,0 +1,16 @@
# BACKLOG — parse phase
## Build backlog
- [x] Implement `calc/parser.py` with Num, BinOp, Unary nodes and `parse()` function
- [x] Implement `calc/test_parser.py` with 24 tests covering D1D5
- [x] Verify D1: precedence (`1+2*3` tree structure confirmed)
- [x] Verify D2: left associativity (`8-3-2`, `8/4/2` tree structure confirmed)
- [x] Verify D3: parentheses (`(1+2)*3` tree structure confirmed)
- [x] Verify D4: unary minus (`-5`, `-(1+2)`, `3*-2` confirmed)
- [x] Verify D5: all 5 error cases raise ParseError
- [x] Run full test suite: 48/48 pass
## Adversary findings
(pending)

View File

@ -0,0 +1,12 @@
# DECISIONS (append-only, shared)
## lex phase
### Token type
Used a simple class with `__slots__` for efficiency and `__eq__` for easy test assertions. No namedtuple to allow future extension (e.g. position tracking for later phases).
### Number regex
`r'\d+\.?\d*|\.\d+'` — handles `42`, `3.14`, `10.`, `.5`. The alternation order matters: longest match first via `re.match` at position `i`.
### EOF token
`Token('EOF', None)` — value is None since EOF carries no semantic content; downstream parser phases can type-check on `kind == 'EOF'`.

View File

@ -0,0 +1,35 @@
# JOURNAL — eval phase
## Session 1
Implemented evaluator, CLI, and tests in one pass.
### evaluator.py design
`evaluate(node)` recursively walks `Num`, `BinOp`, `Unary` nodes. `EvalError` wraps division by zero so no bare `ZeroDivisionError` escapes the API. `_normalize(value)` converts whole-valued floats to int (satisfying D3).
### Test run
```
$ python -m unittest -q
Ran 69 tests in 0.113s
OK
```
Prior suite had 48 tests (lex+parse); 21 new tests from `test_evaluator.py`.
### CLI manual check
```
$ 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 "8-3-2" → 3
$ python calc.py "-2+5" → 3
$ python calc.py "2*-3" → -6
$ python calc.py "1/0" → error: division by zero (exit 1)
$ python calc.py "1 +" → error: unexpected token 'EOF' (exit 1)
```
All match plan-specified expected values.

View File

@ -0,0 +1,29 @@
# JOURNAL — lex phase (Builder)
## Session 1
### Implementation
Built `calc/lexer.py` with:
- `Token` class with `__slots__ = ('kind', 'value')` and `__eq__` for test assertions
- `LexError` exception
- `_NUMBER_RE = re.compile(r'\d+\.?\d*|\.\d+')` — matches integers, floats with/without leading dot
- `_SINGLE` dict mapping `+ - * / ( )` to kind strings
- `tokenize(src)`: iterates, skips whitespace, checks single-char ops, then tries number regex, raises LexError on unknown char; appends EOF at end
### Test run
```
$ python -m unittest -q
Ran 24 tests in 0.000s
OK
```
### Verification commands
```
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
Traceback (most recent call last):
...
calc.lexer.LexError: unexpected character '@' at position 2
```

View File

@ -0,0 +1,66 @@
# JOURNAL — parse phase (Builder)
## 2026-06-15 — Initial implementation
**Design decisions:**
Grammar used (standard precedence climbing via two layers):
```
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := '-' factor | primary
primary := NUMBER | '(' expr ')'
```
Left associativity falls out of the `while` loop pattern in `_expr()` and `_term()` — each new BinOp wraps the accumulated left node.
Unary minus in `_factor()` recurses to itself, so `--5``Unary('-', Unary('-', Num(5)))` correctly. The placement in `_factor()` (between `_term()` and `_primary()`) means `3 * -2` works: `_term()` calls `_factor()` for the right operand, which detects the MINUS.
**Verification runs:**
D1:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
```
D2:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
```
D3:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
```
D4:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
Unary('-', Num(5))
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
Unary('-', BinOp('+', Num(1), Num(2)))
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
BinOp('*', Num(3), Unary('-', Num(2)))
```
D5 (all 5 required error cases + extras):
```
OK: '1 +' -> ParseError: unexpected token 'EOF'
OK: '(1' -> ParseError: unclosed parenthesis, got 'EOF'
OK: '1 2' -> ParseError: unexpected token 'NUMBER'
OK: ')(' -> ParseError: unexpected token 'RPAREN'
OK: '' -> ParseError: empty input
```
D6:
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 48 tests in 0.002s
OK
```

View File

@ -0,0 +1,71 @@
# REVIEW — eval phase
Adversary cold-verification log. Each gate gets its own independent pass.
---
## eval/D1: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestArithmetic -v`
**Result:** 8 tests, 0 failures.
**Spot-check** (all from CLI):
- `2+3*4``14`
- `(2+3)*4``20`
- `8-3-2``3`
- `-2+5``3`
- `2*-3``-6`
**Break-it probes:** Associativity, precedence, and unary minus all checked. No defects found.
---
## eval/D2: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestDivision -v`
**Result:** 3 tests, 0 failures.
**Spot-check:**
- `7/2``3.5`
- `1/0` → error to stderr, exit 1 ✓
**Break-it probes:** Confirmed `EvalError` is raised (not bare `ZeroDivisionError`). Cold Python assertion test confirms `except EvalError` catches it and `except ZeroDivisionError` does not.
---
## eval/D3: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestResultType -v`
**Result:** 4 tests, 0 failures.
**Spot-check:**
- `4/2``2` (int, no `.0`) ✓
- `7/2``3.5` (float) ✓
**Break-it probes:** `isinstance` checks confirm `4/2` returns `int`, `7/2` returns `float`. `_normalize()` correctly converts whole-valued floats to int.
---
## eval/D4: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestCLI -v`
**Result:** 6 tests, 0 failures.
**Spot-check:**
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
- `python calc.py "1 +"` → error to stderr only (stdout empty), exit 1 ✓
- `python calc.py "1/0"` → error to stderr only, exit 1 ✓
- No tracebacks on any error input (verified with `""`, `"1+"`, `"++1"`, `"1 2"`)
---
## eval/D5: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest -q`
**Result:** 69 tests, 0 failures (~0.12s). All prior phases (lex + parse) still pass. No regression.
---
## Summary
All 5 eval gates PASS. No veto. No defects found.

View File

@ -0,0 +1,60 @@
# REVIEW — lex phase (Adversary)
## Status
All four gates verified PASS. No defects found.
## Gate verdicts
### lex/D1: PASS @2026-06-15T06:09:05Z
**Evidence:**
```
NUMBER 42 int
NUMBER 3.14 float
NUMBER 0.5 float
NUMBER 10.0 float
```
- `tokenize("42")``[Token(NUMBER, 42), Token(EOF, None)]` — int type confirmed
- All float variants (`3.14`, `.5`, `10.`) produce float values
- Regex `r'\d+\.?\d*|\.\d+'` correctly handles all cases
### lex/D2: PASS @2026-06-15T06:09:05Z
**Evidence:**
```
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
['PLUS', 'MINUS', 'STAR', 'SLASH', 'LPAREN', 'RPAREN', 'EOF']
```
- `tokenize("1+2*3")` → correct 6-token sequence
- All six operators `+-*/()` map to correct kinds
### lex/D3: PASS @2026-06-15T06:09:05Z
**Evidence:**
- `tokenize(" 12 + 3 ")``['NUMBER', 'PLUS', 'NUMBER', 'EOF']` — spaces skipped
- `tokenize("1\t+\t2")``['NUMBER', 'PLUS', 'NUMBER', 'EOF']` — tabs skipped
- `tokenize("1 @ 2")` raises `calc.lexer.LexError: unexpected character '@' at position 2`
- LexError message contains both the char (`@`) and position (`2`) ✓
- Letters (`abc`) and `$` also raise LexError ✓
### lex/D4: PASS @2026-06-15T06:09:05Z
**Evidence:**
```
----------------------------------------------------------------------
Ran 24 tests in 0.000s
OK
```
`python -m unittest -q` — 24 tests, 0 failures, 0 errors
## Probes run (independent / adversarial)
All probes ran from cold start in Adversary's own clone.
- `tokenize('')``['EOF']` — empty string handled ✓
- `tokenize('10.+.5')``[(NUMBER, 10.0), (PLUS, +), (NUMBER, 0.5), EOF]` — consecutive floats ✓
- `tokenize('-3')``[(MINUS, -), (NUMBER, 3), EOF]` — unary minus handled as operator ✓
- `tokenize('((1))')``[LPAREN, LPAREN, NUMBER, RPAREN, RPAREN, EOF]` — nested parens ✓
- `tokenize('abc')` raises LexError at position 0 ✓
- `tokenize('$')` raises LexError at position 0 ✓
- Plan's own verification command confirmed:
`tokenize('3.5*(1-2)')``[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
No defects found. No veto.

View File

@ -0,0 +1,106 @@
# REVIEW — parse phase (Adversary)
## Status
All six gates verified PASS. No defects found. No veto.
## Gate verdicts
### parse/D1: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) # 1+2*3
BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)) # 2*3+1
BinOp('-', Num(5), BinOp('*', Num(2), Num(3))) # 5-2*3
```
- `*`/`/` bind tighter than `+`/`-` in all three forms ✓
- `_term()` loop handles `STAR`/`SLASH` before `_expr()` handles `PLUS`/`MINUS`
**Adversarial probes (all correct):**
- `1+6/2``BinOp('+', Num(1), BinOp('/', Num(6), Num(2)))`
- `2*3+4*5``BinOp('+', BinOp('*', Num(2), Num(3)), BinOp('*', Num(4), Num(5)))`
- `10-2*3+1``BinOp('+', BinOp('-', Num(10), BinOp('*', Num(2), Num(3))), Num(1))`
---
### parse/D2: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) # 8-3-2
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) # 8/4/2
```
- Both levels associate left via `while` loops in `_expr()` and `_term()`
**Adversarial probes (all correct):**
- `1+2+3``BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))`
- `1-2+3``BinOp('+', BinOp('-', Num(1), Num(2)), Num(3))`
---
### parse/D3: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) # (1+2)*3
BinOp('*', Num(1), BinOp('+', Num(2), Num(3))) # 1*(2+3)
```
- `_primary()` calls `_expr()` recursively on paren contents ✓
**Adversarial probes (all correct):**
- `((1+2))*3``BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
- `(2*(3+4))``BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))`
---
### parse/D4: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
Unary('-', Num(5)) # -5
Unary('-', BinOp('+', Num(1), Num(2))) # -(1+2)
BinOp('*', Num(3), Unary('-', Num(2))) # 3 * -2
```
- `_factor()` recurses for unary minus; `_term()` calls `_factor()` so unary applies at factor level ✓
**Adversarial probes (all correct):**
- `--5``Unary('-', Unary('-', Num(5)))` ✓ (double unary via recursion)
- `-(2*3)``Unary('-', BinOp('*', Num(2), Num(3)))`
- `1 - -2``BinOp('-', Num(1), Unary('-', Num(2)))` ✓ (binary then unary)
- `-1 + 2``BinOp('+', Unary('-', Num(1)), Num(2))`
---
### parse/D5: PASS @2026-06-15T06:13:00Z
**Evidence (cold run) — all five required cases raise ParseError, not LexError/IndexError/etc.:**
```
OK: '1 +' -> ParseError: unexpected token 'EOF'
OK: '(1' -> ParseError: unclosed parenthesis, got 'EOF'
OK: '1 2' -> ParseError: unexpected token 'NUMBER'
OK: ')(' -> ParseError: unexpected token 'RPAREN'
OK: '' -> ParseError: empty input
```
**Adversarial error probes (all raise ParseError cleanly):**
- `'5 *'` → ParseError: unexpected token 'EOF' ✓
- `'()'` → ParseError: unexpected token 'RPAREN' ✓
- `'+5'` → ParseError: unexpected token 'PLUS' ✓ (unary plus unsupported)
- `'1++2'` → ParseError: unexpected token 'PLUS' ✓
- `'1 2 3'` → ParseError: unexpected token 'NUMBER' ✓
- `'((1)'` → ParseError: unclosed parenthesis, got 'EOF' ✓
---
### parse/D6: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
----------------------------------------------------------------------
Ran 48 tests in 0.001s
OK
```
`python -m unittest -q` — 48 tests (24 parser + 24 lexer), 0 failures, 0 errors ✓
---
## Probes run (independent / adversarial)
All probes ran from cold start in Adversary's own clone (work-adv/).
No defects found. No veto.

View File

@ -0,0 +1,97 @@
# STATUS — eval phase
## DONE
All gates D1D5 Adversary-verified PASS @2026-06-15T06:19:58Z. No veto. Sequence complete.
## Current state
All gates D1D5 implemented, claimed, and Adversary-verified.
## Gate D1 — CLAIMED, awaiting Adversary
**WHAT:** `evaluate(parse(tokenize(s)))` correct for +, -, *, /, precedence, parens, unary minus.
**HOW:** Run from repo root:
```bash
python -m unittest calc.test_evaluator.TestArithmetic -v
```
**EXPECTED:** 8 tests pass, 0 failures.
**WHERE:** `calc/evaluator.py`, `calc/test_evaluator.py` @ HEAD
Spot-check direct values:
```
python calc.py "2+3*4" # 14
python calc.py "(2+3)*4" # 20
python calc.py "8-3-2" # 3
python calc.py "-2+5" # 3
python calc.py "2*-3" # -6
```
---
## Gate D2 — CLAIMED, awaiting Adversary
**WHAT:** `/` is true division; division by zero raises `EvalError`, not bare `ZeroDivisionError`.
**HOW:**
```bash
python -m unittest calc.test_evaluator.TestDivision -v
python calc.py "7/2" # 3.5
python calc.py "1/0" # error to stderr, exit 1
```
**EXPECTED:** 3 tests pass; `7/2``3.5`; `1/0` → stderr + exit 1.
**WHERE:** `calc/evaluator.py` lines 3034, `calc/test_evaluator.py` class `TestDivision`.
---
## Gate D3 — CLAIMED, awaiting Adversary
**WHAT:** Whole-valued results → int (no `.0`); fractional → float.
**HOW:**
```bash
python -m unittest calc.test_evaluator.TestResultType -v
python calc.py "4/2" # 2
python calc.py "7/2" # 3.5
```
**EXPECTED:** 4 tests pass; `4/2``2`; `7/2``3.5`.
**WHERE:** `_normalize()` in `calc/evaluator.py` lines 1417; class `TestResultType` in `calc/test_evaluator.py`.
---
## Gate D4 — CLAIMED, awaiting Adversary
**WHAT:** `python calc.py "2+3*4"` prints `14`, exits 0; invalid expr prints to stderr and exits nonzero.
**HOW:**
```bash
python calc.py "2+3*4" # stdout: 14, exit 0
python calc.py "1 +" # stderr: error message, exit 1
python calc.py "1/0" # stderr: error message, exit 1
python -m unittest calc.test_evaluator.TestCLI -v
```
**EXPECTED:** 6 CLI tests pass; outputs as stated above.
**WHERE:** `calc.py` (repo root).
---
## Gate D5 — CLAIMED, awaiting Adversary
**WHAT:** Full suite (lex + parse + eval) passes with 0 failures; prior phases not regressed.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:** 69 tests in ~0.1s, OK, 0 failures.
**WHERE:** `calc/test_lexer.py`, `calc/test_parser.py`, `calc/test_evaluator.py`.

View File

@ -0,0 +1,86 @@
# STATUS — lex phase (Builder)
## DONE
All DoD gates (D1D4) verified PASS by Adversary @2026-06-15T06:09:05Z. No veto. Phase complete.
## Gates
### D1 — numbers: **CLAIMED** — awaiting Adversary
**WHAT:** `tokenize("42")``[Token(NUMBER, 42), Token(EOF, None)]`; floats `3.14`, `.5`, `10.` each produce one NUMBER token with numeric value (int or float).
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; t=tokenize('42'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('3.14'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('.5'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('10.'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
```
**EXPECTED:**
```
NUMBER 42 int
NUMBER 3.14 float
NUMBER 0.5 float
NUMBER 10.0 float
```
**WHERE:** `calc/lexer.py``tokenize()` function
---
### D2 — operators & parens: **CLAIMED** — awaiting Adversary
**WHAT:** `+ - * / ( )` tokenize to `PLUS MINUS STAR SLASH LPAREN RPAREN`; `tokenize("1+2*3")``NUMBER PLUS NUMBER STAR NUMBER EOF`
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('+-*/()')] )"
```
**EXPECTED:**
```
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
['PLUS', 'MINUS', 'STAR', 'SLASH', 'LPAREN', 'RPAREN', 'EOF']
```
**WHERE:** `calc/lexer.py``_SINGLE` dict and `tokenize()`
---
### D3 — whitespace & errors: **CLAIMED** — awaiting Adversary
**WHAT:** Spaces/tabs skipped; invalid chars raise `LexError` with offending char and position.
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize(' 12 + 3 ')])"
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
**EXPECTED:**
- First command: `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
- Second command: raises `calc.lexer.LexError: unexpected character '@' at position 2`
**WHERE:** `calc/lexer.py` — whitespace skip + `LexError` raise
---
### D4 — tests green: **CLAIMED** — awaiting Adversary
**WHAT:** `calc/test_lexer.py` runs 24 tests, 0 failures under `python -m unittest`
**HOW to verify:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
----------------------------------------------------------------------
Ran 24 tests in 0.000s
OK
```
**WHERE:** `calc/test_lexer.py`
---

View File

@ -0,0 +1,159 @@
# STATUS — parse phase (Builder)
## DONE
All DoD gates (D1D6) verified PASS by Adversary @2026-06-15T06:13:00Z. No veto. Phase complete.
## Current state
Implementation complete. `calc/parser.py` and `calc/test_parser.py` created. 48 tests pass (24 lex + 24 parser). All gates claimed and Adversary-verified.
## AST Shape Reference
```
Num(value) — value is int or float
BinOp(op, left, right) — op in ('+', '-', '*', '/')
Unary(op, operand) — op is '-'
```
`repr()` of each node class is canonical (used in test assertions and verification commands).
---
## Gates
### D1 — precedence: **CLAIMED**, awaiting Adversary
**WHAT:** `*` and `/` bind tighter than `+` and `-`. `1+2*3``BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` (not `BinOp('*', BinOp('+', ...), ...)`).
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('2*3+1')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('5-2*3')))"
```
**EXPECTED:**
```
BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
BinOp('-', Num(5), BinOp('*', Num(2), Num(3)))
```
**WHERE:** `calc/parser.py``_expr()` calls `_term()` which handles `*`/`/`; `_expr()` handles `+`/`-` at lower priority.
---
### D2 — left associativity: **CLAIMED**, awaiting Adversary
**WHAT:** Same-precedence operators associate left. `8-3-2``BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))`. `8/4/2``BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))`.
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
```
**EXPECTED:**
```
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
```
**WHERE:** `calc/parser.py``_expr()` and `_term()` use `while` loops, wrapping the accumulating left side.
---
### D3 — parentheses: **CLAIMED**, awaiting Adversary
**WHAT:** Parens override precedence. `(1+2)*3``BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`.
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1*(2+3)')))"
```
**EXPECTED:**
```
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
BinOp('*', Num(1), BinOp('+', Num(2), Num(3)))
```
**WHERE:** `calc/parser.py``_primary()` handles `LPAREN … RPAREN` by calling `_expr()` recursively.
---
### D4 — unary minus: **CLAIMED**, awaiting Adversary
**WHAT:** Leading and nested unary minus parses correctly. `-5``Unary('-', Num(5))`. `-(1+2)``Unary('-', BinOp('+', Num(1), Num(2)))`. `3 * -2``BinOp('*', Num(3), Unary('-', Num(2)))`.
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
```
**EXPECTED:**
```
Unary('-', Num(5))
Unary('-', BinOp('+', Num(1), Num(2)))
BinOp('*', Num(3), Unary('-', Num(2)))
```
**WHERE:** `calc/parser.py``_factor()` detects `MINUS` and recurses, returning `Unary('-', operand)`.
---
### D5 — errors: **CLAIMED**, awaiting Adversary
**WHAT:** Malformed input raises `ParseError` (not `LexError`, `IndexError`, etc.). Five required cases: `"1 +"`, `"(1"`, `"1 2"`, `")("`, `""`.
**HOW to verify:**
```bash
python -c "
from calc.lexer import tokenize
from calc.parser import parse, ParseError
errors = ['1 +', '(1', '1 2', ')(', '']
for src in errors:
try:
parse(tokenize(src))
print(f'FAIL: {src!r} did not raise')
except ParseError as e:
print(f'OK: {src!r} -> ParseError: {e}')
except Exception as e:
print(f'FAIL: {src!r} raised wrong exception: {type(e).__name__}: {e}')
"
```
**EXPECTED:**
```
OK: '1 +' -> ParseError: unexpected token 'EOF'
OK: '(1' -> ParseError: unclosed parenthesis, got 'EOF'
OK: '1 2' -> ParseError: unexpected token 'NUMBER'
OK: ')(' -> ParseError: unexpected token 'RPAREN'
OK: '' -> ParseError: empty input
```
**WHERE:** `calc/parser.py``parse()`, `_primary()`, `_primary()` RPAREN check.
---
### D6 — tests green: **CLAIMED**, awaiting Adversary
**WHAT:** `calc/test_parser.py` + `calc/test_lexer.py` combined: 48 tests, 0 failures under `python -m unittest`.
**HOW to verify:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
----------------------------------------------------------------------
Ran 48 tests in 0.002s
OK
```
**WHERE:** `calc/test_parser.py` (24 tests covering D1D5), `calc/test_lexer.py` (24 tests from lex phase).