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,2 @@
*.pyc
__pycache__/

View File

@ -0,0 +1,29 @@
# git history (claim/review handshake), from the run's shared bare repo
1c5c9d5 status(eval): mark phase DONE — all gates D1-D5 Adversary-verified PASS
f3fd7a8 review(D1,D2,D3,D4,D5): PASS — all eval gates cold-verified, no defects found
21d63f1 claim(D5): 69 tests pass under python -m unittest, 0 failures — lex+parse+eval, no regression
b8e6217 claim(D4): python calc.py "2+3*4" prints 14, exit 0; "1 +" and "1/0" print to stderr, exit 1
ab3ab3a claim(D3): 4/2 => int 2 (no .0); 7/2 => float 3.5 — _normalize() enforces rule
0a16797 claim(D2): 7/2 => 3.5 (true division); 1/0 raises EvalError not ZeroDivisionError
4a4df7e claim(D1): evaluate(parse(tokenize(s))) correct for +,-,*,/, precedence, parens, unary minus
f8f0758 review(eval): initialize REVIEW-eval.md — awaiting Builder claims
ca8ba36 status(parse): mark phase DONE — all gates D1-D6 Adversary-verified PASS
62ad955 review(D1,D2,D3,D4,D5,D6): PASS — all gates cold-verified, no defects found
e33077f claim(D6): 48 tests pass under python -m unittest, 0 failures
f826133 claim(D5): '1 +', '(1', '1 2', ')(', '' all raise ParseError
10577df claim(D4): -5 => Unary('-', Num(5)); 3*-2 => BinOp('*', Num(3), Unary('-', Num(2)))
ba8257f claim(D3): (1+2)*3 => BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) — parens override precedence
61d5f22 claim(D2): 8-3-2 => BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) — left associativity
59efd6a claim(D1): 1+2*3 => BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) — mul binds tighter than add
a2d62a7 feat(parse): implement parser.py + test_parser.py; all 48 tests pass
0e55081 review(parse): initialize REVIEW-parse.md — awaiting Builder claims
959e066 status(lex): mark phase DONE — all gates Adversary-verified PASS
d9457c6 review(D1,D2,D3,D4): PASS — all gates cold-verified, no defects found
ab4eac4 claim(D4): 24 tests pass under python -m unittest, 0 failures
c1ae42b claim(D3): whitespace skipped; LexError raised with char+position
4ba50d9 claim(D2): operators and parens tokenize to correct kinds
7f8620f claim(D1): numbers tokenize correctly — int/float values, EOF appended
8659f9c chore: add .gitignore for pycache
89cce57 feat(lex): add lexer implementation and tests
9a9041a review(init): Adversary initialized, awaiting Builder gate claims
8b5ee89 chore: seed

View File

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

View File

@ -0,0 +1 @@
original path: /tmp/ao-campaign-ufRkmF/builder-adversary-lean/r5

View File

@ -0,0 +1,21 @@
#!/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)
try:
result = evaluate(parse(tokenize(sys.argv[1])))
print(result)
except (LexError, ParseError, EvalError) as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,42 @@
"""Evaluator for calc AST nodes.
evaluate(node) -> int | float
Result-type rule: whole-valued results are returned as int;
fractional results as float. So 4/2 -> 2, 7/2 -> 3.5.
"""
from calc.parser import Num, BinOp, Unary
class EvalError(Exception):
pass
def _normalize(value):
"""Convert a whole-valued float to int; leave everything else alone."""
if isinstance(value, float) and value.is_integer():
return int(value)
return value
def evaluate(node):
"""Walk an AST node and return its numeric value."""
if isinstance(node, Num):
return _normalize(node.value)
if isinstance(node, Unary):
return _normalize(-evaluate(node.operand))
if isinstance(node, BinOp):
left = evaluate(node.left)
right = evaluate(node.right)
if node.op == '+':
return _normalize(left + right)
if node.op == '-':
return _normalize(left - right)
if node.op == '*':
return _normalize(left * right)
if node.op == '/':
if right == 0:
raise EvalError('division by zero')
return _normalize(left / right)
raise EvalError(f'unknown node type: {type(node).__name__}')

View File

@ -0,0 +1,54 @@
import re
_NUMBER_RE = re.compile(r'\d+\.?\d*|\.\d+')
_SINGLE = {
'+': 'PLUS',
'-': 'MINUS',
'*': 'STAR',
'/': 'SLASH',
'(': 'LPAREN',
')': 'RPAREN',
}
class LexError(Exception):
pass
class Token:
__slots__ = ('kind', 'value')
def __init__(self, kind: str, value):
self.kind = kind
self.value = value
def __repr__(self):
return f'Token({self.kind}, {self.value!r})'
def __eq__(self, other):
return isinstance(other, Token) and self.kind == other.kind and self.value == other.value
def tokenize(src: str) -> list:
tokens = []
i = 0
while i < len(src):
ch = src[i]
if ch in ' \t':
i += 1
continue
if ch in _SINGLE:
tokens.append(Token(_SINGLE[ch], ch))
i += 1
continue
m = _NUMBER_RE.match(src, i)
if m:
raw = m.group()
value = float(raw) if '.' in raw else int(raw)
tokens.append(Token('NUMBER', value))
i = m.end()
continue
raise LexError(f"unexpected character {ch!r} at position {i}")
tokens.append(Token('EOF', None))
return tokens

