artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
@ -0,0 +1,9 @@
|
||||
# BACKLOG — eval phase
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 — arithmetic: +, -, *, /, precedence, parens, unary minus
|
||||
- [x] D2 — division: true division, EvalError on div-by-zero
|
||||
- [x] D3 — result type: int for whole values, float otherwise
|
||||
- [x] D4 — CLI: calc.py prints result or error to stderr with non-zero exit
|
||||
- [x] D5 — tests green + end-to-end, no regressions
|
||||
@ -0,0 +1,12 @@
|
||||
# BACKLOG — Phase `lex`
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 — Implement integer/float tokenization → NUMBER tokens
|
||||
- [x] D2 — Implement operator/paren tokenization
|
||||
- [x] D3 — Whitespace skipping + LexError for invalid chars
|
||||
- [x] D4 — Write and pass unittest suite (14 tests, 0 failures)
|
||||
- [ ] Await Adversary PASS on D1–D4
|
||||
|
||||
## Adversary findings
|
||||
<!-- Adversary writes here -->
|
||||
@ -0,0 +1,12 @@
|
||||
# BACKLOG — Phase `parse`
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Implement `calc/parser.py` with `ParseError`, `Num`, `BinOp`, `Unary`, `parse()`
|
||||
- [x] Implement `calc/test_parser.py` with 20 tests covering D1–D5
|
||||
- [x] Run `python -m unittest -q` — 34 tests, all pass
|
||||
- [x] Claim D1–D6
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(none yet)_
|
||||
@ -0,0 +1,19 @@
|
||||
# DECISIONS (append-only, settled design decisions)
|
||||
|
||||
## lex phase
|
||||
|
||||
**D-LEX-1:** `Token` uses `@dataclass` for equality and repr, making test assertions clean.
|
||||
|
||||
**D-LEX-2:** Number parsing scans a contiguous run of digits and dots, then uses `float()` or `int()` based on presence of `.`. Edge cases `.5` and `10.` handled correctly by Python's built-in conversion.
|
||||
|
||||
**D-LEX-3:** `LexError` extends `Exception` directly (no custom fields) — message contains char and position as plain text, sufficient for D3.
|
||||
|
||||
**D-LEX-4:** EOF token always appended as the final token to signal end-of-input to the parser (future phase).
|
||||
|
||||
## parse phase
|
||||
|
||||
**D-PARSE-1:** Recursive-descent with three levels — `_expr` (additive), `_term` (multiplicative), `_unary` (prefix minus), `_primary` (atoms/parens). While loops in `_expr`/`_term` give left-associativity; calling `_term` from `_expr` gives `*/` higher precedence than `+-`.
|
||||
|
||||
**D-PARSE-2:** `Num`, `BinOp`, `Unary` are `@dataclass`s — equality and repr are free, making structural test assertions clean.
|
||||
|
||||
**D-PARSE-3:** `ParseError` extends `Exception` directly. Message contains the unexpected token kind/value, sufficient for D5.
|
||||
@ -0,0 +1,34 @@
|
||||
# JOURNAL — eval phase
|
||||
|
||||
## Implementation run
|
||||
|
||||
Ran all checks locally before commit:
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 45 tests in 0.001s
|
||||
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
|
||||
(exit 1)
|
||||
$ python calc.py "1 +"
|
||||
error: unexpected end of input
|
||||
(exit 1)
|
||||
```
|
||||
|
||||
All 45 tests pass (20 from parse phase, 14 from lex phase, 11 from evaluator tests).
|
||||
|
||||
### Design notes
|
||||
- `_coerce()` handles result type: if a float has no fractional part, cast to int.
|
||||
- Division by zero caught explicitly and re-raised as `EvalError`.
|
||||
- CLI catches `LexError`, `ParseError`, `EvalError` — no raw tracebacks.
|
||||
@ -0,0 +1,24 @@
|
||||
# JOURNAL — Phase `lex`
|
||||
|
||||
## Implementation notes
|
||||
|
||||
### Design choices
|
||||
- Used `@dataclass` for `Token` to get `__eq__` and `__repr__` for free — useful in tests.
|
||||
- Number parsing: scan while char is digit or `.`; if `.` in raw string → `float()`, else `int()`. Handles `42`, `3.14`, `.5`, `10.`.
|
||||
- Single-char operators: dict lookup for O(1) dispatch.
|
||||
- LexError message includes both the character (quoted) and its 0-based position.
|
||||
|
||||
### Test run (verified locally)
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 14 tests in 0.000s
|
||||
OK
|
||||
|
||||
$ 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
|
||||
```
|
||||
@ -0,0 +1,34 @@
|
||||
# JOURNAL — Phase `parse`
|
||||
|
||||
## Session 1 — initial implementation
|
||||
|
||||
**Plan:** Recursive-descent parser. Grammar:
|
||||
```
|
||||
expr → term (('+' | '-') term)*
|
||||
term → unary (('*' | '/') unary)*
|
||||
unary → '-' unary | primary
|
||||
primary → NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
This naturally encodes `*/` tighter than `+-` (D1) and left-associativity via the while loops (D2). Unary minus (D4) handled in `_unary` before `_primary`.
|
||||
|
||||
**Test run:**
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 34 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
**Key assertions verified manually:**
|
||||
```
|
||||
1+2*3 → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓ D1
|
||||
8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓ D2
|
||||
8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓ D2
|
||||
(1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓ D3
|
||||
-5 → Unary('-', Num(5)) ✓ D4
|
||||
-(1+2) → Unary('-', BinOp('+', Num(1), Num(2))) ✓ D4
|
||||
3 * -2 → BinOp('*', Num(3), Unary('-', Num(2))) ✓ D4
|
||||
```
|
||||
|
||||
Error cases ('1 +', '(1', '1 2', ')(', '') all raise ParseError ✓ D5
|
||||
@ -0,0 +1,61 @@
|
||||
# REVIEW — Phase `eval`
|
||||
|
||||
Adversary: verified cold at commit `7e18a9b`.
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
### eval/D1: PASS @2026-06-15T03:58Z
|
||||
```
|
||||
$ 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 ✓
|
||||
```
|
||||
Also probed: `2+3*4+1`→15, `100/10/2`→5, `--5`→5 (double unary). All correct.
|
||||
|
||||
### eval/D2: PASS @2026-06-15T03:58Z
|
||||
```
|
||||
$ python calc.py "7/2" → 3.5 ✓ (true division)
|
||||
$ python calc.py "1/0" → stderr: "error: division by zero", exit 1 ✓
|
||||
```
|
||||
Cold API check: `evaluate(parse(tokenize('1/0')))` raises `EvalError`, not `ZeroDivisionError`.
|
||||
Division by zero is caught before Python's operator is invoked (`right == 0` guard).
|
||||
|
||||
### eval/D3: PASS @2026-06-15T03:58Z
|
||||
```
|
||||
$ python calc.py "4/2" → 2 ✓ (int, no .0)
|
||||
$ python calc.py "7/2" → 3.5 ✓ (float)
|
||||
$ python calc.py "9/3" → 3 ✓
|
||||
$ python calc.py "1/3" → 0.333... ✓
|
||||
$ python calc.py "-6/2" → -3 ✓ (negative whole)
|
||||
$ python calc.py "-7/2" → -3.5 ✓
|
||||
```
|
||||
`_coerce()` rule: if `isinstance(val, float) and val == int(val)` return `int(val)` else return as-is. Consistent with documented rule.
|
||||
|
||||
### eval/D4: PASS @2026-06-15T03:58Z
|
||||
```
|
||||
$ python calc.py "2+3*4" → stdout: 14, exit 0 ✓
|
||||
$ python calc.py "1 +" → stderr: "error: unexpected end of input", exit 1 ✓
|
||||
$ python calc.py "1/0" → stderr: "error: division by zero", exit 1 ✓
|
||||
$ python calc.py "" → stderr: "error: unexpected end of input", exit 1 ✓
|
||||
```
|
||||
No traceback on error (1-line output only). Errors routed to stderr only (stdout empty on error).
|
||||
|
||||
### eval/D5: PASS @2026-06-15T03:58Z
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 45 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
45 tests: 14 lex + 20 parse + 11 evaluator. 0 failures. No regression in prior phases.
|
||||
`test_evaluator.py` covers D1–D3 (arithmetic, division, result types, EvalError for div-by-zero).
|
||||
A CLI check (D4) is exercised via the cold verify commands above.
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(none — no defects found; no veto)_
|
||||
|
||||
## Summary
|
||||
|
||||
All five DoD gates D1–D5 PASS. No veto. Builder may write "## DONE" to STATUS-eval.md.
|
||||
@ -0,0 +1,53 @@
|
||||
# REVIEW — Phase `lex`
|
||||
|
||||
Adversary: cold-verification log. One entry per gate per pass.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### lex/D1: PASS @2026-06-15T03:52Z
|
||||
Cold-ran from own clone at commit 462ad1f.
|
||||
```
|
||||
tokenize("42") → [('NUMBER', 42), ('EOF', None)] ✓ int
|
||||
tokenize(".5") → [('NUMBER', 0.5), ('EOF', None)] ✓ float
|
||||
tokenize("10.") → [('NUMBER', 10.0), ('EOF', None)] ✓ float
|
||||
tokenize("3.14")→ [('NUMBER', 3.14), ('EOF', None)] ✓ float
|
||||
```
|
||||
EOF always appended as final token. int/float types correct.
|
||||
|
||||
### lex/D2: PASS @2026-06-15T03:52Z
|
||||
```
|
||||
tokenize("1+2*3") → ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓
|
||||
tokenize("()+-(*/") → LPAREN RPAREN PLUS MINUS LPAREN STAR SLASH EOF ✓
|
||||
```
|
||||
All six operator/paren kinds map correctly.
|
||||
|
||||
### lex/D3: PASS @2026-06-15T03:52Z
|
||||
```
|
||||
tokenize(" 12 + 3 ") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓ spaces skipped
|
||||
tokenize("1\t+\t2") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓ tabs skipped
|
||||
tokenize("1 @ 2") raises LexError: unexpected character '@' at position 2 ✓
|
||||
tokenize("hello") raises LexError: unexpected character 'h' at position 0 ✓
|
||||
tokenize("$10") raises LexError ✓
|
||||
```
|
||||
LexError message contains the offending character and position.
|
||||
|
||||
### lex/D4: PASS @2026-06-15T03:52Z
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 14 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
14 tests, 0 failures. Plan's canonical verify commands all produce expected output:
|
||||
- `3.5*(1-2)` → `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
|
||||
- `1 @ 2` → raises LexError
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### F1 (non-blocking) — malformed number literals leak ValueError instead of LexError
|
||||
- `tokenize("..")` → `ValueError: could not convert string to float: '..'`
|
||||
- `tokenize("1.2.3")` → `ValueError: could not convert string to float: '1.2.3'`
|
||||
|
||||
The number-scanning loop greedily consumes all `[0-9.]` chars, then calls `float()` which throws a raw ValueError. The DoD (D3) only specifies invalid *characters* (@ $ letters) and these cases are not in the test suite, so this does **not** block DONE. Noted for later phases.
|
||||
|
||||
## Summary
|
||||
All four DoD gates PASS. No veto. Builder may write "## DONE" to STATUS-lex.md.
|
||||
@ -0,0 +1,63 @@
|
||||
# REVIEW — Phase `parse`
|
||||
|
||||
Adversary: cold-verification log. One entry per gate per pass.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### parse/D1: PASS @2026-06-15T04:03Z
|
||||
Cold-ran at commit e9a5152.
|
||||
```
|
||||
parse(tokenize('1+2*3')) → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓
|
||||
```
|
||||
`*` binds tighter than `+` — tree structure matches plan requirement. Independently verified extra cases:
|
||||
`1*2+3*4` → `BinOp('+', BinOp('*', Num(1), Num(2)), BinOp('*', Num(3), Num(4)))` ✓
|
||||
|
||||
### parse/D2: PASS @2026-06-15T04:03Z
|
||||
```
|
||||
parse(tokenize('8-3-2')) → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓
|
||||
parse(tokenize('8/4/2')) → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
|
||||
```
|
||||
Left-associativity confirmed by `while` loop in `_expr`/`_term` (re-read code). Extra probe:
|
||||
`1+2+3+4` → `BinOp('+', BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)), Num(4))` ✓
|
||||
|
||||
### parse/D3: PASS @2026-06-15T04:03Z
|
||||
```
|
||||
parse(tokenize('(1+2)*3')) → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
```
|
||||
Double paren `((3))` → `Num(3)` ✓
|
||||
|
||||
### parse/D4: PASS @2026-06-15T04:03Z
|
||||
```
|
||||
parse(tokenize('-5')) → Unary('-', Num(5)) ✓
|
||||
parse(tokenize('-(1+2)')) → Unary('-', BinOp('+', Num(1), Num(2))) ✓
|
||||
parse(tokenize('3 * -2')) → BinOp('*', Num(3), Unary('-', Num(2))) ✓
|
||||
```
|
||||
Extra probes: `--5` → `Unary('-', Unary('-', Num(5)))` ✓; `1+-2` → `BinOp('+', Num(1), Unary('-', Num(2)))` ✓;
|
||||
`-1-2` → `BinOp('-', Unary('-', Num(1)), Num(2))` ✓; `-(-(1))` → `Unary('-', Unary('-', Num(1)))` ✓
|
||||
|
||||
### parse/D5: PASS @2026-06-15T04:03Z
|
||||
All five plan-mandated malformed inputs raise `ParseError` (not TypeError/ValueError/IndexError):
|
||||
```
|
||||
'1 +' → ParseError: unexpected end of input ✓
|
||||
'(1' → ParseError: expected RPAREN, got 'EOF' (None) ✓
|
||||
'1 2' → ParseError: unexpected token 'NUMBER' (2) ✓
|
||||
')(' → ParseError: unexpected token 'RPAREN' (')') ✓
|
||||
'' → ParseError: unexpected end of input ✓
|
||||
```
|
||||
Extra probes all raise ParseError: `* 1`, `1 * `, `()`, `1 + ()`, `1)`, `(1+2` — all correct.
|
||||
|
||||
### parse/D6: PASS @2026-06-15T04:03Z
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 34 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
34 tests (14 lex + 20 parse), 0 failures.
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(none — no defects found; no veto)_
|
||||
|
||||
## Summary
|
||||
|
||||
All six DoD gates PASS. No veto. Builder may write "## DONE" to STATUS-parse.md.
|
||||
@ -0,0 +1,38 @@
|
||||
# STATUS — eval phase
|
||||
|
||||
## DONE
|
||||
|
||||
## Gate: D1–D5 PASS (Adversary-verified @2026-06-15T03:58Z, commit 7e18a9b)
|
||||
|
||||
### What is claimed
|
||||
All five gates D1–D5 are implemented and verified locally.
|
||||
|
||||
### Commit
|
||||
(see latest `claim(D1-D5)` commit on main)
|
||||
|
||||
### Artifacts
|
||||
- `calc/evaluator.py` — `evaluate(node) -> int | float`, `EvalError`
|
||||
- `calc/test_evaluator.py` — unittest suite covering D1–D3
|
||||
- `calc.py` — CLI entry point (D4)
|
||||
|
||||
### How to verify (cold)
|
||||
|
||||
```bash
|
||||
python -m unittest -q # 45 tests, 0 failures
|
||||
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 to stderr, exit 1
|
||||
python calc.py "1 +" # error to stderr, exit 1
|
||||
```
|
||||
|
||||
### Expected outputs
|
||||
| command | stdout | stderr | exit |
|
||||
|---------|--------|--------|------|
|
||||
| `python calc.py "2+3*4"` | `14` | — | 0 |
|
||||
| `python calc.py "(2+3)*4"` | `20` | — | 0 |
|
||||
| `python calc.py "7/2"` | `3.5` | — | 0 |
|
||||
| `python calc.py "4/2"` | `2` | — | 0 |
|
||||
| `python calc.py "1/0"` | — | `error: division by zero` | 1 |
|
||||
| `python calc.py "1 +"` | — | `error: unexpected end of input` | 1 |
|
||||
@ -0,0 +1,49 @@
|
||||
# STATUS — Phase `lex`
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D4: PASS (Adversary-verified @2026-06-15T03:52Z, commit c974829).
|
||||
Note: Adversary F1 (non-blocking) — malformed floats like `".."` leak ValueError; not in DoD scope.
|
||||
|
||||
## Current State
|
||||
Gates D1–D4: CLAIMED, awaiting Adversary verification.
|
||||
|
||||
## Claims
|
||||
|
||||
### D1 — numbers
|
||||
**WHAT:** Integers and floats tokenize to NUMBER tokens with numeric values.
|
||||
**HOW:** Run `python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"` — expected `[('NUMBER', 42), ('EOF', None)]`. Also `.5` → float 0.5, `10.` → float 10.0, `3.14` → float 3.14.
|
||||
**EXPECTED:** `[('NUMBER', 42), ('EOF', None)]`
|
||||
**WHERE:** `calc/lexer.py`, `calc/test_lexer.py`
|
||||
|
||||
### D2 — operators & parens
|
||||
**WHAT:** `+ - * / ( )` each tokenize to PLUS/MINUS/STAR/SLASH/LPAREN/RPAREN; expression `1+2*3` yields NUMBER PLUS NUMBER STAR NUMBER EOF.
|
||||
**HOW:** `python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"`
|
||||
**EXPECTED:** `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']`
|
||||
**WHERE:** `calc/lexer.py`
|
||||
|
||||
### D3 — whitespace & errors
|
||||
**WHAT:** Spaces/tabs skipped; invalid characters raise LexError with character and position.
|
||||
**HOW:**
|
||||
- `python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize(' 12 + 3 ')])"` → `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
|
||||
- `python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"` → raises `LexError: unexpected character '@' at position 2`
|
||||
**EXPECTED:** whitespace omitted from output; LexError raised with `@` and `2` in message
|
||||
**WHERE:** `calc/lexer.py`
|
||||
|
||||
### D4 — tests green
|
||||
**WHAT:** `calc/test_lexer.py` passes `python -m unittest -q` with 0 failures (14 tests).
|
||||
**HOW:** `python -m unittest -q` from repo root
|
||||
**EXPECTED:** `Ran 14 tests in 0.000s\nOK`
|
||||
**WHERE:** `calc/test_lexer.py`
|
||||
|
||||
## Verification commands (exact, from repo root)
|
||||
```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 outputs:
|
||||
1. `Ran 14 tests in 0.000s` / `OK`
|
||||
2. `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
|
||||
3. `LexError: unexpected character '@' at position 2`
|
||||
@ -0,0 +1,78 @@
|
||||
# STATUS — Phase `parse`
|
||||
|
||||
## DONE
|
||||
|
||||
Gate: D1,D2,D3,D4,D5,D6 — all PASS (Adversary-verified @2026-06-15T04:03Z, commit e9a5152)
|
||||
|
||||
## What is claimed
|
||||
|
||||
All DoD gates D1–D6 implemented and all 20 parser tests pass (34 total across lex+parse).
|
||||
|
||||
**Commit:** see `claim(D1–D6)` commit on main.
|
||||
|
||||
## How to verify (cold, from a fresh clone)
|
||||
|
||||
```bash
|
||||
cd <clone>
|
||||
python -m unittest -q
|
||||
# Expect: Ran 34 tests in ~0.001s — OK
|
||||
|
||||
# D1 — precedence (*/ tighter than +-)
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('1+2*3'))))"
|
||||
# Expected: BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
|
||||
# D2 — left associativity
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('8-3-2'))))"
|
||||
# Expected: BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('8/4/2'))))"
|
||||
# Expected: BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
|
||||
# D3 — parentheses override
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('(1+2)*3'))))"
|
||||
# Expected: BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
|
||||
# D4 — unary minus
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('-5'))))"
|
||||
# Expected: Unary('-', Num(5))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('-(1+2)'))))"
|
||||
# Expected: Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('3 * -2'))))"
|
||||
# Expected: BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
|
||||
# D5 — errors raise ParseError
|
||||
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: {src!r} did not raise')
|
||||
except ParseError as e:
|
||||
print(f'OK ParseError: {src!r}')
|
||||
"
|
||||
```
|
||||
|
||||
## Expected outputs
|
||||
|
||||
- `python -m unittest -q` → `Ran 34 tests in ...s\nOK`
|
||||
- `1+2*3` → `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`
|
||||
- `8-3-2` → `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))`
|
||||
- `8/4/2` → `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))`
|
||||
- `(1+2)*3` → `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
|
||||
- `-5` → `Unary('-', Num(5))`
|
||||
- `-(1+2)` → `Unary('-', BinOp('+', Num(1), Num(2)))`
|
||||
- `3 * -2` → `BinOp('*', Num(3), Unary('-', Num(2)))`
|
||||
- All 5 malformed inputs → `OK ParseError`
|
||||
|
||||
## AST shape (for evaluator)
|
||||
|
||||
```
|
||||
Num(value) — numeric literal; value is int or float
|
||||
BinOp(op, left, right) — binary; op in {'+', '-', '*', '/'}
|
||||
Unary(op, operand) — unary minus; op == '-'
|
||||
```
|
||||
|
||||
All three are `@dataclass`, so `==` comparison works for tests.
|
||||
|
||||
## Where
|
||||
|
||||
- `calc/parser.py` — parser implementation
|
||||
- `calc/test_parser.py` — 20 parser tests covering D1–D5
|
||||
Reference in New Issue
Block a user