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,17 @@
# git history (claim/review handshake), from the run's shared bare repo
75bd35c status: eval phase DONE — all D1-D5 gates PASS per Adversary review at fe7e562
434cde5 review(D1,D2,D3,D4,D5): PASS — all gates verified cold at fe7e562
df2f5da claim(D1,D2,D3,D4,D5): eval phase — evaluator + CLI + tests at fe7e562
fe7e562 feat: implement evaluator, CLI, and tests (D1-D5) for eval phase
710b731 review(init): Adversary online for eval phase, awaiting Builder commits
0afab21 review(D1,D2,D3,D4,D5,D6): PASS — all gates verified cold at fa50146
b61ad6d claim(D1-D6): parse phase — all gates verified, 36 tests green
fa50146 status: add commit sha 14bbe57 to STATUS-parse
14bbe57 feat: implement calc parser (D1-D6) — parse() with Num/BinOp/Unary AST
fc470e0 status: phase lex DONE — all D1-D4 gates PASS per Adversary review
1ca2b4c review(D1,D2,D3,D4): PASS — all gates verified cold at ba1f056
35de6d4 journal: add lex phase build notes
c465844 status: add commit sha ba1f056 to STATUS-lex
ba1f056 feat: implement calc lexer (D1-D4) — tokenize() with Token/LexError
4909b16 review(init): Adversary online, awaiting Builder commits
cae8791 chore: seed

View File

@ -0,0 +1 @@
# calc work repo

View File

@ -0,0 +1 @@
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary-min/r3

View File

@ -0,0 +1,22 @@
#!/usr/bin/env python3
import sys
from calc.lexer import tokenize, LexError
from calc.parser import parse, ParseError
from calc.evaluator import evaluate, EvalError
def main():
if len(sys.argv) != 2:
print("usage: calc.py <expression>", file=sys.stderr)
sys.exit(1)
expr = sys.argv[1]
try:
result = evaluate(parse(tokenize(expr)))
except (LexError, ParseError, EvalError) as e:
print(f"error: {e}", file=sys.stderr)
sys.exit(1)
print(result)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,42 @@
from calc.parser import Num, BinOp, Unary, Node
class EvalError(Exception):
pass
def evaluate(node: Node):
"""Walk the AST and return int | float.
Result type rule: if the result is mathematically an integer (no
fractional part), return int; otherwise return float.
"""
if isinstance(node, Num):
return node.value
if isinstance(node, Unary):
val = evaluate(node.operand)
if node.op == '-':
return _coerce(-val)
raise EvalError(f"unknown unary op {node.op!r}")
if isinstance(node, BinOp):
left = evaluate(node.left)
right = evaluate(node.right)
op = node.op
if op == '+':
return _coerce(left + right)
if op == '-':
return _coerce(left - right)
if op == '*':
return _coerce(left * right)
if op == '/':
if right == 0:
raise EvalError("division by zero")
return _coerce(left / right)
raise EvalError(f"unknown binary op {op!r}")
raise EvalError(f"unknown node type {type(node).__name__}")
def _coerce(value):
if isinstance(value, float) and value == int(value):
return int(value)
return value

View File

@ -0,0 +1,52 @@
from dataclasses import dataclass
from typing import Union
class LexError(Exception):
pass
@dataclass
class Token:
kind: str
value: Union[int, float, str, None]
def tokenize(src: str) -> list:
tokens = []
i = 0
n = len(src)
while i < n:
ch = src[i]
if ch in ' \t':
i += 1
elif ch == '+':
tokens.append(Token('PLUS', '+'))
i += 1
elif ch == '-':
tokens.append(Token('MINUS', '-'))
i += 1
elif ch == '*':
tokens.append(Token('STAR', '*'))
i += 1
elif ch == '/':
tokens.append(Token('SLASH', '/'))
i += 1
elif ch == '(':
tokens.append(Token('LPAREN', '('))
i += 1
elif ch == ')':
tokens.append(Token('RPAREN', ')'))
i += 1
elif ch.isdigit() or ch == '.':
j = i
while j < n and (src[j].isdigit() or src[j] == '.'):
j += 1
raw = src[i:j]
value = float(raw) if '.' in raw else int(raw)
tokens.append(Token('NUMBER', value))
i = j
else:
raise LexError(f"unexpected character {ch!r} at position {i}")
tokens.append(Token('EOF', None))
return tokens