View File

@ -0,0 +1,127 @@
"""Recursive-descent parser for calc expressions.
Grammar:
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := '-' factor | primary
primary := NUMBER | '(' expr ')'
Node shapes:
Num(value) — a numeric literal; value is int or float
BinOp(op, left, right) — op is one of '+' '-' '*' '/'
Unary(op, operand) — op is '-'
"""
class ParseError(Exception):
pass
class Num:
__slots__ = ('value',)
def __init__(self, value):
self.value = value
def __repr__(self):
return f'Num({self.value!r})'
def __eq__(self, other):
return isinstance(other, Num) and self.value == other.value
class BinOp:
__slots__ = ('op', 'left', 'right')
def __init__(self, op, left, right):
self.op = op
self.left = left
self.right = right
def __repr__(self):
return f'BinOp({self.op!r}, {self.left!r}, {self.right!r})'
def __eq__(self, other):
return (isinstance(other, BinOp) and self.op == other.op
and self.left == other.left and self.right == other.right)
class Unary:
__slots__ = ('op', 'operand')
def __init__(self, op, operand):
self.op = op
self.operand = operand
def __repr__(self):
return f'Unary({self.op!r}, {self.operand!r})'
def __eq__(self, other):
return (isinstance(other, Unary) and self.op == other.op
and self.operand == other.operand)
class _Parser:
def __init__(self, tokens):
self._tokens = tokens
self._pos = 0
def _peek(self):
return self._tokens[self._pos]
def _consume(self, kind=None):
tok = self._tokens[self._pos]
if kind and tok.kind != kind:
raise ParseError(f'expected {kind}, got {tok.kind!r}')
self._pos += 1
return tok
def parse(self):
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}')
return node
def _expr(self):
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 = self._factor()
while self._peek().kind in ('STAR', 'SLASH'):
op = self._consume().value
right = self._factor()
node = BinOp(op, node, right)
return node
def _factor(self):
if self._peek().kind == 'MINUS':
self._consume()
operand = self._factor()
return Unary('-', operand)
return self._primary()
def _primary(self):
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'unclosed parenthesis, got {self._peek().kind!r}')
self._consume()
return node
raise ParseError(f'unexpected token {tok.kind!r}')
def parse(tokens):
"""Parse a token list from tokenize() and return an AST node."""
return _Parser(tokens).parse()

View File

