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 (Adversary)
## Adversary findings
_No findings — all D1D5 gates verified PASS. No defects found._
---
## Adversary break-it probes (planned)
When gates are claimed, I will test:
- D1: `2+3*4`→14, `(2+3)*4`→20, `8-3-2`→3, `-2+5`→3, `2*-3`→-6, plus edge cases like `--5`, `0*100`, nested parens
- D2: `7/2`→3.5 (true division); `1/0` raises EvalError (not ZeroDivisionError); `0/0` likewise
- D3: `4/2``2` (no .0); `7/2``3.5`; `6/3``2`; `1/3``0.333...`
- D4: CLI exit 0 for valid; non-zero + stderr for invalid; traceback must NOT appear
- D5: full `python -m unittest -q` including prior lexer+parser tests; check test count

View File

@ -0,0 +1,16 @@
# BACKLOG — Phase lex
## Build backlog
- [x] D1 — numbers: INTEGER and FLOAT tokenization — CLAIMED
- [x] D2 — operators & parens — CLAIMED
- [x] D3 — whitespace & errors (LexError) — CLAIMED
- [x] D4 — tests green (18 tests, 0 failures) — CLAIMED
## Adversary findings
### AF-1 (open): ValueError leaks on malformed number tokens
- `tokenize('1.2.3')``ValueError` (not `LexError`)
- `tokenize('.')``ValueError` (not `LexError`)
- Non-blocking for phase `lex` DoD; recommend fix before parser phase consumes these tokens.
- Repro and details in REVIEW-lex.md § AF-1.

View File

@ -0,0 +1,12 @@
# BACKLOG — Phase parse
## Build backlog
- [x] Write calc/parser.py with parse(tokens) -> Node
- [x] Write calc/test_parser.py with unittest coverage of D1-D5
- [ ] Claim D1 (precedence)
- [ ] Claim D2 (left associativity)
- [ ] Claim D3 (parentheses)
- [ ] Claim D4 (unary minus)
- [ ] Claim D5 (errors)
- [ ] Claim D6 (tests green)

View File

@ -0,0 +1,4 @@
# DECISIONS — Phase lex (shared, append-only)
## 2026-06-15 — Adversary initialized
Adversary loop started monitoring for Builder gate claims on phase `lex`.

View File

@ -0,0 +1,43 @@
# JOURNAL — Phase eval
## Implementation notes
### evaluator.py
Built `evaluate(node) -> int | float` walking `Num`, `BinOp`, `Unary` nodes.
- `Num`: returns `node.value` (already int or float from lexer)
- `Unary('-')`: returns `-evaluate(operand)` recursively
- `BinOp(+,-,*)`: straightforward arithmetic (Python int+int=int)
- `BinOp(/)`: true division via `left / right`; checks `right == 0``EvalError`; if `result == int(result)` → return `int(result)` (D3 rule), else return float
D3 rule: after true division, `4/2 = 2.0`; `2.0 == int(2.0)` is True, so return `int(2.0) = 2`. `7/2 = 3.5`; `3.5 != 3`, so return `3.5` (float).
### calc.py
Top-level CLI at repo root. Imports from `calc.*`. Catches `LexError`, `ParseError`, `EvalError` → prints to stderr, exits 1. Traceback never escapes.
### test_evaluator.py
26 new tests across D1D4 categories:
- D1 (arithmetic): 11 tests including all DoD examples plus edge cases
- D2 (division): 4 tests including EvalError-not-ZeroDivisionError check
- D3 (result type): 5 tests including `str()` formatting
- D4 (CLI): 6 tests using subprocess
### Verification output (local)
```
python -m unittest -q
Ran 63 tests in 0.228s
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' (None) (stderr, exit 1)
```

View File

@ -0,0 +1,30 @@
# JOURNAL — Phase lex (Adversary)
## 2026-06-15T00:00:00Z — Initialized
Adversary loop started. Read phase plan. No Builder activity yet. Watching for gate claims.
---
# JOURNAL — Phase lex (Builder)
## 2026-06-15 — Session 1
Implemented `calc/lexer.py` with `Token` dataclass, `LexError`, and `tokenize()`.
Test run:
```
$ python -m unittest -q
Ran 18 tests in 0.000s
OK
```
Verification commands (from plan):
```
$ 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')" 2>&1
calc.lexer.LexError: unexpected character '@' at position 2
```
Committed as `ab0332e`. All D1D4 conditions met in one session.

View File

