artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
3
calculators/builder-adversary-lean/run-01/.gitignore
vendored
Normal file
3
calculators/builder-adversary-lean/run-01/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
26
calculators/builder-adversary-lean/run-01/GIT-LOG.txt
Normal file
26
calculators/builder-adversary-lean/run-01/GIT-LOG.txt
Normal file
@ -0,0 +1,26 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
7ed6fe1 status(eval): ## DONE — all D1-D5 gates Adversary-verified PASS
|
||||
cb4c0ea review(D2,D3,D4,D5): PASS — all eval gates verified cold; no findings
|
||||
17dc187 review(D1): PASS — all 5 plan cases + 6 adversarial probes correct
|
||||
7312798 claim(D5): tests green — 46/46 pass (15 lexer + 20 parser + 11 evaluator), 0 failures, no regressions
|
||||
cf32e60 claim(D4): CLI — 2+3*4→14 exit:0; 1/0 and 1+ → stderr error, exit:1; no traceback
|
||||
b2c0bca claim(D3): result type — 4/2→int(2), 7/2→float(3.5), pure-int stays int
|
||||
0c86faf claim(D2): division — 7/2=3.5, 1/0 raises EvalError (not ZeroDivisionError)
|
||||
48091db claim(D1): arithmetic — 2+3*4=14, (2+3)*4=20, 8-3-2=3, -2+5=3, 2*-3=-6 verified
|
||||
0fc263d feat(eval): evaluator, CLI, and test suite — D1-D5 implementation
|
||||
75f2228 review(eval): Adversary initialized for eval phase — watching for Builder gate claims
|
||||
2986d49 status(parse): ## DONE — all D1-D6 gates Adversary-verified PASS
|
||||
0a94c02 review(D1,D2,D3,D4,D5,D6): PASS — all parse gates verified cold; no findings
|
||||
2f23906 claim(D6): tests green — 35/35 pass, 0 failures, full D1-D5 coverage
|
||||
4b60673 claim(D5): errors — all 5 malformed cases raise ParseError, verified
|
||||
1614e3e claim(D4): unary minus — -5, -(1+2), 3*-2, --5 all parse correctly, verified
|
||||
d71a86c claim(D3): parentheses override precedence — (1+2)*3 structure verified
|
||||
d01619b claim(D2): left associativity — 8-3-2 and 8/4/2 parse left-to-right, verified
|
||||
72fadd5 claim(D1): precedence — * / bind tighter than + -, verified
|
||||
64d0252 feat(parse): recursive-descent parser, AST nodes, ParseError, and test suite
|
||||
1acbfd7 review(init-parse): Adversary initialized for parse phase — watching for Builder gate claims
|
||||
c667cf2 fix(AF-1): wrap bare-dot ValueError as LexError; write DONE to STATUS
|
||||
1708047 review(D1,D2,D3,D4): PASS — all gates verified cold; AF-1 non-blocking finding logged
|
||||
a27b62d claim(D1,D2,D3,D4): lexer impl + tests — all gates ready for Adversary verify
|
||||
6daf66e review(init): Adversary initialized — watching for Builder gate claims
|
||||
ddbf636 chore: seed
|
||||
1
calculators/builder-adversary-lean/run-01/README.md
Normal file
1
calculators/builder-adversary-lean/run-01/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary-lean/run-01/SOURCE.txt
Normal file
1
calculators/builder-adversary-lean/run-01/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-9awZvZ/builder-adversary-lean/r1
|
||||
23
calculators/builder-adversary-lean/run-01/calc.py
Normal file
23
calculators/builder-adversary-lean/run-01/calc.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/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(f"usage: {sys.argv[0]} <expression>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
expr = sys.argv[1]
|
||||
try:
|
||||
result = evaluate(parse(tokenize(expr)))
|
||||
print(result)
|
||||
except (LexError, ParseError, EvalError) as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
38
calculators/builder-adversary-lean/run-01/calc/evaluator.py
Normal file
38
calculators/builder-adversary-lean/run-01/calc/evaluator.py
Normal file
@ -0,0 +1,38 @@
|
||||
from calc.parser import Num, BinOp, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(node) -> 'int | float':
|
||||
"""Walk an AST node and return the numeric result.
|
||||
|
||||
Result type rule: returns int for whole-valued results (including whole-valued
|
||||
division), float for non-whole. Division by zero raises EvalError.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, Unary):
|
||||
v = evaluate(node.operand)
|
||||
if node.op == '-':
|
||||
return -v
|
||||
raise EvalError(f"unknown unary op {node.op!r}")
|
||||
if isinstance(node, BinOp):
|
||||
left = evaluate(node.left)
|
||||
right = evaluate(node.right)
|
||||
if node.op == '+':
|
||||
return left + right
|
||||
if node.op == '-':
|
||||
return left - right
|
||||
if node.op == '*':
|
||||
return left * right
|
||||
if node.op == '/':
|
||||
if right == 0:
|
||||
raise EvalError("division by zero")
|
||||
result = left / right
|
||||
if isinstance(result, float) and result.is_integer():
|
||||
return int(result)
|
||||
return result
|
||||
raise EvalError(f"unknown binary op {node.op!r}")
|
||||
raise EvalError(f"unknown node type {type(node).__name__!r}")
|
||||
47
calculators/builder-adversary-lean/run-01/calc/lexer.py
Normal file
47
calculators/builder-adversary-lean/run-01/calc/lexer.py
Normal file
@ -0,0 +1,47 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: Union[int, float, str]
|
||||
|
||||
|
||||
_SINGLE = {'+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH',
|
||||
'(': 'LPAREN', ')': 'RPAREN'}
|
||||
|
||||
|
||||
def tokenize(src: str) -> list:
|
||||
tokens = []
|
||||
i = 0
|
||||
n = len(src)
|
||||
while i < n:
|
||||
ch = src[i]
|
||||
if ch in ' \t\n\r':
|
||||
i += 1
|
||||
elif ch in _SINGLE:
|
||||
tokens.append(Token(_SINGLE[ch], ch))
|
||||
i += 1
|
||||
elif ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
has_dot = False
|
||||
while j < n and (src[j].isdigit() or (src[j] == '.' and not has_dot)):
|
||||
if src[j] == '.':
|
||||
has_dot = True
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
try:
|
||||
value = float(raw) if has_dot else int(raw)
|
||||
except ValueError:
|
||||
raise LexError(f"invalid number {raw!r} at position {i}")
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
else:
|
||||
raise LexError(f"unexpected character {ch!r} at position {i}")
|
||||
tokens.append(Token('EOF', ''))
|
||||
return tokens
|
||||
114
calculators/builder-adversary-lean/run-01/calc/parser.py
Normal file
114
calculators/builder-adversary-lean/run-01/calc/parser.py
Normal file
@ -0,0 +1,114 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
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):
|
||||
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 expression")
|
||||
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
|
||||
operand = self._unary()
|
||||
return Unary(op, operand)
|
||||
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 ')' but got {self._peek().kind!r} ({self._peek().value!r})"
|
||||
)
|
||||
self._consume()
|
||||
return node
|
||||
if tok.kind == 'EOF':
|
||||
raise ParseError("unexpected end of expression")
|
||||
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
|
||||
|
||||
|
||||
def parse(tokens: list) -> Node:
|
||||
"""Parse a token list from calc.lexer.tokenize() into an AST.
|
||||
|
||||
Returns one of: Num(value), BinOp(op, left, right), Unary(op, operand).
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
@ -0,0 +1,60 @@
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def ev(expr):
|
||||
return evaluate(parse(tokenize(expr)))
|
||||
|
||||
|
||||
class TestArithmetic(unittest.TestCase):
|
||||
def test_add_mul_precedence(self):
|
||||
self.assertEqual(ev("2+3*4"), 14)
|
||||
|
||||
def test_paren_override(self):
|
||||
self.assertEqual(ev("(2+3)*4"), 20)
|
||||
|
||||
def test_sub_left_assoc(self):
|
||||
self.assertEqual(ev("8-3-2"), 3)
|
||||
|
||||
def test_unary_leading(self):
|
||||
self.assertEqual(ev("-2+5"), 3)
|
||||
|
||||
def test_unary_in_mul(self):
|
||||
self.assertEqual(ev("2*-3"), -6)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
def test_true_division_non_whole(self):
|
||||
self.assertEqual(ev("7/2"), 3.5)
|
||||
|
||||
def test_div_by_zero_literal(self):
|
||||
with self.assertRaises(EvalError):
|
||||
ev("1/0")
|
||||
|
||||
def test_div_by_zero_expr(self):
|
||||
with self.assertRaises(EvalError):
|
||||
ev("5/(2-2)")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
def test_whole_div_returns_int(self):
|
||||
result = ev("4/2")
|
||||
self.assertEqual(result, 2)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_non_whole_div_returns_float(self):
|
||||
result = ev("7/2")
|
||||
self.assertEqual(result, 3.5)
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_pure_int_arithmetic_returns_int(self):
|
||||
result = ev("2+3*4")
|
||||
self.assertEqual(result, 14)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
101
calculators/builder-adversary-lean/run-01/calc/test_lexer.py
Normal file
101
calculators/builder-adversary-lean/run-01/calc/test_lexer.py
Normal 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):
|
||||
toks = tokenize("42")
|
||||
self.assertEqual(len(toks), 2)
|
||||
self.assertEqual(toks[0], Token('NUMBER', 42))
|
||||
self.assertIsInstance(toks[0].value, int)
|
||||
self.assertEqual(toks[1].kind, 'EOF')
|
||||
|
||||
def test_float(self):
|
||||
toks = tokenize("3.14")
|
||||
self.assertEqual(toks[0], Token('NUMBER', 3.14))
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_leading_dot(self):
|
||||
toks = tokenize(".5")
|
||||
self.assertAlmostEqual(toks[0].value, 0.5)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_trailing_dot(self):
|
||||
toks = tokenize("10.")
|
||||
self.assertAlmostEqual(toks[0].value, 10.0)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_zero(self):
|
||||
toks = tokenize("0")
|
||||
self.assertEqual(toks[0].value, 0)
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
def test_simple_expr(self):
|
||||
k = kinds("1+2*3")
|
||||
self.assertEqual(k, ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_all_ops(self):
|
||||
k = kinds("1-2/3")
|
||||
self.assertEqual(k, ['NUMBER', 'MINUS', 'NUMBER', 'SLASH', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_parens(self):
|
||||
k = kinds("(1)")
|
||||
self.assertEqual(k, ['LPAREN', 'NUMBER', 'RPAREN', 'EOF'])
|
||||
|
||||
def test_complex(self):
|
||||
result = values("3.5*(1-2)")
|
||||
self.assertEqual(result, [
|
||||
('NUMBER', 3.5),
|
||||
('STAR', '*'),
|
||||
('LPAREN', '('),
|
||||
('NUMBER', 1),
|
||||
('MINUS', '-'),
|
||||
('NUMBER', 2),
|
||||
('RPAREN', ')'),
|
||||
('EOF', ''),
|
||||
])
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
def test_spaces_skipped(self):
|
||||
k = kinds(" 12 + 3 ")
|
||||
self.assertEqual(k, ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
toks = tokenize(" 12 + 3 ")
|
||||
self.assertEqual(toks[0].value, 12)
|
||||
self.assertEqual(toks[2].value, 3)
|
||||
|
||||
def test_tab_skipped(self):
|
||||
k = kinds("1\t+\t2")
|
||||
self.assertEqual(k, ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_invalid_at(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_invalid_dollar(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("$")
|
||||
self.assertIn('$', str(ctx.exception))
|
||||
|
||||
def test_invalid_letter(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("abc")
|
||||
|
||||
def test_error_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('2', str(ctx.exception)) # position 2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
110
calculators/builder-adversary-lean/run-01/calc/test_parser.py
Normal file
110
calculators/builder-adversary-lean/run-01/calc/test_parser.py
Normal file
@ -0,0 +1,110 @@
|
||||
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):
|
||||
"""D1 — * and / bind tighter than + and -"""
|
||||
|
||||
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+4 => BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
self.assertEqual(p('2*3+4'), BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
def test_sub_mul(self):
|
||||
# 6-2*3 => BinOp('-', Num(6), BinOp('*', Num(2), Num(3)))
|
||||
self.assertEqual(p('6-2*3'), BinOp('-', Num(6), BinOp('*', Num(2), Num(3))))
|
||||
|
||||
def test_div_before_add(self):
|
||||
# 1+6/2 => BinOp('+', Num(1), BinOp('/', Num(6), Num(2)))
|
||||
self.assertEqual(p('1+6/2'), BinOp('+', Num(1), BinOp('/', Num(6), Num(2))))
|
||||
|
||||
|
||||
class TestAssociativity(unittest.TestCase):
|
||||
"""D2 — same-precedence operators associate left"""
|
||||
|
||||
def test_sub_left_assoc(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_div_left_assoc(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_add_left_assoc(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_mul_left_assoc(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):
|
||||
"""D3 — parentheses override precedence"""
|
||||
|
||||
def test_parens_override(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):
|
||||
# ((2+3)) => BinOp('+', Num(2), Num(3))
|
||||
self.assertEqual(p('((2+3))'), BinOp('+', Num(2), Num(3)))
|
||||
|
||||
def test_parens_left_of_mul(self):
|
||||
# 3*(2+1) => BinOp('*', Num(3), BinOp('+', Num(2), Num(1)))
|
||||
self.assertEqual(p('3*(2+1)'), BinOp('*', Num(3), BinOp('+', Num(2), Num(1))))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
"""D4 — unary minus"""
|
||||
|
||||
def test_simple_neg(self):
|
||||
self.assertEqual(p('-5'), Unary('-', Num(5)))
|
||||
|
||||
def test_neg_paren(self):
|
||||
# -(1+2) => Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
self.assertEqual(p('-(1+2)'), Unary('-', BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_mul_neg(self):
|
||||
# 3 * -2 => BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
self.assertEqual(p('3 * -2'), BinOp('*', Num(3), Unary('-', Num(2))))
|
||||
|
||||
def test_double_neg(self):
|
||||
# --5 => Unary('-', Unary('-', Num(5)))
|
||||
self.assertEqual(p('--5'), Unary('-', Unary('-', Num(5))))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
"""D5 — ParseError on malformed input"""
|
||||
|
||||
def test_trailing_op(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('1 +')
|
||||
|
||||
def test_unclosed_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('(1')
|
||||
|
||||
def test_two_nums(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()
|
||||
@ -0,0 +1,9 @@
|
||||
# BACKLOG — Phase eval (Builder)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 — arithmetic: CLAIMED
|
||||
- [ ] D2 — division: pending Adversary PASS on D1
|
||||
- [ ] D3 — result type: pending Adversary PASS on D1
|
||||
- [ ] D4 — CLI: pending Adversary PASS on D1
|
||||
- [ ] D5 — tests green: pending Adversary PASSes on D1-D4
|
||||
@ -0,0 +1,19 @@
|
||||
# BACKLOG — Phase lex
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/ package with __init__.py
|
||||
- [x] Implement calc/lexer.py: Token dataclass, LexError, tokenize()
|
||||
- [x] Implement calc/test_lexer.py: unittest 15 tests covering D1-D3
|
||||
- [x] Run tests — 15/15 PASS
|
||||
- [ ] Claim D1 (numbers) → await PASS
|
||||
- [ ] Claim D2 (operators & parens) → await PASS
|
||||
- [ ] Claim D3 (whitespace & errors) → await PASS
|
||||
- [ ] Claim D4 (tests green) → await PASS
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### AF-1 (non-blocking): bare `.` leaks `ValueError` instead of `LexError`
|
||||
- Repro: `tokenize('.')` → `ValueError: could not convert string to float: '.'`
|
||||
- Expected: `LexError` (or any error that doesn't expose Python internals)
|
||||
- Status: OPEN, non-blocking — not required by DoD, but may bite future phases
|
||||
@ -0,0 +1,18 @@
|
||||
# BACKLOG — Phase parse (Builder)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Read phase plan
|
||||
- [ ] Implement calc/parser.py (Num, BinOp, Unary, parse(), ParseError)
|
||||
- [ ] Implement calc/test_parser.py
|
||||
- [ ] Run tests locally — confirm green
|
||||
- [ ] Claim D1 (precedence)
|
||||
- [ ] Claim D2 (left associativity)
|
||||
- [ ] Claim D3 (parentheses)
|
||||
- [ ] Claim D4 (unary minus)
|
||||
- [ ] Claim D5 (errors)
|
||||
- [ ] Claim D6 (tests green)
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_None yet._
|
||||
@ -0,0 +1,3 @@
|
||||
# DECISIONS — Phase lex (shared, append-only)
|
||||
|
||||
_No decisions recorded yet._
|
||||
@ -0,0 +1,27 @@
|
||||
# JOURNAL — Phase eval (Builder)
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Built evaluator, CLI, and test suite in one pass.
|
||||
|
||||
AST walk in `calc/evaluator.py`:
|
||||
- `Num`: return `node.value` (int or float as stored by lexer)
|
||||
- `Unary('-')`: negate result of recursive evaluate
|
||||
- `BinOp('+'/'-'/'*')`: straightforward arithmetic on evaluated children
|
||||
- `BinOp('/')`: true division; guard `right == 0` → `EvalError`; if result is float and whole, return `int(result)` (D3 rule)
|
||||
|
||||
D3 result type rule: `evaluate()` returns `int` for whole-valued results, `float` for non-whole. This means `4/2` → `int(2)` (prints `2`), `7/2` → `float(3.5)` (prints `3.5`). Pure integer arithmetic stays int throughout (Python int + int = int).
|
||||
|
||||
CLI `calc.py`: catches `LexError`, `ParseError`, `EvalError` → stderr + exit 1. No traceback exposed.
|
||||
|
||||
Test run: 46 tests pass (15 lexer + 20 parser + 11 evaluator). No regressions.
|
||||
|
||||
Verified all plan cases:
|
||||
```
|
||||
python calc.py "2+3*4" → 14
|
||||
python calc.py "(2+3)*4" → 20
|
||||
python calc.py "7/2" → 3.5
|
||||
python calc.py "4/2" → 2
|
||||
python calc.py "1/0" → error to stderr, exit 1
|
||||
python calc.py "1 +" → error to stderr, exit 1
|
||||
```
|
||||
@ -0,0 +1,21 @@
|
||||
# JOURNAL — Phase lex (Adversary)
|
||||
|
||||
## 2026-06-15T06:23:05Z — Adversary initialized
|
||||
|
||||
- Read phase plan from /home/loops/project-orchestrator/projects/agent-orchestrator-benchmark/plans/calc/lex.md
|
||||
- DoD gates: D1 (numbers), D2 (operators & parens), D3 (whitespace & errors), D4 (tests green)
|
||||
- Builder has not yet created STATUS-lex.md or any code
|
||||
- Initialized REVIEW-lex.md and BACKLOG-lex.md
|
||||
- Going idle; will check again in 10 min
|
||||
|
||||
## 2026-06-15T06:30:00Z — Builder: initial implementation complete
|
||||
|
||||
- Created calc/__init__.py, calc/lexer.py, calc/test_lexer.py
|
||||
- Token is a dataclass with kind:str and value:int|float|str
|
||||
- LexError is a plain Exception subclass
|
||||
- tokenize() loops over src with index, handles: whitespace skip, NUMBER (int+float incl. leading/trailing dot), operators/parens via dict, raises LexError for unknowns
|
||||
- 15 unittest tests, all PASS: `Ran 15 tests in 0.000s OK`
|
||||
- Plan verify commands output:
|
||||
- `tokenize('3.5*(1-2)')` → `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', '')]`
|
||||
- `tokenize('1 @ 2')` → `LexError: unexpected character '@' at position 2`
|
||||
- Claiming D1, D2, D3, D4 in one commit
|
||||
@ -0,0 +1,56 @@
|
||||
# JOURNAL — Phase parse (Builder)
|
||||
|
||||
## 2026-06-15 — Start
|
||||
|
||||
Reading phase plan. Lex phase is DONE. Building parser on top of `calc/lexer.py`.
|
||||
|
||||
### Design
|
||||
|
||||
Grammar (recursive descent):
|
||||
```
|
||||
expr → term (('+' | '-') term)*
|
||||
term → unary (('*' | '/') unary)*
|
||||
unary → '-' unary | primary
|
||||
primary → NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
- `expr`/`term` loop = left-associative by construction
|
||||
- `term` deeper than `expr` = `*`/`/` higher precedence than `+`/`-`
|
||||
- `unary` recursive = right-associative unary (`--5` parses as `-(-5)`)
|
||||
|
||||
AST nodes:
|
||||
- `Num(value)` — leaf
|
||||
- `BinOp(op, left, right)` — binary operator
|
||||
- `Unary(op, operand)` — unary operator
|
||||
|
||||
ParseError raised for: empty string, trailing operator, unclosed paren, consecutive numbers, mismatched parens.
|
||||
|
||||
## 2026-06-15 — Implementation + tests run
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 35 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
(15 from test_lexer + 20 from test_parser)
|
||||
|
||||
Plan verify commands:
|
||||
```
|
||||
$ python -c "...parse(tokenize('1+2*3'))"
|
||||
BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) # D1 ✓
|
||||
|
||||
$ python -c "...parse(tokenize('(1+2)*3'))"
|
||||
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) # D3 ✓
|
||||
|
||||
$ python -c "...parse(tokenize('8-3-2'))"
|
||||
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) # D2 ✓
|
||||
|
||||
$ python -c "...parse(tokenize('-5'))"
|
||||
Unary('-', Num(5)) # D4 ✓
|
||||
|
||||
$ python -c "...parse(tokenize('3 * -2'))"
|
||||
BinOp('*', Num(3), Unary('-', Num(2))) # D4 ✓
|
||||
|
||||
$ python -c "...parse(tokenize('1 +'))"
|
||||
calc.parser.ParseError: unexpected end of expression # D5 ✓
|
||||
```
|
||||
@ -0,0 +1,37 @@
|
||||
# REVIEW — Phase eval (Adversary)
|
||||
|
||||
## Status
|
||||
All D1–D5 verified PASS. Awaiting Builder DONE declaration.
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
### eval/D1: PASS @2026-06-15T06:37Z
|
||||
|
||||
Cold run of all 5 plan cases: 2+3*4→14, (2+3)*4→20, 8-3-2→3, -2+5→3, 2*-3→-6 all correct.
|
||||
Adversarial extras (1+2+3→6, 10-2*3→4, 6/2→3, 2*3+4*5→26, -(3)→-3, -(-5)→5) all correct.
|
||||
Implementation in calc/evaluator.py (commit 0fc263d) handles Num, BinOp, Unary nodes correctly.
|
||||
|
||||
### eval/D2: PASS @2026-06-15T06:43Z
|
||||
|
||||
7/2→3.5 confirmed. 1/0 raises EvalError (not ZeroDivisionError). ZeroDivisionError does not escape.
|
||||
Adversarial: 5/(2-2) and 3/(1-1) both raise EvalError; 9/3→3 (int), 10/4→2.5 (float).
|
||||
|
||||
### eval/D3: PASS @2026-06-15T06:43Z
|
||||
|
||||
4/2→2 (int, no .0), 7/2→3.5 (float), 2+3*4→14 (int). print() output matches DoD spec exactly.
|
||||
Extra: 9/3→3, 10/4→2.5. Whole-valued division returns int; non-whole returns float.
|
||||
|
||||
### eval/D4: PASS @2026-06-15T06:43Z
|
||||
|
||||
`python calc.py "2+3*4"` → 14 exit:0; (2+3)*4 → 20; 7/2 → 3.5; 4/2 → 2; all exit:0.
|
||||
1/0 → stderr "error: division by zero", exit:1. "1 +" → stderr error, exit:1. No traceback.
|
||||
Adversarial: wrong arg count exits 1 with usage message; lex error (abc) exits 1 with error.
|
||||
|
||||
### eval/D5: PASS @2026-06-15T06:43Z
|
||||
|
||||
`python -m unittest -q` → Ran 46 tests in 0.001s — OK. 0 failures, no regressions.
|
||||
15 lexer + 20 parser + 11 evaluator tests all pass (commit 0fc263d).
|
||||
|
||||
## Adversary findings
|
||||
|
||||
<!-- Defects will be filed here -->
|
||||
@ -0,0 +1,73 @@
|
||||
# REVIEW — Phase lex (Adversary)
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1: PASS @2026-06-15T06:26:41Z
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
NUMBER 42 int ✓ (integer, value==42, isinstance int)
|
||||
NUMBER 3.14 float ✓
|
||||
NUMBER 0.5 float ✓ (leading dot)
|
||||
NUMBER 10.0 float ✓ (trailing dot)
|
||||
```
|
||||
Structure check: `tokenize("42")` → 2 tokens, `[NUMBER(42), EOF]`. Exact match.
|
||||
|
||||
Break-it probes: bare `.` raises `ValueError` (not `LexError`) — see Adversary finding AF-1 below. Not a DoD blocker (bare dot not in spec), logged as defect.
|
||||
|
||||
### D2: PASS @2026-06-15T06:26:41Z
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
tokenize("1+2*3") → ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓
|
||||
tokenize("+-*/()") → ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'] ✓
|
||||
```
|
||||
All 6 operator/paren kinds verified.
|
||||
|
||||
### D3: PASS @2026-06-15T06:26:41Z
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
tokenize(" 12 + 3 ") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓
|
||||
tokenize("1\t+\t2") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓
|
||||
tokenize("1 @ 2") → calc.lexer.LexError: unexpected character '@' at position 2 ✓
|
||||
tokenize("abc") → calc.lexer.LexError: unexpected character 'a' at position 0 ✓
|
||||
tokenize("$") → calc.lexer.LexError: unexpected character '$' at position 0 ✓
|
||||
```
|
||||
LexError message contains offending char and its position.
|
||||
|
||||
### D4: PASS @2026-06-15T06:26:41Z
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
Ran 15 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
Plan's exact commands:
|
||||
```
|
||||
tokenize('3.5*(1-2)') → [('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', '')] ✓
|
||||
tokenize('1 @ 2') → calc.lexer.LexError: unexpected character '@' at position 2 ✓
|
||||
```
|
||||
Tests cover D1–D3 including all plan-required cases: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"`.
|
||||
|
||||
---
|
||||
|
||||
## Adversary Findings
|
||||
|
||||
### AF-1 (non-blocking): bare `.` leaks `ValueError` instead of `LexError`
|
||||
|
||||
**Repro:** `python -c "from calc.lexer import tokenize; tokenize('.')"`
|
||||
|
||||
**Actual:** `ValueError: could not convert string to float: '.'`
|
||||
|
||||
**Expected:** `LexError` (or at minimum, not a raw `ValueError` from Python internals)
|
||||
|
||||
**Impact:** The DoD does not list bare dot as a required error case. Not blocking DONE, but future parser phases may hit this if they ever pass a stray `.` to the lexer. Recommend wrapping in a try/except and re-raising as LexError.
|
||||
|
||||
**Status:** OPEN (non-blocking)
|
||||
|
||||
---
|
||||
|
||||
## No VETO
|
||||
|
||||
All DoD gates (D1, D2, D3, D4) verified PASS. Builder may write `## DONE` to STATUS-lex.md.
|
||||
@ -0,0 +1,113 @@
|
||||
# REVIEW — Phase parse (Adversary)
|
||||
|
||||
## Status
|
||||
All gates D1–D6 verified PASS @2026-06-15T06:32:30Z.
|
||||
|
||||
---
|
||||
|
||||
## Verdicts
|
||||
|
||||
### parse/D1: PASS @2026-06-15T06:32:30Z
|
||||
|
||||
Cold re-run of Builder's claimed commands:
|
||||
```
|
||||
BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) # 1+2*3 — * binds tighter ✓
|
||||
BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)) # 2*3+4 — * evaluated first ✓
|
||||
```
|
||||
|
||||
Independent break-it probes:
|
||||
- `1+2*3+4` → `BinOp('+', BinOp('+', Num(1), BinOp('*', Num(2), Num(3))), Num(4))` ✓
|
||||
- `4*5-6/2` → `BinOp('-', BinOp('*', Num(4), Num(5)), BinOp('/', Num(6), Num(2)))` ✓
|
||||
|
||||
Derivation confirmed: `_expr` loops over `+/-` consuming `_term` results; `_term` loops over `*//` consuming `_unary` results — two-level precedence grammar is structurally correct.
|
||||
|
||||
---
|
||||
|
||||
### parse/D2: PASS @2026-06-15T06:32:30Z
|
||||
|
||||
Cold re-run:
|
||||
```
|
||||
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) # 8-3-2 — left-assoc ✓
|
||||
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) # 8/4/2 — left-assoc ✓
|
||||
```
|
||||
|
||||
Independent break-it probe:
|
||||
- `1-2+3` → `BinOp('+', BinOp('-', Num(1), Num(2)), Num(3))` ✓ (left-assoc across mixed +/-)
|
||||
|
||||
While-loop accumulation pattern in `_expr` and `_term` correctly produces left-leaning trees.
|
||||
|
||||
---
|
||||
|
||||
### parse/D3: PASS @2026-06-15T06:32:30Z
|
||||
|
||||
Cold re-run:
|
||||
```
|
||||
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) # (1+2)*3 — + under * ✓
|
||||
BinOp('+', Num(2), Num(3)) # ((2+3)) — parens stripped ✓
|
||||
```
|
||||
|
||||
Independent break-it probe:
|
||||
- `3*(1+2)*4` → `BinOp('*', BinOp('*', Num(3), BinOp('+', Num(1), Num(2))), Num(4))` ✓
|
||||
|
||||
`_primary()` correctly enters `_expr()` recursively inside parens and consumes the closing RPAREN.
|
||||
|
||||
---
|
||||
|
||||
### parse/D4: PASS @2026-06-15T06:32:30Z
|
||||
|
||||
Cold re-run:
|
||||
```
|
||||
Unary('-', Num(5)) # -5 ✓
|
||||
Unary('-', BinOp('+', Num(1), Num(2))) # -(1+2) ✓
|
||||
BinOp('*', Num(3), Unary('-', Num(2))) # 3 * -2 ✓
|
||||
Unary('-', Unary('-', Num(5))) # --5 ✓
|
||||
```
|
||||
|
||||
Independent break-it probes:
|
||||
- `1 - -2` → `BinOp('-', Num(1), Unary('-', Num(2)))` ✓
|
||||
- `-1 + -2` → `BinOp('+', Unary('-', Num(1)), Unary('-', Num(2)))` ✓
|
||||
- `-(-(3))` → `Unary('-', Unary('-', Num(3)))` ✓
|
||||
|
||||
`_unary()` is right-recursive (calls itself), so multiple leading negations stack correctly.
|
||||
|
||||
---
|
||||
|
||||
### parse/D5: PASS @2026-06-15T06:32:30Z
|
||||
|
||||
Cold re-run — all 5 plan-required error cases:
|
||||
```
|
||||
OK ParseError: '1 +' -> unexpected end of expression
|
||||
OK ParseError: '(1' -> expected ')' but got 'EOF' ('')
|
||||
OK ParseError: '1 2' -> unexpected token 'NUMBER' (2)
|
||||
OK ParseError: ')(' -> unexpected token 'RPAREN' (')')
|
||||
OK ParseError: '' -> empty expression
|
||||
```
|
||||
|
||||
All raise `ParseError` (not bare `ValueError`, `IndexError`, etc.) ✓
|
||||
|
||||
Independent extra probes:
|
||||
- `'*'` → `ParseError: unexpected token 'STAR' ('*')` ✓
|
||||
- `'((1)'` → `ParseError: expected ')' but got 'EOF' ('')` ✓
|
||||
- `'1++2'` → `ParseError: unexpected token 'PLUS' ('+')` ✓
|
||||
- `'+5'` → `ParseError: unexpected token 'PLUS' ('+')` ✓
|
||||
|
||||
---
|
||||
|
||||
### parse/D6: PASS @2026-06-15T06:32:30Z
|
||||
|
||||
Cold `python -m unittest -q` run:
|
||||
```
|
||||
Ran 35 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
Test file inspection confirms:
|
||||
- All 20 parser tests use `assertEqual` on dataclass instances (e.g. `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`), not on evaluated numeric results ✓
|
||||
- Coverage: D1 (4 tests), D2 (4 tests), D3 (3 tests), D4 (4 tests), D5 (5 tests) ✓
|
||||
|
||||
---
|
||||
|
||||
## Adversary findings
|
||||
|
||||
No findings. All DoD gates pass with no defects detected.
|
||||
@ -0,0 +1,167 @@
|
||||
# STATUS — Phase eval (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D5 Adversary-verified PASS @2026-06-15T06:43Z. Phase eval complete.
|
||||
|
||||
## Current State
|
||||
|
||||
Gates D1–D5 implemented, claimed, and Adversary-verified. Phase complete.
|
||||
|
||||
Implementation commit: 0fc263d
|
||||
|
||||
---
|
||||
|
||||
## Gate D1 — arithmetic — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `evaluate(parse(tokenize(s)))` is correct for `+ - * /`, precedence, parens, and unary minus.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import evaluate
|
||||
cases = [('2+3*4', 14), ('(2+3)*4', 20), ('8-3-2', 3), ('-2+5', 3), ('2*-3', -6)]
|
||||
for expr, expected in cases:
|
||||
result = evaluate(parse(tokenize(expr)))
|
||||
status = 'OK' if result == expected else f'FAIL (got {result!r})'
|
||||
print(f'{status}: {expr!r} -> {result}')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
OK: '2+3*4' -> 14
|
||||
OK: '(2+3)*4' -> 20
|
||||
OK: '8-3-2' -> 3
|
||||
OK: '-2+5' -> 3
|
||||
OK: '2*-3' -> -6
|
||||
```
|
||||
|
||||
**WHERE:** `calc/evaluator.py` — commit 0fc263d
|
||||
|
||||
---
|
||||
|
||||
## Gate D2 — division — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `/` is true division (`"7/2"` → 3.5). Division by zero raises `EvalError`, not bare `ZeroDivisionError`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
print(evaluate(parse(tokenize('7/2'))))
|
||||
|
||||
try:
|
||||
evaluate(parse(tokenize('1/0')))
|
||||
print('FAIL: no exception')
|
||||
except EvalError as e:
|
||||
print(f'OK EvalError: {e}')
|
||||
except ZeroDivisionError:
|
||||
print('FAIL: bare ZeroDivisionError escaped')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
3.5
|
||||
OK EvalError: division by zero
|
||||
```
|
||||
|
||||
**WHERE:** `calc/evaluator.py` `evaluate()` BinOp '/' branch — commit 0fc263d
|
||||
|
||||
---
|
||||
|
||||
## Gate D3 — result type — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Whole-valued results return `int` (no trailing `.0`); non-whole return `float`.
|
||||
|
||||
Rule: after division, if `result.is_integer()`, return `int(result)`. Integer arithmetic stays `int` natively.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import evaluate
|
||||
|
||||
r1 = evaluate(parse(tokenize('4/2')))
|
||||
r2 = evaluate(parse(tokenize('7/2')))
|
||||
r3 = evaluate(parse(tokenize('2+3*4')))
|
||||
print(repr(r1), type(r1).__name__)
|
||||
print(repr(r2), type(r2).__name__)
|
||||
print(repr(r3), type(r3).__name__)
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
2 int
|
||||
3.5 float
|
||||
14 int
|
||||
```
|
||||
|
||||
**WHERE:** `calc/evaluator.py` `evaluate()` BinOp '/' branch — commit 0fc263d
|
||||
|
||||
---
|
||||
|
||||
## Gate D4 — CLI — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `python calc.py "2+3*4"` prints `14` and exits 0; invalid input prints error to stderr and exits non-zero (no traceback).
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python calc.py "2+3*4"; echo "exit:$?"
|
||||
python calc.py "(2+3)*4"; echo "exit:$?"
|
||||
python calc.py "7/2"; echo "exit:$?"
|
||||
python calc.py "4/2"; echo "exit:$?"
|
||||
python calc.py "1/0" 2>&1; echo "exit:$?"
|
||||
python calc.py "1 +" 2>&1; echo "exit:$?"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
14
|
||||
exit:0
|
||||
20
|
||||
exit:0
|
||||
3.5
|
||||
exit:0
|
||||
2
|
||||
exit:0
|
||||
error: division by zero
|
||||
exit:1
|
||||
error: unexpected end of expression
|
||||
exit:1
|
||||
```
|
||||
|
||||
**WHERE:** `calc.py` — commit 0fc263d
|
||||
|
||||
---
|
||||
|
||||
## Gate D5 — tests green + end-to-end — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Full unittest suite passes with 0 failures: 15 lexer + 20 parser + 11 evaluator = 46 tests.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Ran 46 tests in 0.00Xs
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
Test coverage in `calc/test_evaluator.py`:
|
||||
- `TestArithmetic` (5 tests): D1 — 2+3*4, (2+3)*4, 8-3-2, -2+5, 2*-3
|
||||
- `TestDivision` (3 tests): D2 — 7/2, 1/0, 5/(2-2)
|
||||
- `TestResultType` (3 tests): D3 — 4/2→int, 7/2→float, 2+3*4→int
|
||||
|
||||
**WHERE:** `calc/test_evaluator.py`, `calc/evaluator.py` — commit 0fc263d
|
||||
@ -0,0 +1,103 @@
|
||||
# STATUS — Phase lex (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1, D2, D3, D4 verified PASS by Adversary @2026-06-15T06:26:41Z.
|
||||
AF-1 fixed: bare `.` now raises `LexError` instead of leaking `ValueError`.
|
||||
|
||||
## Current State
|
||||
|
||||
All implementation complete. All gates PASSED.
|
||||
|
||||
---
|
||||
|
||||
## Gate D1 — numbers — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `calc/lexer.py` tokenizes integers and floats to NUMBER tokens with numeric values.
|
||||
|
||||
**HOW:** Run from repo root (where calc/ lives):
|
||||
```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
|
||||
```
|
||||
(Each followed by EOF as second token.)
|
||||
|
||||
**WHERE:** `calc/lexer.py` — commit to be pushed with this status.
|
||||
|
||||
---
|
||||
|
||||
## Gate D2 — operators & parens — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `+ - * / ( )` each tokenize to PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN.
|
||||
|
||||
**HOW:**
|
||||
```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']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gate D3 — whitespace & errors — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Spaces/tabs are skipped; invalid chars raise LexError with char and position.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize(' 12 + 3 ')])"
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'EOF']
|
||||
calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gate D4 — tests green — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `calc/test_lexer.py` passes with 0 failures.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Ran 15 tests in 0.00Xs
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
Also the plan's exact verification commands:
|
||||
```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', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', '')]
|
||||
```
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
```
|
||||
**Expected:** raises `calc.lexer.LexError: unexpected character '@' at position 2`
|
||||
|
||||
**WHERE:** `calc/test_lexer.py`, `calc/lexer.py` — see commit sha after push.
|
||||
@ -0,0 +1,157 @@
|
||||
# STATUS — Phase parse (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D6 Adversary-verified PASS @2026-06-15T06:32:30Z. Phase parse complete.
|
||||
|
||||
## Current State
|
||||
|
||||
Gates D1 through D6 implemented, claimed, and Adversary-verified. Phase complete.
|
||||
|
||||
---
|
||||
|
||||
## Gate D1 — precedence — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `*` and `/` bind tighter than `+` and `-`. `1+2*3` parses as `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`, not `BinOp('+', BinOp('+',…))`.
|
||||
|
||||
**HOW:**
|
||||
```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+4')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` — commit 64d0252
|
||||
|
||||
---
|
||||
|
||||
## Gate D2 — left associativity — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Same-precedence operators associate left. `8-3-2` → `(8-3)-2`; `8/4/2` → `(8/4)/2`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` — commit 64d0252
|
||||
|
||||
---
|
||||
|
||||
## Gate D3 — parentheses — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Parens override precedence. `(1+2)*3` parses with `+` under `*`.
|
||||
|
||||
**HOW:**
|
||||
```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))')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
BinOp('+', Num(2), Num(3))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` `_primary()` method — commit 64d0252
|
||||
|
||||
---
|
||||
|
||||
## Gate D4 — unary minus — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Leading and nested unary minus parses correctly: `-5`, `-(1+2)`, `3 * -2`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('--5')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Unary('-', Num(5))
|
||||
Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
Unary('-', Unary('-', Num(5)))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` `_unary()` method — commit 64d0252
|
||||
|
||||
---
|
||||
|
||||
## Gate D5 — errors — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** Malformed input raises `ParseError` (not any other exception) for all five plan cases.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError
|
||||
|
||||
cases = ['1 +', '(1', '1 2', ')(' , '']
|
||||
for src in cases:
|
||||
try:
|
||||
parse(tokenize(src))
|
||||
print(f'FAIL: {src!r} did not raise')
|
||||
except ParseError as e:
|
||||
print(f'OK ParseError: {src!r} -> {e}')
|
||||
except Exception as e:
|
||||
print(f'FAIL wrong exc: {src!r} -> {type(e).__name__}: {e}')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
OK ParseError: '1 +' -> unexpected end of expression
|
||||
OK ParseError: '(1' -> expected ')' but got 'EOF' ('')
|
||||
OK ParseError: '1 2' -> unexpected token 'NUMBER' (2)
|
||||
OK ParseError: ')(' -> unexpected token 'RPAREN' (')')
|
||||
OK ParseError: '' -> empty expression
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` `parse()`, `_primary()`, `_expr()` — commit 64d0252
|
||||
|
||||
---
|
||||
|
||||
## Gate D6 — tests green — CLAIMED, awaiting Adversary
|
||||
|
||||
**WHAT:** `calc/test_parser.py` (unittest) passes with 0 failures, covering D1–D5. Total 35 tests pass (15 lexer + 20 parser).
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Ran 35 tests in 0.00Xs
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
Test coverage in `calc/test_parser.py`:
|
||||
- `TestPrecedence` (4 tests): D1 — `1+2*3`, `2*3+4`, `6-2*3`, `1+6/2`
|
||||
- `TestAssociativity` (4 tests): D2 — `8-3-2`, `8/4/2`, `1+2+3`, `2*3*4`
|
||||
- `TestParentheses` (3 tests): D3 — `(1+2)*3`, `((2+3))`, `3*(2+1)`
|
||||
- `TestUnaryMinus` (4 tests): D4 — `-5`, `-(1+2)`, `3 * -2`, `--5`
|
||||
- `TestErrors` (5 tests): D5 — `1 +`, `(1`, `1 2`, `)(`, `""`
|
||||
|
||||
All tests assert on tree structure via `assertEqual` on dataclass instances (not on evaluation).
|
||||
|
||||
**WHERE:** `calc/test_parser.py`, `calc/parser.py` — commit 64d0252
|
||||
Reference in New Issue
Block a user