@ -0,0 +1,126 @@
import subprocess
import sys
import unittest
from calc.lexer import tokenize
from calc.parser import parse
from calc.evaluator import evaluate, EvalError
def calc(s):
return evaluate(parse(tokenize(s)))
class TestArithmetic(unittest.TestCase):
"""D1 — arithmetic correctness."""
def test_add_mul_precedence(self):
self.assertEqual(calc('2+3*4'), 14)
def test_parens_override_precedence(self):
self.assertEqual(calc('(2+3)*4'), 20)
def test_sub_left_assoc(self):
self.assertEqual(calc('8-3-2'), 3)
def test_unary_minus_simple(self):
self.assertEqual(calc('-2+5'), 3)
def test_unary_minus_in_mul(self):
self.assertEqual(calc('2*-3'), -6)
def test_simple_add(self):
self.assertEqual(calc('1+2'), 3)
def test_simple_sub(self):
self.assertEqual(calc('10-4'), 6)
def test_simple_mul(self):
self.assertEqual(calc('3*4'), 12)
class TestDivision(unittest.TestCase):
"""D2 — division and EvalError."""
def test_true_division(self):
self.assertAlmostEqual(calc('7/2'), 3.5)
def test_division_by_zero_raises_eval_error(self):
with self.assertRaises(EvalError):
calc('1/0')
def test_division_by_zero_not_bare_exception(self):
try:
calc('1/0')
self.fail('expected EvalError')
except EvalError:
pass
except ZeroDivisionError:
self.fail('ZeroDivisionError escaped the API')
class TestResultType(unittest.TestCase):
"""D3 — result type: whole -> int, fractional -> float."""
def test_whole_division_returns_int(self):
result = calc('4/2')
self.assertIsInstance(result, int)
self.assertEqual(result, 2)
def test_fractional_division_returns_float(self):
result = calc('7/2')
self.assertIsInstance(result, float)
self.assertEqual(result, 3.5)
def test_int_arithmetic_returns_int(self):
self.assertIsInstance(calc('2+3'), int)
def test_whole_float_input_normalizes(self):
result = calc('2.0+1.0')
self.assertIsInstance(result, int)
self.assertEqual(result, 3)
class TestCLI(unittest.TestCase):
"""D4 — CLI behaviour."""
def _run(self, expr):
return subprocess.run(
[sys.executable, 'calc.py', expr],
capture_output=True, text=True,
cwd=__file__.replace('/calc/test_evaluator.py', ''),
)
def test_valid_expression_exits_zero_and_prints_result(self):
r = self._run('2+3*4')
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), '14')
def test_invalid_expression_exits_nonzero_and_stderr(self):
r = self._run('1 +')
self.assertNotEqual(r.returncode, 0)
self.assertTrue(r.stderr.strip())
def test_division_by_zero_exits_nonzero_and_stderr(self):
r = self._run('1/0')
self.assertNotEqual(r.returncode, 0)
self.assertTrue(r.stderr.strip())
def test_parens_result(self):
r = self._run('(2+3)*4')
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), '20')
def test_true_division_result(self):
r = self._run('7/2')
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), '3.5')
def test_whole_division_no_dot_zero(self):
r = self._run('4/2')
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), '2')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,120 @@
import unittest
from calc.lexer import tokenize, Token, LexError
def kinds(src):
return [t.kind for t in tokenize(src)]
def pairs(src):
return [(t.kind, t.value) for t in tokenize(src)]
class TestNumbers(unittest.TestCase):
def test_integer(self):
toks = tokenize("42")
self.assertEqual(toks, [Token('NUMBER', 42), Token('EOF', None)])
def test_float(self):
toks = tokenize("3.14")
self.assertEqual(toks, [Token('NUMBER', 3.14), Token('EOF', None)])
def test_float_leading_dot(self):
toks = tokenize(".5")
self.assertEqual(toks, [Token('NUMBER', 0.5), Token('EOF', None)])
def test_float_trailing_dot(self):
toks = tokenize("10.")
self.assertEqual(toks, [Token('NUMBER', 10.0), Token('EOF', None)])
def test_integer_value_is_int(self):
toks = tokenize("7")
self.assertIsInstance(toks[0].value, int)
def test_float_value_is_float(self):
toks = tokenize("3.14")
self.assertIsInstance(toks[0].value, float)
class TestOperatorsAndParens(unittest.TestCase):
def test_single_plus(self):
self.assertEqual(kinds("+"), ['PLUS', 'EOF'])
def test_single_minus(self):
self.assertEqual(kinds("-"), ['MINUS', 'EOF'])
def test_single_star(self):
self.assertEqual(kinds("*"), ['STAR', 'EOF'])
def test_single_slash(self):
self.assertEqual(kinds("/"), ['SLASH', 'EOF'])
def test_single_lparen(self):
self.assertEqual(kinds("("), ['LPAREN', 'EOF'])
def test_single_rparen(self):
self.assertEqual(kinds(")"), ['RPAREN', 'EOF'])
def test_expression_1_plus_2_star_3(self):
self.assertEqual(kinds("1+2*3"), ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'])
def test_expression_3_5_star_paren(self):
self.assertEqual(kinds("3.5*(1-2)"),
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'])
class TestWhitespaceAndErrors(unittest.TestCase):
def test_spaces_between_tokens(self):
self.assertEqual(kinds(" 12 + 3 "), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_tab_between_tokens(self):
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_spaces_values(self):
toks = tokenize(" 12 + 3 ")
self.assertEqual(toks[0].value, 12)
self.assertEqual(toks[1].kind, 'PLUS')
self.assertEqual(toks[2].value, 3)
def test_lex_error_at_sign(self):
with self.assertRaises(LexError):
tokenize("1 @ 2")
def test_lex_error_dollar(self):
with self.assertRaises(LexError):
tokenize("$")
def test_lex_error_letter(self):
with self.assertRaises(LexError):
tokenize("x")
def test_lex_error_message_contains_char(self):
try:
tokenize("1 @ 2")
self.fail("expected LexError")
except LexError as e:
self.assertIn('@', str(e))
def test_lex_error_message_contains_position(self):
try:
tokenize("1 @ 2")
self.fail("expected LexError")
except LexError as e:
# position 2 is the '@'
self.assertIn('2', str(e))
def test_empty_string(self):
self.assertEqual(kinds(""), ['EOF'])
def test_complex_expression(self):
toks = tokenize("3.5*(1-2)")
self.assertEqual(
[(t.kind, t.value) for t in toks],
[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('),
('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2),
('RPAREN', ')'), ('EOF', None)]
)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,114 @@
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_then_mul(self):
# 1+2*3 => BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
self.assertEqual(p('1+2*3'), BinOp('+', Num(1), BinOp('*', Num(2), Num(3))))
def test_mul_then_add(self):
# 2*3+1 => BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
self.assertEqual(p('2*3+1'), BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)))
def test_add_then_div(self):
# 6+10/2 => BinOp('+', Num(6), BinOp('/', Num(10), Num(2)))
self.assertEqual(p('6+10/2'), BinOp('+', Num(6), BinOp('/', Num(10), Num(2))))
def test_sub_then_mul(self):
# 5-2*3 => BinOp('-', Num(5), BinOp('*', Num(2), Num(3)))
self.assertEqual(p('5-2*3'), BinOp('-', Num(5), BinOp('*', Num(2), Num(3))))
class TestLeftAssociativity(unittest.TestCase):
def test_subtraction(self):
# 8-3-2 => BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
self.assertEqual(p('8-3-2'), BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
def test_division(self):
# 8/4/2 => BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
self.assertEqual(p('8/4/2'), BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
def test_addition(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_multiplication(self):
# 2*3*4 => BinOp('*', BinOp('*', Num(2), Num(3)), Num(4))
self.assertEqual(p('2*3*4'), BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
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_parens_right(self):
# 1*(2+3) => BinOp('*', Num(1), BinOp('+', Num(2), Num(3)))
self.assertEqual(p('1*(2+3)'), BinOp('*', Num(1), BinOp('+', Num(2), Num(3))))
def test_nested_parens(self):
# ((4)) => Num(4)
self.assertEqual(p('((4))'), Num(4))
def test_parens_with_sub_vs_mul(self):
# 3*(4-1) => BinOp('*', Num(3), BinOp('-', Num(4), Num(1)))
self.assertEqual(p('3*(4-1)'), BinOp('*', Num(3), BinOp('-', Num(4), Num(1))))
class TestUnaryMinus(unittest.TestCase):
def test_simple_unary(self):
# -5 => Unary('-', Num(5))
self.assertEqual(p('-5'), Unary('-', Num(5)))
def test_unary_parens(self):
# -(1+2) => Unary('-', BinOp('+', Num(1), Num(2)))
self.assertEqual(p('-(1+2)'), Unary('-', BinOp('+', Num(1), Num(2))))
def test_mul_unary(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))))
def test_unary_in_add(self):
# 1 + -2 => BinOp('+', Num(1), Unary('-', Num(2)))
self.assertEqual(p('1 + -2'), BinOp('+', Num(1), Unary('-', Num(2))))
class TestErrors(unittest.TestCase):
def _raises(self, src):
with self.assertRaises(ParseError):
p(src)
def test_trailing_op(self):
self._raises('1 +')
def test_unclosed_paren(self):
self._raises('(1')
def test_two_numbers(self):
self._raises('1 2')
def test_close_then_open(self):
self._raises(')(')
def test_empty(self):
self._raises('')
def test_op_only(self):
self._raises('*')
def test_mismatched_parens(self):
self._raises('(1+2')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,7 @@
# BACKLOG — eval phase
## Build backlog
<!-- Builder maintains this section -->
## Adversary findings
<!-- Adversary-only section. Items opened here; only Adversary closes them. -->

View File

@ -0,0 +1,15 @@
# BACKLOG — lex phase
## Build backlog
- [x] Implement `calc/lexer.py` with Token, LexError, tokenize()
- [x] Implement `calc/test_lexer.py` with full coverage
- [x] Claim D1 (numbers)
- [x] Claim D2 (operators & parens)
- [x] Claim D3 (whitespace & errors)
- [x] Claim D4 (tests green)
- [ ] Await Adversary PASS on all gates → write ## DONE to STATUS
## Adversary findings
<!-- Adversary owns this section -->

View File

@ -0,0 +1,16 @@
# BACKLOG — parse phase
## Build backlog
- [x] Implement `calc/parser.py` with Num, BinOp, Unary nodes and `parse()` function
- [x] Implement `calc/test_parser.py` with 24 tests covering D1D5
- [x] Verify D1: precedence (`1+2*3` tree structure confirmed)
- [x] Verify D2: left associativity (`8-3-2`, `8/4/2` tree structure confirmed)
- [x] Verify D3: parentheses (`(1+2)*3` tree structure confirmed)
- [x] Verify D4: unary minus (`-5`, `-(1+2)`, `3*-2` confirmed)
- [x] Verify D5: all 5 error cases raise ParseError
- [x] Run full test suite: 48/48 pass
## Adversary findings
(pending)

View File

@ -0,0 +1,12 @@
# DECISIONS (append-only, shared)
## lex phase
### Token type
Used a simple class with `__slots__` for efficiency and `__eq__` for easy test assertions. No namedtuple to allow future extension (e.g. position tracking for later phases).
### Number regex
`r'\d+\.?\d*|\.\d+'` — handles `42`, `3.14`, `10.`, `.5`. The alternation order matters: longest match first via `re.match` at position `i`.
### EOF token
`Token('EOF', None)` — value is None since EOF carries no semantic content; downstream parser phases can type-check on `kind == 'EOF'`.

View File

@ -0,0 +1,35 @@
# JOURNAL — eval phase
## Session 1
Implemented evaluator, CLI, and tests in one pass.
### evaluator.py design
`evaluate(node)` recursively walks `Num`, `BinOp`, `Unary` nodes. `EvalError` wraps division by zero so no bare `ZeroDivisionError` escapes the API. `_normalize(value)` converts whole-valued floats to int (satisfying D3).
### Test run
```
$ python -m unittest -q
Ran 69 tests in 0.113s
OK
```
Prior suite had 48 tests (lex+parse); 21 new tests from `test_evaluator.py`.
### CLI manual check
```
$ 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 "8-3-2" → 3
$ python calc.py "-2+5" → 3
$ python calc.py "2*-3" → -6
$ python calc.py "1/0" → error: division by zero (exit 1)
$ python calc.py "1 +" → error: unexpected token 'EOF' (exit 1)
```
All match plan-specified expected values.

View File

@ -0,0 +1,29 @@
# JOURNAL — lex phase (Builder)
## Session 1
### Implementation
Built `calc/lexer.py` with:
- `Token` class with `__slots__ = ('kind', 'value')` and `__eq__` for test assertions
- `LexError` exception
- `_NUMBER_RE = re.compile(r'\d+\.?\d*|\.\d+')` — matches integers, floats with/without leading dot
- `_SINGLE` dict mapping `+ - * / ( )` to kind strings
- `tokenize(src)`: iterates, skips whitespace, checks single-char ops, then tries number regex, raises LexError on unknown char; appends EOF at end
### Test run
```
$ python -m unittest -q
Ran 24 tests in 0.000s
OK
```
### Verification commands
```
$ 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
```

View File

@ -0,0 +1,66 @@
# JOURNAL — parse phase (Builder)
## 2026-06-15 — Initial implementation
**Design decisions:**
Grammar used (standard precedence climbing via two layers):
```
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := '-' factor | primary
primary := NUMBER | '(' expr ')'
```
Left associativity falls out of the `while` loop pattern in `_expr()` and `_term()` — each new BinOp wraps the accumulated left node.
Unary minus in `_factor()` recurses to itself, so `--5``Unary('-', Unary('-', Num(5)))` correctly. The placement in `_factor()` (between `_term()` and `_primary()`) means `3 * -2` works: `_term()` calls `_factor()` for the right operand, which detects the MINUS.
**Verification runs:**
D1:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
```
D2:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
```
D3:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
```
D4:
```
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
Unary('-', Num(5))
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
Unary('-', BinOp('+', Num(1), Num(2)))
$ python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
BinOp('*', Num(3), Unary('-', Num(2)))
```
D5 (all 5 required error cases + extras):
```
OK: '1 +' -> ParseError: unexpected token 'EOF'
OK: '(1' -> ParseError: unclosed parenthesis, got 'EOF'
OK: '1 2' -> ParseError: unexpected token 'NUMBER'
OK: ')(' -> ParseError: unexpected token 'RPAREN'
OK: '' -> ParseError: empty input
```
D6:
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 48 tests in 0.002s
OK
```

View File

@ -0,0 +1,71 @@
# REVIEW — eval phase
Adversary cold-verification log. Each gate gets its own independent pass.
---
## eval/D1: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestArithmetic -v`
**Result:** 8 tests, 0 failures.
**Spot-check** (all from CLI):
- `2+3*4``14`
- `(2+3)*4``20`
- `8-3-2``3`
- `-2+5``3`
- `2*-3``-6`
**Break-it probes:** Associativity, precedence, and unary minus all checked. No defects found.
---
## eval/D2: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestDivision -v`
**Result:** 3 tests, 0 failures.
**Spot-check:**
- `7/2``3.5`
- `1/0` → error to stderr, exit 1 ✓
**Break-it probes:** Confirmed `EvalError` is raised (not bare `ZeroDivisionError`). Cold Python assertion test confirms `except EvalError` catches it and `except ZeroDivisionError` does not.
---
## eval/D3: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestResultType -v`
**Result:** 4 tests, 0 failures.
**Spot-check:**
- `4/2``2` (int, no `.0`) ✓
- `7/2``3.5` (float) ✓
**Break-it probes:** `isinstance` checks confirm `4/2` returns `int`, `7/2` returns `float`. `_normalize()` correctly converts whole-valued floats to int.
---
## eval/D4: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest calc.test_evaluator.TestCLI -v`
**Result:** 6 tests, 0 failures.
**Spot-check:**
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
- `python calc.py "1 +"` → error to stderr only (stdout empty), exit 1 ✓
- `python calc.py "1/0"` → error to stderr only, exit 1 ✓
- No tracebacks on any error input (verified with `""`, `"1+"`, `"++1"`, `"1 2"`)
---
## eval/D5: PASS @2026-06-15T06:19:58Z
**Command:** `python -m unittest -q`
**Result:** 69 tests, 0 failures (~0.12s). All prior phases (lex + parse) still pass. No regression.
---
## Summary
All 5 eval gates PASS. No veto. No defects found.

View File

@ -0,0 +1,60 @@
# REVIEW — lex phase (Adversary)
## Status
All four gates verified PASS. No defects found.
## Gate verdicts
### lex/D1: PASS @2026-06-15T06:09:05Z
**Evidence:**
```
NUMBER 42 int
NUMBER 3.14 float
NUMBER 0.5 float
NUMBER 10.0 float
```
- `tokenize("42")``[Token(NUMBER, 42), Token(EOF, None)]` — int type confirmed
- All float variants (`3.14`, `.5`, `10.`) produce float values
- Regex `r'\d+\.?\d*|\.\d+'` correctly handles all cases
### lex/D2: PASS @2026-06-15T06:09:05Z
**Evidence:**
```
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
['PLUS', 'MINUS', 'STAR', 'SLASH', 'LPAREN', 'RPAREN', 'EOF']
```
- `tokenize("1+2*3")` → correct 6-token sequence
- All six operators `+-*/()` map to correct kinds
### lex/D3: PASS @2026-06-15T06:09:05Z
**Evidence:**
- `tokenize(" 12 + 3 ")``['NUMBER', 'PLUS', 'NUMBER', 'EOF']` — spaces skipped
- `tokenize("1\t+\t2")``['NUMBER', 'PLUS', 'NUMBER', 'EOF']` — tabs skipped
- `tokenize("1 @ 2")` raises `calc.lexer.LexError: unexpected character '@' at position 2`
- LexError message contains both the char (`@`) and position (`2`) ✓
- Letters (`abc`) and `$` also raise LexError ✓
### lex/D4: PASS @2026-06-15T06:09:05Z
**Evidence:**
```
----------------------------------------------------------------------
Ran 24 tests in 0.000s
OK
```
`python -m unittest -q` — 24 tests, 0 failures, 0 errors
## Probes run (independent / adversarial)
All probes ran from cold start in Adversary's own clone.
- `tokenize('')``['EOF']` — empty string handled ✓
- `tokenize('10.+.5')``[(NUMBER, 10.0), (PLUS, +), (NUMBER, 0.5), EOF]` — consecutive floats ✓
- `tokenize('-3')``[(MINUS, -), (NUMBER, 3), EOF]` — unary minus handled as operator ✓
- `tokenize('((1))')``[LPAREN, LPAREN, NUMBER, RPAREN, RPAREN, EOF]` — nested parens ✓
- `tokenize('abc')` raises LexError at position 0 ✓
- `tokenize('$')` raises LexError at position 0 ✓
- Plan's own verification command confirmed:
`tokenize('3.5*(1-2)')``[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
No defects found. No veto.

View File

@ -0,0 +1,106 @@
# REVIEW — parse phase (Adversary)
## Status
All six gates verified PASS. No defects found. No veto.
## Gate verdicts
### parse/D1: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) # 1+2*3
BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)) # 2*3+1
BinOp('-', Num(5), BinOp('*', Num(2), Num(3))) # 5-2*3
```
- `*`/`/` bind tighter than `+`/`-` in all three forms ✓
- `_term()` loop handles `STAR`/`SLASH` before `_expr()` handles `PLUS`/`MINUS`
**Adversarial probes (all correct):**
- `1+6/2``BinOp('+', Num(1), BinOp('/', Num(6), Num(2)))`
- `2*3+4*5``BinOp('+', BinOp('*', Num(2), Num(3)), BinOp('*', Num(4), Num(5)))`
- `10-2*3+1``BinOp('+', BinOp('-', Num(10), BinOp('*', Num(2), Num(3))), Num(1))`
---
### parse/D2: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) # 8-3-2
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) # 8/4/2
```
- Both levels associate left via `while` loops in `_expr()` and `_term()`
**Adversarial probes (all correct):**
- `1+2+3``BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))`
- `1-2+3``BinOp('+', BinOp('-', Num(1), Num(2)), Num(3))`
---
### parse/D3: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) # (1+2)*3
BinOp('*', Num(1), BinOp('+', Num(2), Num(3))) # 1*(2+3)
```
- `_primary()` calls `_expr()` recursively on paren contents ✓
**Adversarial probes (all correct):**
- `((1+2))*3``BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
- `(2*(3+4))``BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))`
---
### parse/D4: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
Unary('-', Num(5)) # -5
Unary('-', BinOp('+', Num(1), Num(2))) # -(1+2)
BinOp('*', Num(3), Unary('-', Num(2))) # 3 * -2
```
- `_factor()` recurses for unary minus; `_term()` calls `_factor()` so unary applies at factor level ✓
**Adversarial probes (all correct):**
- `--5``Unary('-', Unary('-', Num(5)))` ✓ (double unary via recursion)
- `-(2*3)``Unary('-', BinOp('*', Num(2), Num(3)))`
- `1 - -2``BinOp('-', Num(1), Unary('-', Num(2)))` ✓ (binary then unary)
- `-1 + 2``BinOp('+', Unary('-', Num(1)), Num(2))`
---
### parse/D5: PASS @2026-06-15T06:13:00Z
**Evidence (cold run) — all five required cases raise ParseError, not LexError/IndexError/etc.:**
```
OK: '1 +' -> ParseError: unexpected token 'EOF'
OK: '(1' -> ParseError: unclosed parenthesis, got 'EOF'
OK: '1 2' -> ParseError: unexpected token 'NUMBER'
OK: ')(' -> ParseError: unexpected token 'RPAREN'
OK: '' -> ParseError: empty input
```
**Adversarial error probes (all raise ParseError cleanly):**
- `'5 *'` → ParseError: unexpected token 'EOF' ✓
- `'()'` → ParseError: unexpected token 'RPAREN' ✓
- `'+5'` → ParseError: unexpected token 'PLUS' ✓ (unary plus unsupported)
- `'1++2'` → ParseError: unexpected token 'PLUS' ✓
- `'1 2 3'` → ParseError: unexpected token 'NUMBER' ✓
- `'((1)'` → ParseError: unclosed parenthesis, got 'EOF' ✓
---
### parse/D6: PASS @2026-06-15T06:13:00Z
**Evidence (cold run):**
```
----------------------------------------------------------------------
Ran 48 tests in 0.001s
OK
```
`python -m unittest -q` — 48 tests (24 parser + 24 lexer), 0 failures, 0 errors ✓
---
## Probes run (independent / adversarial)
All probes ran from cold start in Adversary's own clone (work-adv/).
No defects found. No veto.