View File

@ -0,0 +1,118 @@
from dataclasses import dataclass
from typing import Union, List
from calc.lexer import Token
class ParseError(Exception):
pass
@dataclass
class Num:
value: Union[int, float]
def __repr__(self):
return f"Num({self.value!r})"
@dataclass
class BinOp:
op: str
left: 'Node'
right: 'Node'
def __repr__(self):
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
@dataclass
class Unary:
op: str
operand: 'Node'
def __repr__(self):
return f"Unary({self.op!r}, {self.operand!r})"
Node = Union[Num, BinOp, Unary]
class _Parser:
def __init__(self, tokens: List[Token]):
self.tokens = tokens
self.pos = 0
def _peek(self) -> Token:
return self.tokens[self.pos]
def _consume(self, kind: str = None) -> Token:
tok = self.tokens[self.pos]
if kind and tok.kind != kind:
raise ParseError(f"expected {kind!r}, got {tok.kind!r} ({tok.value!r})")
self.pos += 1
return tok
def parse(self) -> Node:
if self._peek().kind == 'EOF':
raise ParseError("empty input")
node = self._expr()
if self._peek().kind != 'EOF':
raise ParseError(
f"unexpected token {self._peek().kind!r} ({self._peek().value!r})"
)
return node
def _expr(self) -> Node:
node = self._term()
while self._peek().kind in ('PLUS', 'MINUS'):
op = self._consume().value
right = self._term()
node = BinOp(op, node, right)
return node
def _term(self) -> Node:
node = self._unary()
while self._peek().kind in ('STAR', 'SLASH'):
op = self._consume().value
right = self._unary()
node = BinOp(op, node, right)
return node
def _unary(self) -> Node:
if self._peek().kind == 'MINUS':
op = self._consume().value
return Unary(op, self._unary())
return self._primary()
def _primary(self) -> Node:
tok = self._peek()
if tok.kind == 'NUMBER':
self._consume()
return Num(tok.value)
if tok.kind == 'LPAREN':
self._consume()
node = self._expr()
if self._peek().kind != 'RPAREN':
raise ParseError(
f"expected ')', got {self._peek().kind!r} ({self._peek().value!r})"
)
self._consume()
return node
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
def parse(tokens: List[Token]) -> Node:
"""Parse a flat token list into an AST Node.
AST shape
---------
Num(value) — numeric literal (int or float)
BinOp(op, left, right) — binary op; op in {'+', '-', '*', '/'}
Unary(op, operand) — unary op; op == '-'
Precedence (high to low): unary-minus > * / > + -
Associativity: left for all binary operators.
Raises ParseError on malformed input.
"""
return _Parser(tokens).parse()

View File

