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 (shared)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Write `calc/evaluator.py` with `evaluate(node)` and `EvalError`
|
||||
- [x] Write `calc/test_evaluator.py` (unittest, 15 cases, D1–D3)
|
||||
- [x] Write `calc.py` CLI (D4)
|
||||
- [x] Verify 56 tests green (D5, no regression)
|
||||
- [x] Write STATUS-eval.md with cold-verify commands
|
||||
- [ ] Await Adversary PASS on all gates; write ## DONE
|
||||
|
||||
## Adversary findings
|
||||
|
||||
<!-- Adversary-owned — filed when breaking probes find issues -->
|
||||
|
||||
No findings yet.
|
||||
@ -0,0 +1,15 @@
|
||||
# BACKLOG — phase: lex
|
||||
|
||||
## Build backlog (Builder-owned)
|
||||
|
||||
- [x] Create machine-docs phase files
|
||||
- [x] Create calc/__init__.py
|
||||
- [x] Create calc/lexer.py with Token, LexError, tokenize()
|
||||
- [x] Create calc/test_lexer.py with unittest suite
|
||||
- [x] Run tests locally and verify all pass (22/22)
|
||||
- [x] Claim D1-D4
|
||||
- [ ] Wait for Adversary PASS on all gates
|
||||
- [ ] Write ## DONE to STATUS-lex.md
|
||||
|
||||
## Adversary findings
|
||||
(Adversary-owned — do not edit)
|
||||
@ -0,0 +1,12 @@
|
||||
# BACKLOG — phase: parse (Builder)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Implement `calc/parser.py` with ParseError, Num, BinOp, Unary, parse()
|
||||
- [x] Implement `calc/test_parser.py` with 19 tests covering D1–D5
|
||||
- [x] Run full test suite (41 tests green)
|
||||
- [x] Write STATUS-parse.md with WHAT/HOW/EXPECTED/WHERE
|
||||
- [x] Claim D1–D6
|
||||
|
||||
## Adversary findings
|
||||
(Read-only — Adversary writes here)
|
||||
@ -0,0 +1,11 @@
|
||||
# DECISIONS — shared (append-only)
|
||||
|
||||
## 2026-06-15 — Token.value for non-numeric tokens
|
||||
|
||||
Decision: `Token.value` is `None` for operator/paren/EOF tokens, and `int` or `float` for NUMBER tokens.
|
||||
Rationale: Parser phases only need the numeric value; operator/paren tokens carry no useful payload beyond their kind.
|
||||
|
||||
## 2026-06-15 — Float parsing strategy
|
||||
|
||||
Decision: Use Python's built-in `float()` for converting float literals. Detect float vs int by presence of `.` in the matched string.
|
||||
Rationale: Handles edge cases like `.5` and `10.` correctly via stdlib, avoids manual parsing bugs.
|
||||
@ -0,0 +1,39 @@
|
||||
# JOURNAL — phase: eval (Builder)
|
||||
|
||||
## 2026-06-15 — implementation
|
||||
|
||||
### Design
|
||||
|
||||
AST nodes from parse phase: `Num(value)`, `BinOp(op, left, right)`, `Unary(op, operand)`.
|
||||
Evaluator is a recursive tree walk. Division uses Python's `/` (true division), then normalises
|
||||
whole-valued floats to `int` (avoids trailing `.0` in CLI output without needing special fmt logic).
|
||||
|
||||
### Runs
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 56 tests in 0.002s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
```
|
||||
$ python calc.py "2+3*4"
|
||||
14
|
||||
$ python calc.py "(2+3)*4"
|
||||
20
|
||||
$ python calc.py "7/2"
|
||||
3.5
|
||||
$ python calc.py "4/2"
|
||||
2
|
||||
$ python calc.py "1/0"
|
||||
error: division by zero (stderr, exit 1)
|
||||
$ python calc.py "1 +"
|
||||
error: unexpected token 'EOF' (stderr, exit 1)
|
||||
```
|
||||
|
||||
### Type normalisation choice
|
||||
|
||||
`int(result) if result == int(result) else result` — chose this over a `fmt` conversion in the CLI
|
||||
so `evaluate` itself is honest about the value type and tests can assert `isinstance(result, int)`.
|
||||
@ -0,0 +1,57 @@
|
||||
# JOURNAL — phase: lex
|
||||
|
||||
## 2026-06-15 — Start
|
||||
|
||||
Read phase plan. Mission: build `calc/lexer.py` with `tokenize()`, Token type, LexError, and unittest suite.
|
||||
|
||||
Plan:
|
||||
- Token: dataclass with `kind` (str) and `value` (int | float | None)
|
||||
- Kinds: NUMBER, PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN, EOF
|
||||
- LexError: custom Exception with char + position
|
||||
- tokenize(): iterate over chars, match numbers (int/float), operators, parens; skip whitespace; raise LexError on unknown char; append EOF at end
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Created:
|
||||
- `calc/__init__.py` (empty package marker)
|
||||
- `calc/lexer.py` — token scanner using manual char-by-char iteration
|
||||
- `calc/test_lexer.py` — 22 unittest cases
|
||||
|
||||
Float parsing design: detect float vs int by presence of `.` in scanned substring. Handle `.5` (leading dot) and `10.` (trailing dot) correctly by scanning digits before/after the dot separately.
|
||||
|
||||
## 2026-06-15 — Test run
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 22 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
## 2026-06-15 — Verify commands
|
||||
|
||||
```
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
|
||||
[('NUMBER', 42), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
|
||||
[('NUMBER', 3.14), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
|
||||
[('NUMBER', 0.5), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
|
||||
[('NUMBER', 10.0), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
|
||||
calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
|
||||
All plan verify commands produce expected output. Claiming D1–D4.
|
||||
@ -0,0 +1,46 @@
|
||||
# JOURNAL — phase: parse (Builder)
|
||||
|
||||
## 2026-06-15
|
||||
|
||||
### Design decisions
|
||||
|
||||
Chose a classic recursive-descent parser with separate grammar levels for precedence:
|
||||
|
||||
- `_expr` handles `+`/`-` (low precedence, left-associative via while-loop)
|
||||
- `_term` handles `*`/`/` (high precedence, left-associative via while-loop)
|
||||
- `_unary` handles prefix `-` (right-associative via recursion)
|
||||
- `_primary` handles `NUMBER` and `(expr)`
|
||||
|
||||
Left-associativity comes naturally from the iterative loop pattern (each iteration wraps the accumulating `left` node deeper).
|
||||
|
||||
### Verification runs
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 41 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
Key shape outputs verified:
|
||||
```
|
||||
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓ D1
|
||||
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓ D2
|
||||
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓ D2
|
||||
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓ D3
|
||||
-5 → Unary(MINUS, Num(5)) ✓ D4
|
||||
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓ D4
|
||||
3*-2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓ D4
|
||||
```
|
||||
|
||||
D5 errors all raise `ParseError` (not `SyntaxError`, `ValueError`, etc.):
|
||||
- `"1 +"` → ParseError: unexpected token 'EOF'
|
||||
- `"(1"` → ParseError: expected ')', got 'EOF'
|
||||
- `"1 2"` → ParseError: unexpected token 'NUMBER' after expression
|
||||
- `")("` → ParseError: unexpected token 'RPAREN'
|
||||
- `""` → ParseError: empty input
|
||||
|
||||
### Empty-string handling
|
||||
|
||||
`tokenize('')` returns `[Token('EOF')]`. The `parse()` function checks the first token;
|
||||
if it's `EOF`, raises `ParseError("empty input")` immediately, avoiding an ambiguous
|
||||
"unexpected token 'EOF'" message.
|
||||
@ -0,0 +1,52 @@
|
||||
# REVIEW — phase: eval (Adversary)
|
||||
|
||||
## Status
|
||||
Initialized @2026-06-15T01:43:40Z
|
||||
Verdicts written @2026-06-15T01:58:00Z — ALL PASS
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
### eval/D1: PASS @2026-06-15T01:58:00Z
|
||||
All 5 required expressions correct from cold run:
|
||||
- `2+3*4` → `14` (precedence: * before +) ✓
|
||||
- `(2+3)*4` → `20` (parens override precedence) ✓
|
||||
- `8-3-2` → `3` (left-associativity) ✓
|
||||
- `-2+5` → `3` (unary minus leading) ✓
|
||||
- `2*-3` → `-6` (unary minus in mul) ✓
|
||||
Additional probes: `2*(3+4)-1`→13, `-(3+4)`→-7, `--5`→5 all correct.
|
||||
|
||||
### eval/D2: PASS @2026-06-15T01:58:00Z
|
||||
- `7/2` → `3.5` (true division, not floor) ✓
|
||||
- `1/0` raises `EvalError("division by zero")` — not bare `ZeroDivisionError` ✓
|
||||
- `0/0` also raises `EvalError` (edge case probed) ✓
|
||||
- Error goes to stderr, exit code 1 ✓
|
||||
- API-level check confirmed: `try/except EvalError` catches it, `ZeroDivisionError` does not ✓
|
||||
|
||||
### eval/D3: PASS @2026-06-15T01:58:00Z
|
||||
- `4/2` → `2` (int, no trailing `.0`) ✓
|
||||
- `7/2` → `3.5` (float) ✓
|
||||
- `6/3` → `2`, `9/3` → `3`, `10/5` → `2` (whole-valued normalisation consistent) ✓
|
||||
- Integer arithmetic stays `int`: `2+3` → int, `3*4` → int, `-5` → int ✓
|
||||
- Type assertions: `assertIsInstance(calc("4/2"), int)` and `assertIsInstance(calc("7/2"), float)` both pass ✓
|
||||
|
||||
### eval/D4: PASS @2026-06-15T01:58:00Z
|
||||
- `python calc.py "2+3*4"` → `14`, exit 0 ✓
|
||||
- `python calc.py "(2+3)*4"` → `20`, exit 0 ✓
|
||||
- `python calc.py "7/2"` → `3.5`, exit 0 ✓
|
||||
- `python calc.py "4/2"` → `2`, exit 0 ✓
|
||||
- `python calc.py "1/0"` → error to stderr, exit 1 ✓
|
||||
- `python calc.py "1 +"` → error to stderr, exit 1 ✓
|
||||
- `python calc.py ""` → `error: empty input`, exit 1 ✓
|
||||
- `python calc.py "abc"` → exit 1 (LexError caught) ✓
|
||||
- No tracebacks leak to stderr (grep for 'traceback' found nothing) ✓
|
||||
- Exact output format confirmed: `[14]`, `[3.5]` — no extra whitespace ✓
|
||||
|
||||
### eval/D5: PASS @2026-06-15T01:58:00Z
|
||||
- `python -m unittest -q` → `Ran 56 tests in 0.001s / OK` ✓
|
||||
- 41 lex+parse tests: still all green (no regression) ✓
|
||||
- 15 new evaluator tests: all green ✓
|
||||
- Test classes: TestArithmetic (8), TestDivision (4), TestResultType (3) ✓
|
||||
|
||||
## No findings, no VETO
|
||||
|
||||
All five gates PASS. No defects found. Builder may write ## DONE.
|
||||
@ -0,0 +1,56 @@
|
||||
# REVIEW — phase `lex` (Adversary)
|
||||
|
||||
## Status
|
||||
All 4 gates verified. Phase COMPLETE.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### lex/D1: PASS @2026-06-15T01:35:56Z
|
||||
Cold-run evidence:
|
||||
```
|
||||
tokenize('42') → [('NUMBER', 42), ('EOF', None)] — int ✓
|
||||
tokenize('3.14') → [('NUMBER', 3.14), ('EOF', None)] — float ✓
|
||||
tokenize('.5') → [('NUMBER', 0.5), ('EOF', None)] — float ✓
|
||||
tokenize('10.') → [('NUMBER', 10.0), ('EOF', None)] — float ✓
|
||||
type(tokenize('42')[0].value) == int ✓
|
||||
type(tokenize('3.14')[0].value) == float ✓
|
||||
```
|
||||
Value is int for integers, float for floats. DoD fully met.
|
||||
|
||||
### lex/D2: PASS @2026-06-15T01:35:56Z
|
||||
Cold-run evidence:
|
||||
```
|
||||
tokenize('1+2*3') → ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'] ✓
|
||||
tokenize('+-*/()') → ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'] ✓
|
||||
tokenize('3.5*(1-2)') → correct kind/value pairs ✓
|
||||
```
|
||||
All 6 operator/paren kinds present and correct.
|
||||
|
||||
### lex/D3: PASS @2026-06-15T01:35:56Z
|
||||
Cold-run evidence:
|
||||
```
|
||||
tokenize(' 12 + 3 ') → NUMBER(12) PLUS NUMBER(3) EOF — spaces skipped ✓
|
||||
tokenize('1\t+\t2') → NUMBER PLUS NUMBER EOF — tabs skipped ✓
|
||||
tokenize('1 @ 2') → LexError: unexpected character '@' at position 2 ✓
|
||||
tokenize('$5') → LexError: unexpected character '$' at position 0 ✓
|
||||
tokenize('x') → LexError: unexpected character 'x' at position 0 ✓
|
||||
```
|
||||
Offending char and position both present in message. DoD fully met.
|
||||
|
||||
### lex/D4: PASS @2026-06-15T01:35:56Z
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 22 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
Zero failures, zero errors. All 22 tests cover D1–D3 including required cases:
|
||||
`" 12 + 3 "`, `"3.5*(1-2)"`, and `"1 @ 2"` raising LexError.
|
||||
|
||||
## Informational finding (non-blocking)
|
||||
|
||||
**F1 — lone `.` raises `ValueError` not `LexError`:** `tokenize('.')` crashes with
|
||||
`ValueError: could not convert string to float: '.'` instead of a `LexError`. The
|
||||
DoD does not require this case to be handled, so this is NOT a blocker for any gate.
|
||||
Noted for future phases that may extend the lexer.
|
||||
|
||||
No VETO. All DoD items independently verified.
|
||||
@ -0,0 +1,106 @@
|
||||
# REVIEW — phase: parse (Adversary)
|
||||
|
||||
## Verdict: ALL GATES PASS @2026-06-15T01:42:00Z
|
||||
|
||||
Cold-verified from commit `23d0ae9` (work-adv clone, fresh shell, no cached state).
|
||||
|
||||
---
|
||||
|
||||
### D6 (tests green): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 41 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
41 tests (22 lexer + 19 parser), 0 failures. Tests assert on `repr()`/tree structure, not evaluation — satisfies the plan requirement.
|
||||
|
||||
---
|
||||
|
||||
### D1 (precedence): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓
|
||||
6-4/2 → BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2))) ✓
|
||||
```
|
||||
|
||||
Re-derived independently: grammar `expr: term((+|-)term)*` / `term: unary((*|/)unary)*` guarantees `*`/`/` subtrees sit inside `+`/`-` nodes. Observed matches.
|
||||
|
||||
Extra adversarial probe:
|
||||
```
|
||||
1+2*3+4 → BinOp(PLUS, BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))), Num(4)) ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### D2 (left associativity): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓
|
||||
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓
|
||||
```
|
||||
|
||||
Extra probes:
|
||||
```
|
||||
1+2+3 → BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
|
||||
2*3*4 → BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4)) ✓
|
||||
4*3/2 → BinOp(SLASH, BinOp(STAR, Num(4), Num(3)), Num(2)) ✓
|
||||
```
|
||||
|
||||
Left-associativity correctly implemented via iterative while-loop (not recursion).
|
||||
|
||||
---
|
||||
|
||||
### D3 (parentheses): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
|
||||
((2)) → Num(2) ✓
|
||||
(-5) → Unary(MINUS, Num(5)) ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### D4 (unary minus): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
-5 → Unary(MINUS, Num(5)) ✓
|
||||
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓
|
||||
3 * -2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓
|
||||
--5 → Unary(MINUS, Unary(MINUS, Num(5))) ✓ (double unary)
|
||||
2+-3 → BinOp(PLUS, Num(2), Unary(MINUS, Num(3))) ✓ (unary after binary)
|
||||
```
|
||||
|
||||
Right-recursive `_unary` correctly handles chained negation.
|
||||
|
||||
---
|
||||
|
||||
### D5 (errors raise ParseError): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
All five required cases verified — each raises `ParseError`, not `ValueError`, `IndexError`, or any other exception:
|
||||
|
||||
```
|
||||
'1 +' → ParseError: unexpected token 'EOF' ✓
|
||||
'(1' → ParseError: expected ')', got 'EOF' ✓
|
||||
'1 2' → ParseError: unexpected token 'NUMBER' after expression ✓
|
||||
')(' → ParseError: unexpected token 'RPAREN' ✓
|
||||
'' → ParseError: empty input ✓
|
||||
```
|
||||
|
||||
Extra adversarial error probes (all raise ParseError):
|
||||
```
|
||||
'*5' → ParseError: unexpected token 'STAR' ✓
|
||||
'5*' → ParseError: unexpected token 'EOF' ✓
|
||||
'()' → ParseError: unexpected token 'RPAREN' ✓
|
||||
'(+5)' → ParseError: unexpected token 'PLUS' ✓
|
||||
'1++2' → ParseError: unexpected token 'PLUS' ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
All six DoD gates D1–D6 independently verified cold. No defects found. The grammar in STATUS exactly matches the implementation (`_expr`/`_term`/`_unary`/`_primary`). Node shape is stable and documented in the `parse()` docstring. No veto.
|
||||
|
||||
Builder may write `## DONE` to STATUS-parse.md.
|
||||
@ -0,0 +1,44 @@
|
||||
# STATUS — phase: eval (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
Gate: D1 D2 D3 D4 D5 — Adversary PASS @2026-06-15T01:58:00Z. Phase complete.
|
||||
|
||||
## What was built
|
||||
|
||||
- `calc/evaluator.py` — `evaluate(node) -> int | float`; `EvalError` for bad nodes / division by zero
|
||||
- `calc/test_evaluator.py` — 15 unittest cases covering D1–D3
|
||||
- `calc.py` (repo root) — CLI: `python calc.py "<expr>"`
|
||||
|
||||
## Commit SHA
|
||||
|
||||
See `git log --oneline -1` on origin/main after this push.
|
||||
|
||||
## Verify commands (cold — run from repo root)
|
||||
|
||||
```bash
|
||||
python -m unittest -q
|
||||
# Expected: Ran 56 tests in <N>s / OK
|
||||
|
||||
python calc.py "2+3*4" # Expected: 14
|
||||
python calc.py "(2+3)*4" # Expected: 20
|
||||
python calc.py "7/2" # Expected: 3.5
|
||||
python calc.py "4/2" # Expected: 2
|
||||
python calc.py "1/0" # Expected: prints error to stderr, exits 1
|
||||
python calc.py "1 +" # Expected: prints error to stderr, exits 1
|
||||
```
|
||||
|
||||
## Gate mapping
|
||||
|
||||
| Gate | DoD | Verify |
|
||||
|------|-----|--------|
|
||||
| D1 | arithmetic: `+ - * /`, precedence, parens, unary minus | `python -m unittest -q` (TestArithmetic) |
|
||||
| D2 | true division; `EvalError` on div-by-zero (not bare `ZeroDivisionError`) | `python -m unittest -q` (TestDivision) |
|
||||
| D3 | whole-valued → int (no `.0`); non-whole → float | `python -m unittest -q` (TestResultType) |
|
||||
| D4 | `python calc.py "2+3*4"` → 14 exit 0; error → stderr + exit 1 | manual CLI commands above |
|
||||
| D5 | 56 tests total (lex+parse+eval), 0 failures | `python -m unittest -q` |
|
||||
|
||||
## Result type rule (D3)
|
||||
|
||||
Division result is normalised: `result = left / right; return int(result) if result == int(result) else result`.
|
||||
The CLI's `_fmt` calls `str(result)`, so `int` prints as `"2"` and `float` prints as `"3.5"`.
|
||||
@ -0,0 +1,70 @@
|
||||
# STATUS — phase: lex (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
All gates verified PASS by Adversary @2026-06-15T01:35:56Z.
|
||||
|
||||
## Gate: D1, D2, D3, D4 — PASS
|
||||
|
||||
### WHAT is claimed
|
||||
All four gates D1–D4 are implemented and tested.
|
||||
|
||||
### HOW to verify (run from a fresh clone)
|
||||
|
||||
```bash
|
||||
cd <repo>
|
||||
python -m unittest -q
|
||||
```
|
||||
Expected: 22 tests, 0 failures, 0 errors.
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 42), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 3.14), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 0.5), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 10.0), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
|
||||
```
|
||||
Expected: `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
```
|
||||
Expected: raises `calc.lexer.LexError: unexpected character '@' at position 2`
|
||||
|
||||
### WHERE (commit sha)
|
||||
`7022854acf94b35ebc79cb48315e91b82c7cc4ec`
|
||||
|
||||
### Files
|
||||
- `calc/__init__.py` — empty package marker
|
||||
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
|
||||
- `calc/test_lexer.py` — 22 unittest cases covering D1–D3
|
||||
|
||||
### Gate checklist
|
||||
- D1 (numbers): PASS @2026-06-15T01:35:56Z
|
||||
- D2 (operators & parens): PASS @2026-06-15T01:35:56Z
|
||||
- D3 (whitespace & errors): PASS @2026-06-15T01:35:56Z
|
||||
- D4 (tests green): PASS @2026-06-15T01:35:56Z
|
||||
|
||||
## Informational (non-blocking)
|
||||
- F1: lone `.` raises `ValueError` not `LexError` — not required by DoD, noted for future phases
|
||||
@ -0,0 +1,115 @@
|
||||
# STATUS — phase: parse (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
## Gate: D1–D6 — ALL PASS (Adversary-verified @2026-06-15T01:42:00Z)
|
||||
|
||||
### WHAT is claimed
|
||||
All six gates D1–D6 are implemented and tested.
|
||||
|
||||
- **D1 (precedence):** `*`/`/` bind tighter than `+`/`-`
|
||||
- **D2 (left associativity):** Same-precedence ops associate left
|
||||
- **D3 (parentheses):** Parens override precedence
|
||||
- **D4 (unary minus):** Leading and nested unary minus
|
||||
- **D5 (errors):** Five malformed inputs each raise `ParseError`
|
||||
- **D6 (tests green):** 19 parser tests + 22 lexer tests = 41 total, 0 failures
|
||||
|
||||
### HOW to verify (run from a fresh clone)
|
||||
|
||||
**D6 — all tests green:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
Expected output: `Ran 41 tests in 0.00xs` / `OK`
|
||||
|
||||
**D1 — precedence (structural check):**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
|
||||
```
|
||||
Expected: `BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('6-4/2')))"
|
||||
```
|
||||
Expected: `BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))`
|
||||
|
||||
**D2 — left associativity:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
|
||||
```
|
||||
Expected: `BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
|
||||
```
|
||||
Expected: `BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))`
|
||||
|
||||
**D3 — parentheses:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
|
||||
```
|
||||
Expected: `BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))`
|
||||
|
||||
**D4 — unary minus:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
|
||||
```
|
||||
Expected: `Unary(MINUS, Num(5))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
|
||||
```
|
||||
Expected: `Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
|
||||
```
|
||||
Expected: `BinOp(STAR, Num(3), Unary(MINUS, Num(2)))`
|
||||
|
||||
**D5 — errors (each must raise `ParseError`, not a different exception):**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 +'))"
|
||||
# raises: ParseError: unexpected token 'EOF'
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('(1'))"
|
||||
# raises: ParseError: expected ')', got 'EOF'
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 2'))"
|
||||
# raises: ParseError: unexpected token 'NUMBER' after expression
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(')('))"
|
||||
# raises: ParseError: unexpected token 'RPAREN'
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(''))"
|
||||
# raises: ParseError: empty input
|
||||
```
|
||||
|
||||
### EXPECTED AST shapes (re-derivable from grammar)
|
||||
|
||||
AST node types:
|
||||
- `Num(value)` — numeric literal; value is int or float
|
||||
- `BinOp(op, left, right)` — op in `{'PLUS','MINUS','STAR','SLASH'}`
|
||||
- `Unary(op, operand)` — op == `'MINUS'`
|
||||
|
||||
Grammar used (drives precedence + associativity):
|
||||
```
|
||||
expr : term (('+' | '-') term)* ← left-assoc, low precedence
|
||||
term : unary (('*' | '/') unary)* ← left-assoc, high precedence
|
||||
unary : '-' unary | primary ← right-assoc chain, prefix only
|
||||
primary : NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
### WHERE (commit sha)
|
||||
`23d0ae9` (claim(D1,D2,D3,D4,D5,D6): parser complete, all 41 tests green)
|
||||
|
||||
### Files
|
||||
- `calc/parser.py` — ParseError, Num, BinOp, Unary, _Parser, parse()
|
||||
- `calc/test_parser.py` — 19 unittest cases covering D1–D5
|
||||
|
||||
### Gate checklist
|
||||
- D1 (precedence): CLAIMED
|
||||
- D2 (left associativity): CLAIMED
|
||||
- D3 (parentheses): CLAIMED
|
||||
- D4 (unary minus): CLAIMED
|
||||
- D5 (errors): CLAIMED
|
||||
- D6 (tests green): CLAIMED
|
||||
Reference in New Issue
Block a user