@ -0,0 +1,33 @@
# JOURNAL — Phase parse
## 2026-06-15 — Implementation
Built `calc/parser.py` using a classic recursive-descent approach with three precedence levels:
1. `_expr` handles `+`/`-` (lowest precedence, left-assoc via loop)
2. `_term` handles `*`/`/` (medium precedence, left-assoc via loop)
3. `_unary` handles leading `-` (right-assoc via recursion)
4. `_primary` handles numbers and parenthesized expressions
Left-associativity falls out naturally from the `while` loops in `_expr` and `_term` — each iteration wraps the accumulated `node` as the left child of a new `BinOp`, building a left-leaning tree.
Test verification output:
```
D1: BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))
D2: BinOp(op='-', left=BinOp(op='-', left=Num(value=8), right=Num(value=3)), right=Num(value=2))
D3: BinOp(op='*', left=BinOp(op='+', left=Num(value=1), right=Num(value=2)), right=Num(value=3))
D4a: Unary(op='-', operand=Num(value=5))
D4b: Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))
D4c: BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))
D5 errors:
OK ParseError for '1 +': unexpected token 'EOF' (None)
OK ParseError for '(1': unclosed parenthesis, got 'EOF'
OK ParseError for '1 2': unexpected token 'NUMBER' (2)
OK ParseError for ')(': unexpected token 'RPAREN' (')')
OK ParseError for '': empty input
D6: Ran 37 tests in 0.001s OK
```
All 6 gates claimed and pushed. Awaiting Adversary verification.

View File

@ -0,0 +1,68 @@
# REVIEW — Phase eval (Adversary)
## D1: PASS @2026-06-15T05:01Z
Cold-ran all five DoD arithmetic checks from the plan:
- `2+3*4` → 14 ✓ (precedence: `*` before `+`)
- `(2+3)*4` → 20 ✓ (parens override precedence)
- `8-3-2` → 3 ✓ (left-associativity)
- `-2+5` → 3 ✓ (unary minus)
- `2*-3` → -6 ✓ (unary minus in binary context)
Break-it probes:
- `--5` → 5 ✓ (double unary, recursive)
- `((2+3))` → 5 ✓ (nested parens)
- `1+2+3+4` → 10 ✓ (chain addition)
- `2*3+4/2` → 8 ✓ (mixed precedence, `4/2` → int 2, `6+2` → int 8)
### D2: PASS @2026-06-15T05:01Z
- `7/2` → 3.5 (true division) ✓
- `1/0` → raises `EvalError("division by zero")`, NOT `ZeroDivisionError`
- Break-it: `0/0``EvalError` ✓ (zero-zero handled by same `right == 0` check)
- Break-it: `-1/0``EvalError`
Code review: `evaluator.py:3031` — explicit `if right == 0: raise EvalError(...)` before Python's `/` operator, so `ZeroDivisionError` can never escape. Correct.
### D3: PASS @2026-06-15T05:01Z
- `4/2``2` (type `int`) ✓
- `7/2``3.5` (type `float`) ✓
- Break-it: `6/3``2` int ✓
- Break-it: `0/1``0` int ✓
- Break-it: `2+3` (no division) → `5` int ✓ (integer arithmetic always stays int)
- Break-it: `1/3``0.333...` float ✓
Code review: `evaluator.py:3335``if result == int(result): return int(result)` applied only in the `/` branch. Correct scope.
CLI check: `python calc.py '4/2'``2` (no `.0`) ✓; `python calc.py '7/2'``3.5`
### D4: PASS @2026-06-15T05:01Z
Valid expressions:
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
- `python calc.py "(2+3)*4"` → stdout `20`, exit 0 ✓
- `python calc.py "7/2"` → stdout `3.5`, exit 0 ✓
- `python calc.py "4/2"` → stdout `2`, exit 0 ✓
Error cases:
- `python calc.py "1/0"` → stderr `error: division by zero`, exit 1, stdout empty ✓
- `python calc.py "1 +"` → stderr `error: unexpected token 'EOF' (None)`, exit 1, stdout empty ✓
Break-it probes:
- No `Traceback` in stderr for either error case ✓
- No-arg case (`python calc.py`) → stderr `usage: python calc.py <expression>`, exit 1 ✓
- `LexError` also caught (imported and handled in `calc.py:17`) ✓
### D5: PASS @2026-06-15T05:01Z
Cold-ran: `python -m unittest -q`
Output: `Ran 63 tests in 0.232s OK` — 0 failures ✓
Prior suite (37 lex+parse tests) still passes; 26 new evaluator tests added. No regressions.
Plan's exact Verify-section commands all ran with matching expected outputs.
## No adversary findings — all DoD gates verified PASS
All D1D5 gates independently verified with break-it probes. No defects found. Builder may mark DONE.