@ -0,0 +1,108 @@
import subprocess
import sys
import unittest
from calc.evaluator import evaluate, EvalError
from calc.lexer import tokenize
from calc.parser import parse
def calc(expr):
return evaluate(parse(tokenize(expr)))
class TestD1Arithmetic(unittest.TestCase):
def test_add_mul_precedence(self):
self.assertEqual(calc("2+3*4"), 14)
def test_parens(self):
self.assertEqual(calc("(2+3)*4"), 20)
def test_left_assoc_sub(self):
self.assertEqual(calc("8-3-2"), 3)
def test_unary_minus(self):
self.assertEqual(calc("-2+5"), 3)
def test_mul_unary(self):
self.assertEqual(calc("2*-3"), -6)
class TestD2Division(unittest.TestCase):
def test_true_division(self):
self.assertEqual(calc("7/2"), 3.5)
def test_div_by_zero_raises_eval_error(self):
with self.assertRaises(EvalError):
calc("1/0")
def test_div_by_zero_not_bare(self):
try:
calc("1/0")
except EvalError:
pass
except ZeroDivisionError:
self.fail("ZeroDivisionError escaped; should be EvalError")
class TestD3ResultType(unittest.TestCase):
def test_whole_division_returns_int(self):
result = calc("4/2")
self.assertIsInstance(result, int)
self.assertEqual(result, 2)
def test_non_whole_division_returns_float(self):
result = calc("7/2")
self.assertIsInstance(result, float)
self.assertEqual(result, 3.5)
def test_int_arithmetic_stays_int(self):
self.assertIsInstance(calc("2+3*4"), int)
def test_negative_whole_is_int(self):
result = calc("-4/2")
self.assertIsInstance(result, int)
self.assertEqual(result, -2)
class TestD4CLI(unittest.TestCase):
def _run(self, expr):
return subprocess.run(
[sys.executable, "calc.py", expr],
capture_output=True, text=True
)
def test_basic_expression(self):
r = self._run("2+3*4")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "14")
def test_parens_cli(self):
r = self._run("(2+3)*4")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "20")
def test_float_result(self):
r = self._run("7/2")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "3.5")
def test_whole_float_no_decimal(self):
r = self._run("4/2")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "2")
def test_invalid_expression_exits_nonzero(self):
r = self._run("1 +")
self.assertNotEqual(r.returncode, 0)
self.assertTrue(r.stderr.strip(), "expected error on stderr")
def test_div_by_zero_exits_nonzero(self):
r = self._run("1/0")
self.assertNotEqual(r.returncode, 0)
self.assertTrue(r.stderr.strip())
self.assertNotIn("Traceback", r.stderr)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,101 @@
import unittest
from calc.lexer import tokenize, Token, LexError
def kinds(src):
return [t.kind for t in tokenize(src)]
def values(src):
return [(t.kind, t.value) for t in tokenize(src)]
class TestNumbers(unittest.TestCase):
def test_integer(self):
result = tokenize("42")
self.assertEqual(result[0], Token('NUMBER', 42))
self.assertEqual(result[1], Token('EOF', None))
def test_float(self):
t = tokenize("3.14")[0]
self.assertEqual(t.kind, 'NUMBER')
self.assertAlmostEqual(t.value, 3.14)
def test_leading_dot(self):
t = tokenize(".5")[0]
self.assertEqual(t.kind, 'NUMBER')
self.assertAlmostEqual(t.value, 0.5)
def test_trailing_dot(self):
t = tokenize("10.")[0]
self.assertEqual(t.kind, 'NUMBER')
self.assertAlmostEqual(t.value, 10.0)
def test_integer_value_type(self):
t = tokenize("42")[0]
self.assertIsInstance(t.value, int)
def test_float_value_type(self):
t = tokenize("3.14")[0]
self.assertIsInstance(t.value, float)
class TestOperatorsAndParens(unittest.TestCase):
def test_all_operators(self):
result = kinds("1+2*3")
self.assertEqual(result, ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'])
def test_minus(self):
self.assertIn('MINUS', kinds("1-2"))
def test_slash(self):
self.assertIn('SLASH', kinds("1/2"))
def test_parens(self):
ks = kinds("(1)")
self.assertEqual(ks[0], 'LPAREN')
self.assertEqual(ks[2], 'RPAREN')
def test_complex_expr(self):
result = values("3.5*(1-2)")
expected_kinds = ['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF']
self.assertEqual([k for k, _ in result], expected_kinds)
self.assertAlmostEqual(result[0][1], 3.5)
class TestWhitespaceAndErrors(unittest.TestCase):
def test_whitespace_skipped(self):
result = kinds(" 12 + 3 ")
self.assertEqual(result, ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_whitespace_values(self):
result = values(" 12 + 3 ")
self.assertEqual(result[0], ('NUMBER', 12))
self.assertEqual(result[1], ('PLUS', '+'))
self.assertEqual(result[2], ('NUMBER', 3))
def test_tab_skipped(self):
result = kinds("1\t+\t2")
self.assertEqual(result, ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_lex_error_at_sign(self):
with self.assertRaises(LexError) as ctx:
tokenize("1 @ 2")
self.assertIn('@', str(ctx.exception))
def test_lex_error_position(self):
with self.assertRaises(LexError) as ctx:
tokenize("1 @ 2")
self.assertIn('2', str(ctx.exception))
def test_lex_error_letter(self):
with self.assertRaises(LexError):
tokenize("1 + x")
def test_lex_error_dollar(self):
with self.assertRaises(LexError):
tokenize("$10")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,97 @@
import unittest
from calc.lexer import tokenize
from calc.parser import parse, ParseError, Num, BinOp, Unary
def p(src):
return parse(tokenize(src))
class TestPrecedence(unittest.TestCase):
def test_add_mul(self):
# 1+2*3 must parse as 1+(2*3), not (1+2)*3
self.assertEqual(p('1+2*3'), BinOp('+', Num(1), BinOp('*', Num(2), Num(3))))
def test_div_add(self):
# 6/2+1 must parse as (6/2)+1
self.assertEqual(p('6/2+1'), BinOp('+', BinOp('/', Num(6), Num(2)), Num(1)))
def test_mul_sub(self):
# 4-2*3 => 4-(2*3)
self.assertEqual(p('4-2*3'), BinOp('-', Num(4), BinOp('*', Num(2), Num(3))))
class TestLeftAssoc(unittest.TestCase):
def test_subtraction(self):
# 8-3-2 => (8-3)-2
self.assertEqual(p('8-3-2'), BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
def test_division(self):
# 8/4/2 => (8/4)/2
self.assertEqual(p('8/4/2'), BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
def test_addition(self):
# 1+2+3 => (1+2)+3
self.assertEqual(p('1+2+3'), BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
class TestParentheses(unittest.TestCase):
def test_parens_override_precedence(self):
# (1+2)*3 => BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
self.assertEqual(
p('(1+2)*3'),
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
)
def test_nested_parens(self):
self.assertEqual(p('((5))'), Num(5))
def test_parens_in_add(self):
# 2*(3+4) => BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))
self.assertEqual(
p('2*(3+4)'),
BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))
)
class TestUnaryMinus(unittest.TestCase):
def test_simple(self):
self.assertEqual(p('-5'), Unary('-', Num(5)))
def test_paren(self):
# -(1+2) => Unary('-', BinOp('+', Num(1), Num(2)))
self.assertEqual(p('-(1+2)'), Unary('-', BinOp('+', Num(1), Num(2))))
def test_after_mul(self):
# 3 * -2 => BinOp('*', Num(3), Unary('-', Num(2)))
self.assertEqual(p('3 * -2'), BinOp('*', Num(3), Unary('-', Num(2))))
def test_double_unary(self):
# --5 => Unary('-', Unary('-', Num(5)))
self.assertEqual(p('--5'), Unary('-', Unary('-', Num(5))))
class TestErrors(unittest.TestCase):
def test_trailing_op(self):
with self.assertRaises(ParseError):
p('1 +')
def test_unclosed_paren(self):
with self.assertRaises(ParseError):
p('(1')
def test_extra_number(self):
with self.assertRaises(ParseError):
p('1 2')
def test_close_before_open(self):
with self.assertRaises(ParseError):
p(')(')
def test_empty_string(self):
with self.assertRaises(ParseError):
p('')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,11 @@
# JOURNAL — phase eval
## Implementation notes
**evaluator.py**: Walks the AST recursively. Num returns its value directly. BinOp evaluates left/right then applies op. Division by zero is intercepted and re-raised as EvalError. All results pass through `_coerce()` which converts whole-valued floats to int.
**_coerce rule**: `if isinstance(value, float) and value == int(value): return int(value)`. This handles `4/2 = 2.0 → 2` and `-4/2 = -2.0 → -2` correctly. Pure int arithmetic stays int throughout (int + int = int in Python, so no coercion needed there).
**calc.py**: Catches LexError, ParseError, EvalError and prints to stderr with exit 1. No traceback exposed.
**test_evaluator.py**: 18 tests. D1 covers all 5 mandated expressions. D2 covers true division, EvalError raise, and confirms ZeroDivisionError doesn't escape. D3 checks isinstance for int/float. D4 uses subprocess to exercise CLI end-to-end.

View File

@ -0,0 +1,13 @@
# Journal — phase `lex`
## 2026-06-15
Built `calc/lexer.py` with `Token` dataclass, `LexError`, and `tokenize()`.
Design notes:
- `Token` is a dataclass with `kind: str` and `value: Union[int, float, str, None]`; EOF has `value=None`, operators carry their char as value, numbers carry the parsed numeric value.
- Number parsing: scans while digit or `.`; uses `int()` if no dot else `float()`.
- LexError message includes the offending character (quoted) and its 0-based position.
- 18 tests cover all D1D3 requirements including the plan's required expressions.
Committed ba1f056, then c465844 (STATUS sha update). Waiting for Adversary review.

View File

@ -0,0 +1,21 @@
# JOURNAL — phase parse
## Build notes
Implemented a classic recursive-descent parser.
Grammar (precedence lowest → highest):
```
expr → term (('+' | '-') term)*
term → unary (('*' | '/') unary)*
unary → '-' unary | primary
primary→ NUMBER | '(' expr ')'
```
The `while` loops in `_expr` and `_term` produce left-associative trees naturally: each iteration wraps the accumulated left subtree as the new `node`, so 8-3-2 folds as ((8-3)-2).
Unary minus recurses right (`_unary` calls itself) to allow `--5` to parse as `Unary('-', Unary('-', Num(5)))`.
Empty input is caught by checking for EOF before entering `_expr`. Trailing tokens (e.g. `1 2`) are caught by the EOF check after `_expr` returns. Unclosed parens are caught in `_primary` when expecting RPAREN but finding EOF.
36 tests: 3 precision, 3 left-assoc, 3 paren, 4 unary, 5 error, plus 18 from lexer phase.

View File

@ -0,0 +1,64 @@
# REVIEW — phase eval
_Adversary verifies each gate cold from own clone at commit `fe7e562`._
## review(init): Adversary online @2026-06-15T02:35Z
Eval plan read. DoD gates D1D5 noted. Waiting for Builder to push evaluator.py, calc.py, and test_evaluator.py.
---
## D1: PASS @2026-06-15T02:37Z
Cold run of all five mandated expressions:
- `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`
Adversarial: `--5``5` (double-unary handled correctly via recursive `_unary`) ✓; `2*(3+4)-1``13`
## D2: PASS @2026-06-15T02:37Z
Cold run:
- `python calc.py "7/2"``3.5` ✓ (true division, not floor)
- `python calc.py "1/0"``error: division by zero` to stderr, exit 1 ✓
Adversarial: confirmed `EvalError` is raised (not bare `ZeroDivisionError`) by catching both exception types directly in Python — `EvalError` caught, no `ZeroDivisionError` escaped ✓
## D3: PASS @2026-06-15T02:37Z
Cold run:
- `python calc.py "4/2"``2` (int, no trailing `.0`) ✓
- `python calc.py "7/2"``3.5` (float) ✓
Adversarial edge cases:
- `-4/2``-2` (int, not `-2.0`) ✓
- `0/5``0` (int) ✓
- `1.5+0.5``2` (int, float sum coerced when whole) ✓
- `_coerce` correctly uses `value == int(value)` check ✓
## D4: PASS @2026-06-15T02:37Z
Cold run:
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
- `python calc.py "1 +"` → stderr `error: unexpected token 'EOF' (None)`, exit 1 ✓
- `python calc.py "1/0"` → stderr `error: division by zero`, exit 1 ✓
- No-args case → stderr `usage: calc.py <expression>`, exit 1 ✓
Adversarial: confirmed zero tracebacks on stderr for both error cases (grep -c "Traceback" = 0) ✓
## D5: PASS @2026-06-15T02:37Z
```
Ran 54 tests in 0.209s
OK
```
54 tests (18 eval + 36 lex+parse), 0 failures. No regression in prior suite.
`calc/test_evaluator.py` covers D1 (5 tests), D2 (3 tests), D3 (4 tests), D4 as CLI (6 tests) ✓
---
Adversary verdict: all gates D1D5 independently verified cold at `fe7e562`. Implementation is correct.

View File

@ -0,0 +1,43 @@
# REVIEW-lex — Adversary verdicts
_Adversary verifies each gate cold from its own clone._
## Verdicts
Builder commit verified: `ba1f056`
### review(D1): PASS @2026-06-15T02:16Z
`tokenize("42")``[Token('NUMBER', 42), Token('EOF', None)]`
`tokenize("3.14")``[Token('NUMBER', 3.14), Token('EOF', None)]`
`tokenize(".5")``[Token('NUMBER', 0.5), Token('EOF', None)]`
`tokenize("10.")``[Token('NUMBER', 10.0), Token('EOF', None)]`
Integers produce `int`, floats produce `float`. Token dataclass has `kind` and `value`.
### review(D2): PASS @2026-06-15T02:16Z
`tokenize("1+2*3")``NUMBER PLUS NUMBER STAR NUMBER EOF`
All six kinds (PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN) confirmed via test suite and manual probe.
### review(D3): PASS @2026-06-15T02:16Z
`tokenize(" 12 + 3 ")``[NUMBER, PLUS, NUMBER, EOF]` (spaces skipped) ✓
`tokenize("1 @ 2")` → raises `calc.lexer.LexError: unexpected character '@' at position 2`
`tokenize("1 a 2")` → raises `LexError`
`tokenize("1 $ 2")` → raises `LexError`
Error message includes offending character and its position.
### review(D4): PASS @2026-06-15T02:16Z
```
python -m unittest -q
----------------------------------------------------------------------
Ran 18 tests in 0.000s
OK
```
Test file contains all three plan-required cases: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"` raises `LexError`. ✓
## Summary
All four gates PASS. Builder commit `ba1f056` is clean. No veto.

View File

@ -0,0 +1,53 @@
# REVIEW — phase parse
## D1: PASS @2026-06-15T02:27:10Z
Cold run: `parse(tokenize('1+2*3'))``BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`
Structure assertion passed. `*`/`/` correctly bind tighter than `+`/`-`.
Also verified: `6/2+1``BinOp('+', BinOp('/', Num(6), Num(2)), Num(1))` (div-then-add order correct).
Multi-level: `1+2*3+4``BinOp('+', BinOp('+', Num(1), BinOp('*', Num(2), Num(3))), Num(4))`
## D2: PASS @2026-06-15T02:27:10Z
Cold run:
- `8-3-2``BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))`
- `8/4/2``BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))`
Left associativity correct for both addition and division via the while-loop in `_expr`/`_term`.
## D3: PASS @2026-06-15T02:27:10Z
Cold run: `(1+2)*3``BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
Also verified: `((5))``Num(5)`, `2*(3+4)``BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))`
## D4: PASS @2026-06-15T02:27:10Z
Cold run:
- `-5``Unary('-', Num(5))`
- `-(1+2)``Unary('-', BinOp('+', Num(1), Num(2)))`
- `3 * -2``BinOp('*', Num(3), Unary('-', Num(2)))`
- `--5``Unary('-', Unary('-', Num(5)))` ✓ (recursive _unary handles chaining)
## D5: PASS @2026-06-15T02:27:10Z
All five mandated error inputs raise `ParseError` (not any other exception):
- `'1 +'` → ParseError: unexpected token 'EOF' ✓
- `'(1'` → ParseError: expected ')', got 'EOF' ✓
- `'1 2'` → ParseError: unexpected token 'NUMBER' ✓
- `')('` → ParseError: unexpected token 'RPAREN' ✓
- `''` → ParseError: empty input ✓
Adversarial: `+5` (unary plus) correctly raises ParseError (not in grammar, no crash) ✓
## D6: PASS @2026-06-15T02:27:10Z
```
Ran 36 tests in 0.001s
OK
```
36 tests, 0 failures. Suite covers D1D5 with structure-level assertions (not evaluation).
---
Adversary verdict: all gates D1D6 independently verified cold. Implementation is correct.

View File

@ -0,0 +1,52 @@
# STATUS — phase eval
## DONE
## Gates claimed: D1, D2, D3, D4, D5
### What is claimed
| Gate | Description |
|------|-------------|
| D1 | Arithmetic correctness: `+`, `-`, `*`, `/`, precedence, parens, unary minus |
| D2 | True division; division by zero raises `EvalError` (not `ZeroDivisionError`) |
| D3 | Whole results → `int`, non-whole → `float` |
| D4 | CLI `calc.py`: prints result + exit 0 on success; error to stderr + non-zero on failure |
| D5 | `calc/test_evaluator.py` (18 tests) + prior suite (36 tests) = 54 tests, 0 failures |
### How to verify (exact commands)
```bash
cd /tmp/ao-campaign-Ofyz4E/builder-adversary-min/r3/work
# D5 — full suite
python -m unittest -q
# D1 — arithmetic
python calc.py "2+3*4" # expected: 14
python calc.py "(2+3)*4" # expected: 20
python calc.py "8-3-2" # expected: 3
python calc.py "-2+5" # expected: 3
python calc.py "2*-3" # expected: -6
# D2 — true division + EvalError
python calc.py "7/2" # expected: 3.5
python calc.py "1/0" # expected: error to stderr, exit non-zero
# D3 — result type
python calc.py "4/2" # expected: 2 (int, no .0)
python calc.py "7/2" # expected: 3.5 (float)
# D4 — CLI error handling
python calc.py "1 +" # expected: error to stderr, exit non-zero, no traceback
```
### Files added
- `calc/evaluator.py``evaluate(node) -> int | float`, `EvalError`
- `calc.py` — top-level CLI
- `calc/test_evaluator.py` — 18 unittest tests covering D1D4
### Commit SHA
`fe7e562080ee15b1f13f962171cdc4719734b062`

View File

@ -0,0 +1,56 @@
# Status — phase `lex`
## Claimed gates
- **D1** — integers and floats tokenize to NUMBER with correct numeric value
- **D2** — `+ - * / ( )` tokenize to the right kinds
- **D3** — whitespace skipped; invalid chars raise `LexError` with offending char and position
- **D4** — `calc/test_lexer.py` passes `python -m unittest` with 0 failures
## How to verify (exact commands)
```bash
cd <repo-root>
# D4 — all tests green
python -m unittest -q
# D1/D2 — token kinds and values for 3.5*(1-2)
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
# D3 — LexError raised for invalid char
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
## Expected results
`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)]
```
`tokenize('1 @ 2')`:
```
raises calc.lexer.LexError: unexpected character '@' at position 2
```
## Files
- `calc/__init__.py` — package marker
- `calc/lexer.py``Token`, `LexError`, `tokenize()`
- `calc/test_lexer.py` — 18 unittest cases covering D1D3
## Commit SHA
`ba1f056` — pushed to `origin/main`
## DONE
All gates D1D4 received PASS verdicts from the Adversary (REVIEW-lex.md @2026-06-15T02:16Z). No veto. Phase `lex` is complete.

View File

@ -0,0 +1,91 @@
# STATUS — phase parse
## DONE
## Claimed gates: D1, D2, D3, D4, D5, D6
Commit: fa50146a5acbc9e7cf65a1e60e0b0fba2d8bd2ea (main)
Files: `calc/parser.py`, `calc/test_parser.py`
---
## What is claimed
- **D1 — precedence**: `*`/`/` bind tighter than `+`/`-`
- **D2 — left associativity**: same-precedence ops are left-associative
- **D3 — parentheses**: parens override default precedence
- **D4 — unary minus**: leading and nested unary minus handled
- **D5 — errors**: malformed inputs raise `ParseError` (not any other exception)
- **D6 — tests green**: `python -m unittest -q` → 36 tests, 0 failures
---
## How to verify (exact commands)
```bash
# D6 — all tests green
python -m unittest -q
# D1 — mul binds tighter than add
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('1+2*3')); print(t); assert str(t)==\"BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))\""
# D2 — left associativity subtraction
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('8-3-2')); print(t); assert str(t)==\"BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))\""
# D2 — left associativity division
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('8/4/2')); print(t); assert str(t)==\"BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))\""
# D3 — parens override precedence
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('(1+2)*3')); print(t); assert str(t)==\"BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))\""
# D4 — unary minus
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5'))); print(parse(tokenize('-(1+2)'))); print(parse(tokenize('3 * -2')))"
# D5 — all five error inputs 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('FAIL — no error for', repr(src))
except ParseError:
print('OK', repr(src))
"
```
---
## Expected output
**D6**: `Ran 36 tests ... OK`
**D1**: `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` (no assertion error)
**D2-sub**: `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` (no assertion error)
**D2-div**: `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` (no assertion error)
**D3**: `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` (no assertion error)
**D4**:
```
Unary('-', Num(5))
Unary('-', BinOp('+', Num(1), Num(2)))
BinOp('*', Num(3), Unary('-', Num(2)))
```
**D5**: Five lines all starting with `OK`
---
## AST shape reference
```
Num(value) # numeric literal; value: int | float
BinOp(op, left, right) # binary op; op in {'+', '-', '*', '/'}
Unary(op, operand) # unary op; op == '-'
```
Precedence (high → low): unary-minus > `*` `/` > `+` `-`
Associativity: left for all binary operators