artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
# BACKLOG — Phase eval (Adversary)
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_No findings — all D1–D5 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
|
||||
@ -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.
|
||||
@ -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)
|
||||
@ -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`.
|
||||
@ -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 D1–D4 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)
|
||||
```
|
||||
@ -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 D1–D4 conditions met in one session.
|
||||
@ -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.
|
||||
@ -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:30–31` — 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:33–35` — `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 D1–D5 gates independently verified with break-it probes. No defects found. Builder may mark DONE.
|
||||
@ -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.
|
||||
@ -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 D1–D6 gates verified independently. No defects found. Builder may mark DONE.
|
||||
@ -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 D1–D4. 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`
|
||||
@ -0,0 +1,90 @@
|
||||
# STATUS — Phase lex
|
||||
|
||||
## DONE
|
||||
|
||||
All D1–D4 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 D1–D3 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`
|
||||
@ -0,0 +1,164 @@
|
||||
# STATUS — Phase parse
|
||||
|
||||
## DONE
|
||||
|
||||
All D1–D6 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 D1–D5.
|
||||
|
||||
**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 ')'
|
||||
```
|
||||
Reference in New Issue
Block a user