View File

@ -0,0 +1,58 @@
# REVIEW — Phase lex (Adversary)
## Verdicts
### lex/D1: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `tokenize('42')``NUMBER 42 int` ✓ (int type confirmed)
- `tokenize('3.14')``NUMBER 3.14 float` ✓ (float type confirmed)
- `tokenize('.5')``NUMBER 0.5` ✓ (leading dot)
- `tokenize('10.')``NUMBER 10.0` ✓ (trailing dot → float)
- EOF appended in all cases ✓
---
### lex/D2: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `tokenize('1+2*3')``[('NUMBER', 1), ('PLUS', '+'), ('NUMBER', 2), ('STAR', '*'), ('NUMBER', 3), ('EOF', None)]`
- Matches expected exactly ✓
- All 6 operator/paren kinds verified in test suite ✓
---
### lex/D3: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `tokenize(' 12 + 3 ')``['NUMBER', 'PLUS', 'NUMBER', 'EOF']` ✓ (spaces skipped)
- `tokenize('1 @ 2')` raises `calc.lexer.LexError: unexpected character '@' at position 2`
- Offending character `'@'` in message ✓
- Position `2` in message ✓
- Letters (`abc`), `$` also raise `LexError` per test suite ✓
---
### lex/D4: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `python -m unittest -q``Ran 18 tests in 0.000s OK`
- `tokenize('3.5*(1-2)')``[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]` ✓ (exact plan match)
- `tokenize('1 @ 2')` raises `LexError` (exit 1) ✓
- Required test cases present: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"`
- 18 tests, 0 failures ✓
---
## Adversary findings (non-blocking for this phase)
### AF-1: `ValueError` leaks on malformed number tokens
**Repro:**
```
python -c "from calc.lexer import tokenize; tokenize('1.2.3')"
# → ValueError: could not convert string to float: '1.2.3'
python -c "from calc.lexer import tokenize; tokenize('.')"
# → ValueError: could not convert string to float: '.'
```
The number-scanning loop (`ch.isdigit() or ch == '.'`) greedily consumes all digits and dots, then hands the raw span to `float()` which raises `ValueError` on malformed input like `1.2.3` or bare `.`. These should raise `LexError` for consistency — the caller can't distinguish a lexer malfunction from a Python type error.
**Severity:** Not blocking — the DoD only requires `LexError` for invalid *characters* (`@`, `$`, letters). `1.2.3` and `.` are outside the explicit D1/D3 test cases. However, the parser phase will likely encounter these and must handle them.
**Recommendation:** Wrap the `float(raw)` call in a `try/except ValueError` and re-raise as `LexError`. Flag for builder attention in BUILDER-INBOX.

View File

@ -0,0 +1,41 @@
# REVIEW — Phase parse (Adversary)
## Verdicts
### D1: PASS @2026-06-15T05:00Z
Cold-ran: `python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"`
Got: `BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))`
Matches expected exactly. Break-it: `3*2+1``BinOp(+, BinOp(*, 3, 2), 1)` (correct — `*` binds tighter from left too).
### D2: PASS @2026-06-15T05:00Z
Cold-ran: `8-3-2``BinOp(-, BinOp(-, Num(8), Num(3)), Num(2))`
`8/4/2``BinOp(/, BinOp(/, Num(8), Num(4)), Num(2))`
Break-it: `10-3-2-1` → fully left-assoc triple nesting ✓. Grammar's while-loop guarantees left-assoc.
### D3: PASS @2026-06-15T05:00Z
Cold-ran: `(1+2)*3``BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))`
Break-it: `((2+3))*4``BinOp(*, BinOp(+, Num(2), Num(3)), Num(4))` ✓ (nested parens unwrap cleanly).
### D4: PASS @2026-06-15T05:00Z
Cold-ran all three required cases:
- `-5``Unary(op='-', operand=Num(value=5))`
- `-(1+2)``Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))`
- `3 * -2``BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))`
Break-it: `--5``Unary('-', Unary('-', Num(5)))` (recursive unary works, double-negation parses correctly).
### D5: PASS @2026-06-15T05:00Z
Cold-ran all five required error cases — each raised `ParseError`, none raised a different exception:
- `'1 +'``ParseError: unexpected token 'EOF' (None)`
- `'(1'``ParseError: unclosed parenthesis, got 'EOF'`
- `'1 2'``ParseError: unexpected token 'NUMBER' (2)`
- `')('``ParseError: unexpected token 'RPAREN' (')')`
- `''``ParseError: empty input`
Break-it: `1+2)``ParseError: unexpected token 'RPAREN' (')')` ✓ (trailing paren caught by EOF check).
### D6: PASS @2026-06-15T05:00Z
Cold-ran: `python -m unittest -q`
Output: `Ran 37 tests in 0.001s OK` (18 lexer + 19 parser, 0 failures) ✓
## No adversary findings — all DoD gates verified PASS
All D1D6 gates verified independently. No defects found. Builder may mark DONE.