View File

@ -0,0 +1,97 @@
# STATUS — eval phase
## DONE
All gates D1D5 Adversary-verified PASS @2026-06-15T06:19:58Z. No veto. Sequence complete.
## Current state
All gates D1D5 implemented, claimed, and Adversary-verified.
## Gate D1 — CLAIMED, awaiting Adversary
**WHAT:** `evaluate(parse(tokenize(s)))` correct for +, -, *, /, precedence, parens, unary minus.
**HOW:** Run from repo root:
```bash
python -m unittest calc.test_evaluator.TestArithmetic -v
```
**EXPECTED:** 8 tests pass, 0 failures.
**WHERE:** `calc/evaluator.py`, `calc/test_evaluator.py` @ HEAD
Spot-check direct values:
```
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
```
---
## Gate D2 — CLAIMED, awaiting Adversary
**WHAT:** `/` is true division; division by zero raises `EvalError`, not bare `ZeroDivisionError`.
**HOW:**
```bash
python -m unittest calc.test_evaluator.TestDivision -v
python calc.py "7/2" # 3.5
python calc.py "1/0" # error to stderr, exit 1
```
**EXPECTED:** 3 tests pass; `7/2``3.5`; `1/0` → stderr + exit 1.
**WHERE:** `calc/evaluator.py` lines 3034, `calc/test_evaluator.py` class `TestDivision`.
---
## Gate D3 — CLAIMED, awaiting Adversary
**WHAT:** Whole-valued results → int (no `.0`); fractional → float.
**HOW:**
```bash
python -m unittest calc.test_evaluator.TestResultType -v
python calc.py "4/2" # 2
python calc.py "7/2" # 3.5
```
**EXPECTED:** 4 tests pass; `4/2``2`; `7/2``3.5`.
**WHERE:** `_normalize()` in `calc/evaluator.py` lines 1417; class `TestResultType` in `calc/test_evaluator.py`.
---
## Gate D4 — CLAIMED, awaiting Adversary
**WHAT:** `python calc.py "2+3*4"` prints `14`, exits 0; invalid expr prints to stderr and exits nonzero.
**HOW:**
```bash
python calc.py "2+3*4" # stdout: 14, exit 0
python calc.py "1 +" # stderr: error message, exit 1
python calc.py "1/0" # stderr: error message, exit 1
python -m unittest calc.test_evaluator.TestCLI -v
```
**EXPECTED:** 6 CLI tests pass; outputs as stated above.
**WHERE:** `calc.py` (repo root).
---
## Gate D5 — CLAIMED, awaiting Adversary
**WHAT:** Full suite (lex + parse + eval) passes with 0 failures; prior phases not regressed.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:** 69 tests in ~0.1s, OK, 0 failures.
**WHERE:** `calc/test_lexer.py`, `calc/test_parser.py`, `calc/test_evaluator.py`.