View File

@ -0,0 +1,115 @@
# STATUS — Phase eval
## Gate D1 CLAIMED — awaiting Adversary
**WHAT:** `evaluate(parse(tokenize(s)))` is correct for `+ - * /`, precedence, parens, and unary minus.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate
def c(s): return evaluate(parse(tokenize(s)))
assert c('2+3*4') == 14
assert c('(2+3)*4') == 20
assert c('8-3-2') == 3
assert c('-2+5') == 3
assert c('2*-3') == -6
print('D1 OK')
"
```
**EXPECTED:** prints `D1 OK`, no assertion errors.
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
---
## Gate D2 CLAIMED — awaiting Adversary
**WHAT:** `/` is true division (`"7/2"` → 3.5). Division by zero raises `EvalError`, not `ZeroDivisionError`.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate, EvalError
def c(s): return evaluate(parse(tokenize(s)))
assert c('7/2') == 3.5
try:
c('1/0')
assert False, 'no error raised'
except EvalError:
pass
except ZeroDivisionError:
assert False, 'bare ZeroDivisionError escaped'
print('D2 OK')
"
```
**EXPECTED:** prints `D2 OK`, no assertion errors.
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
---
## Gate D3 CLAIMED — awaiting Adversary
**WHAT:** Whole-valued results print without `.0` (`"4/2"``2`), non-whole as float (`"7/2"``3.5`).
Rule: after division, if `result == int(result)`, return `int(result)`; otherwise return `float`.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate
def c(s): return evaluate(parse(tokenize(s)))
r1 = c('4/2'); assert r1 == 2 and isinstance(r1, int), f'got {r1!r}'
r2 = c('7/2'); assert isinstance(r2, float) and r2 == 3.5, f'got {r2!r}'
print('D3 OK')
"
python calc.py '4/2' # must print: 2
python calc.py '7/2' # must print: 3.5
```
**EXPECTED:** `D3 OK`, then `2`, then `3.5`.
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
---
## Gate D4 CLAIMED — awaiting Adversary
**WHAT:** `python calc.py "2+3*4"` prints `14` and exits 0; invalid expression prints error to stderr and exits non-zero (no traceback).
**HOW:**
```bash
python calc.py "2+3*4" # stdout: 14, exit 0
python calc.py "(2+3)*4" # stdout: 20, exit 0
python calc.py "7/2" # stdout: 3.5, exit 0
python calc.py "4/2" # stdout: 2, exit 0
python calc.py "1/0" # stderr: error: ..., exit non-zero, stdout empty
python calc.py "1 +" # stderr: error: ..., exit non-zero, stdout empty
```
**EXPECTED:** exactly as above — no Python traceback in stderr, error message starts with `error:`.
**WHERE:** `calc.py` @ commit `7167e33`
---
## Gate D5 CLAIMED — awaiting Adversary
**WHAT:** Full test suite passes: `python -m unittest -q`, 0 failures. New `calc/test_evaluator.py` covers D1D4. Prior lex+parse suite (37 tests) still passes (no regression). Total: 63 tests.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 63 tests in X.XXXs
OK
```
**WHERE:** `calc/test_evaluator.py` (26 new tests) + `calc/test_lexer.py` + `calc/test_parser.py` @ commit `7167e33`

View File

@ -0,0 +1,90 @@
# STATUS — Phase lex
## DONE
All D1D4 gates PASS (Adversary-verified @2026-06-15T05:05Z).
Post-DoD fix applied: AF-1 (`ValueError` on malformed numbers now raises `LexError`).
---
## Adversary
Initialized. Watching for Builder gate claims.
## Builder
### Gate D1 CLAIMED — PASS
**WHAT:** Integers and floats tokenize to NUMBER tokens with correct Python type.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; r=tokenize('42'); print(r[0].kind, r[0].value, type(r[0].value).__name__)"
python -c "from calc.lexer import tokenize; r=tokenize('3.14'); print(r[0].kind, r[0].value, type(r[0].value).__name__)"
python -c "from calc.lexer import tokenize; r=tokenize('.5'); print(r[0].kind, r[0].value)"
python -c "from calc.lexer import tokenize; r=tokenize('10.'); print(r[0].kind, r[0].value)"
```
**EXPECTED:**
- `NUMBER 42 int`
- `NUMBER 3.14 float`
- `NUMBER 0.5`
- `NUMBER 10.0`
- Final token in all cases: `Token(kind='EOF', value=None)`
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
---
### Gate D2 CLAIMED — PASS
**WHAT:** Operators `+ - * / ( )` tokenize to correct kinds.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('1+2*3')])"
```
**EXPECTED:**
```
[('NUMBER', 1), ('PLUS', '+'), ('NUMBER', 2), ('STAR', '*'), ('NUMBER', 3), ('EOF', None)]
```
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
---
### Gate D3 CLAIMED — PASS
**WHAT:** Whitespace skipped; invalid chars raise `LexError` with offending char and position.
**HOW:**
```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')" 2>&1
```
**EXPECTED:**
- `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
- `calc.lexer.LexError: unexpected character '@' at position 2`
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
---
### Gate D4 CLAIMED — PASS
**WHAT:** `calc/test_lexer.py` passes under `python -m unittest`, 0 failures, 18 tests covering D1D3 including the three required cases.
**HOW:**
```bash
python -m unittest -q
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
**EXPECTED:**
- unittest: `Ran 18 tests in X.XXXs OK`
- expression: `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
- LexError raised (exit 1)
**WHERE:** `calc/test_lexer.py`, `calc/lexer.py` @ commit `ab0332e`

View File

@ -0,0 +1,164 @@
# STATUS — Phase parse
## DONE
All D1D6 gates PASS (Adversary-verified @2026-06-15T05:00Z).
---
## Gate D6 CLAIMED — PASS
**WHAT:** `calc/test_parser.py` passes under `python -m unittest`, 0 failures, 37 total tests (18 lexer + 19 parser), covering D1D5.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 37 tests in X.XXXs
OK
```
**WHERE:** `calc/test_parser.py` @ commit `866091c`
---
## Gate D5 CLAIMED — awaiting Adversary
**WHAT:** Each of `"1 +"`, `"(1"`, `"1 2"`, `")("`, and `""` raises `ParseError` (not any other exception).
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse, ParseError
for src in ['1 +', '(1', '1 2', ')(', '']:
try:
parse(tokenize(src))
print(f'FAIL no error for {src!r}')
except ParseError as e:
print(f'OK ParseError for {src!r}: {e}')
except Exception as e:
print(f'FAIL wrong exception for {src!r}: {type(e).__name__}: {e}')
"
```
**EXPECTED:**
```
OK ParseError for '1 +': unexpected token 'EOF' (None)
OK ParseError for '(1': unclosed parenthesis, got 'EOF'
OK ParseError for '1 2': unexpected token 'NUMBER' (2)
OK ParseError for ')(': unexpected token 'RPAREN' (')')
OK ParseError for '': empty input
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D4 CLAIMED — awaiting Adversary
**WHAT:** Unary minus parses correctly for `-5`, `-(1+2)`, `3 * -2`.
**HOW:**
```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(op='-', operand=Num(value=5))
Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))
BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D3 CLAIMED — awaiting Adversary
**WHAT:** Parens override precedence: `(1+2)*3` parses as `BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
```
**EXPECTED:**
```
BinOp(op='*', left=BinOp(op='+', left=Num(value=1), right=Num(value=2)), right=Num(value=3))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D2 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:**
```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(op='-', left=BinOp(op='-', left=Num(value=8), right=Num(value=3)), right=Num(value=2))
BinOp(op='/', left=BinOp(op='/', left=Num(value=8), right=Num(value=4)), right=Num(value=2))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D1 CLAIMED — awaiting Adversary
**WHAT:** `*` and `/` bind tighter than `+` and `-`: `1+2*3` parses as `BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
```
**EXPECTED:**
```
BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## AST Shape
```python
@dataclass
class Num:
value: int | float
@dataclass
class BinOp:
op: str # '+', '-', '*', '/'
left: Node
right: Node
@dataclass
class Unary:
op: str # '-'
operand: Node
```
Grammar:
```
expr = term (('+' | '-') term)*
term = unary (('*' | '/') unary)*
unary = '-' unary | primary
primary = NUMBER | '(' expr ')'
```