View File

@ -0,0 +1,86 @@
# STATUS — lex phase (Builder)
## DONE
All DoD gates (D1D4) verified PASS by Adversary @2026-06-15T06:09:05Z. No veto. Phase complete.
## Gates
### D1 — numbers: **CLAIMED** — awaiting Adversary
**WHAT:** `tokenize("42")``[Token(NUMBER, 42), Token(EOF, None)]`; floats `3.14`, `.5`, `10.` each produce one NUMBER token with numeric value (int or float).
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; t=tokenize('42'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('3.14'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('.5'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('10.'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
```
**EXPECTED:**
```
NUMBER 42 int
NUMBER 3.14 float
NUMBER 0.5 float
NUMBER 10.0 float
```
**WHERE:** `calc/lexer.py``tokenize()` function
---
### D2 — operators & parens: **CLAIMED** — awaiting Adversary
**WHAT:** `+ - * / ( )` tokenize to `PLUS MINUS STAR SLASH LPAREN RPAREN`; `tokenize("1+2*3")``NUMBER PLUS NUMBER STAR NUMBER EOF`
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('+-*/()')] )"
```
**EXPECTED:**
```
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
['PLUS', 'MINUS', 'STAR', 'SLASH', 'LPAREN', 'RPAREN', 'EOF']
```
**WHERE:** `calc/lexer.py``_SINGLE` dict and `tokenize()`
---
### D3 — whitespace & errors: **CLAIMED** — awaiting Adversary
**WHAT:** Spaces/tabs skipped; invalid chars raise `LexError` with offending char and position.
**HOW to verify:**
```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')"
```
**EXPECTED:**
- First command: `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
- Second command: raises `calc.lexer.LexError: unexpected character '@' at position 2`
**WHERE:** `calc/lexer.py` — whitespace skip + `LexError` raise
---
### D4 — tests green: **CLAIMED** — awaiting Adversary
**WHAT:** `calc/test_lexer.py` runs 24 tests, 0 failures under `python -m unittest`
**HOW to verify:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
----------------------------------------------------------------------
Ran 24 tests in 0.000s
OK
```
**WHERE:** `calc/test_lexer.py`
---

View File

@ -0,0 +1,159 @@
# STATUS — parse phase (Builder)
## DONE
All DoD gates (D1D6) verified PASS by Adversary @2026-06-15T06:13:00Z. No veto. Phase complete.
## Current state
Implementation complete. `calc/parser.py` and `calc/test_parser.py` created. 48 tests pass (24 lex + 24 parser). All gates claimed and Adversary-verified.
## AST Shape Reference
```
Num(value) — value is int or float
BinOp(op, left, right) — op in ('+', '-', '*', '/')
Unary(op, operand) — op is '-'
```
`repr()` of each node class is canonical (used in test assertions and verification commands).
---
## Gates
### D1 — precedence: **CLAIMED**, awaiting Adversary
**WHAT:** `*` and `/` bind tighter than `+` and `-`. `1+2*3``BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` (not `BinOp('*', BinOp('+', ...), ...)`).
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('2*3+1')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('5-2*3')))"
```
**EXPECTED:**
```
BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
BinOp('-', Num(5), BinOp('*', Num(2), Num(3)))
```
**WHERE:** `calc/parser.py``_expr()` calls `_term()` which handles `*`/`/`; `_expr()` handles `+`/`-` at lower priority.
---
### D2 — left associativity: **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 to verify:**
```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('-', BinOp('-', Num(8), Num(3)), Num(2))
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
```
**WHERE:** `calc/parser.py``_expr()` and `_term()` use `while` loops, wrapping the accumulating left side.
---
### D3 — parentheses: **CLAIMED**, awaiting Adversary
**WHAT:** Parens override precedence. `(1+2)*3``BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`.
**HOW to verify:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1*(2+3)')))"
```
**EXPECTED:**
```
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
BinOp('*', Num(1), BinOp('+', Num(2), Num(3)))
```
**WHERE:** `calc/parser.py``_primary()` handles `LPAREN … RPAREN` by calling `_expr()` recursively.
---
### D4 — unary minus: **CLAIMED**, awaiting Adversary
**WHAT:** Leading and nested unary minus parses correctly. `-5``Unary('-', Num(5))`. `-(1+2)``Unary('-', BinOp('+', Num(1), Num(2)))`. `3 * -2``BinOp('*', Num(3), Unary('-', Num(2)))`.
**HOW to verify:**
```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('-', Num(5))
Unary('-', BinOp('+', Num(1), Num(2)))
BinOp('*', Num(3), Unary('-', Num(2)))
```
**WHERE:** `calc/parser.py``_factor()` detects `MINUS` and recurses, returning `Unary('-', operand)`.
---
### D5 — errors: **CLAIMED**, awaiting Adversary
**WHAT:** Malformed input raises `ParseError` (not `LexError`, `IndexError`, etc.). Five required cases: `"1 +"`, `"(1"`, `"1 2"`, `")("`, `""`.
**HOW to verify:**
```bash
python -c "
from calc.lexer import tokenize
from calc.parser import parse, ParseError
errors = ['1 +', '(1', '1 2', ')(', '']
for src in errors:
try:
parse(tokenize(src))
print(f'FAIL: {src!r} did not raise')
except ParseError as e:
print(f'OK: {src!r} -> ParseError: {e}')
except Exception as e:
print(f'FAIL: {src!r} raised wrong exception: {type(e).__name__}: {e}')
"
```
**EXPECTED:**
```
OK: '1 +' -> ParseError: unexpected token 'EOF'
OK: '(1' -> ParseError: unclosed parenthesis, got 'EOF'
OK: '1 2' -> ParseError: unexpected token 'NUMBER'
OK: ')(' -> ParseError: unexpected token 'RPAREN'
OK: '' -> ParseError: empty input
```
**WHERE:** `calc/parser.py``parse()`, `_primary()`, `_primary()` RPAREN check.
---
### D6 — tests green: **CLAIMED**, awaiting Adversary
**WHAT:** `calc/test_parser.py` + `calc/test_lexer.py` combined: 48 tests, 0 failures under `python -m unittest`.
**HOW to verify:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
----------------------------------------------------------------------
Ran 48 tests in 0.002s
OK
```
**WHERE:** `calc/test_parser.py` (24 tests covering D1D5), `calc/test_lexer.py` (24 tests from lex phase).