artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
4
calculators/builder-adversary/run-01/.gitignore
vendored
Normal file
4
calculators/builder-adversary/run-01/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
15
calculators/builder-adversary/run-01/GIT-LOG.txt
Normal file
15
calculators/builder-adversary/run-01/GIT-LOG.txt
Normal file
@ -0,0 +1,15 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
a3263ed status(eval): ## DONE — all D1-D5 Adversary-verified PASS
|
||||
df84a12 review(D1-D5): PASS — all gates cold-verified, 62 tests OK, break-it probes held
|
||||
165c7cc claim(D1-D5): implement evaluator + CLI — all eval gates ready for Adversary verification
|
||||
e1dece1 review(init): Adversary initialized for phase eval — watching for Builder claims
|
||||
4146139 status(parse): ## DONE — all D1-D6 Adversary-verified PASS
|
||||
bed53d5 review(D1-D6): PASS — all gates cold-verified, 45 tests OK, break-it probes held
|
||||
7768832 claim(D1-D6): implement parser — all gates ready for Adversary verification
|
||||
1bcba29 review(init): Adversary initialized for phase parse — watching for Builder claims
|
||||
026f551 status(lex): ## DONE — all D1-D4 Adversary-verified PASS
|
||||
2f4fd6b review(D1-D4): PASS — all gates cold-verified, 21 tests OK, break-it probes held
|
||||
071871f chore: add .gitignore, remove __pycache__ from tracking
|
||||
531a8cf claim(D1-D4): implement lexer — all gates ready for Adversary verification
|
||||
5826b69 review(init): Adversary initialized for phase lex — watching for Builder claims
|
||||
13c0db5 chore: seed
|
||||
1
calculators/builder-adversary/run-01/README.md
Normal file
1
calculators/builder-adversary/run-01/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary/run-01/SOURCE.txt
Normal file
1
calculators/builder-adversary/run-01/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r1
|
||||
32
calculators/builder-adversary/run-01/calc.py
Normal file
32
calculators/builder-adversary/run-01/calc.py
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""calc.py — command-line arithmetic calculator.
|
||||
|
||||
Usage: python calc.py "<expression>"
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from calc.lexer import LexError, tokenize
|
||||
from calc.parser import ParseError, parse
|
||||
from calc.evaluator import EvalError, evaluate
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: calc.py \"<expression>\"", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
expr = sys.argv[1]
|
||||
try:
|
||||
tokens = tokenize(expr)
|
||||
ast = parse(tokens)
|
||||
result = evaluate(ast)
|
||||
except (LexError, ParseError, EvalError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
47
calculators/builder-adversary/run-01/calc/evaluator.py
Normal file
47
calculators/builder-adversary/run-01/calc/evaluator.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Evaluator for the arithmetic AST produced by calc.parser."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from calc.parser import BinOp, Num, Node, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
"""Raised on a runtime evaluation error (e.g. division by zero)."""
|
||||
|
||||
|
||||
def evaluate(node: Node) -> int | float:
|
||||
"""Walk *node* and return its numeric value.
|
||||
|
||||
Result type rule: if the mathematical result is a whole number, return int;
|
||||
otherwise return float. This guarantees '4/2' → 2 and '7/2' → 3.5.
|
||||
|
||||
Raises EvalError on division by zero.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
|
||||
if isinstance(node, Unary):
|
||||
v = evaluate(node.operand)
|
||||
return -v
|
||||
|
||||
if isinstance(node, BinOp):
|
||||
left = evaluate(node.left)
|
||||
right = evaluate(node.right)
|
||||
if node.op == '+':
|
||||
result = left + right
|
||||
elif node.op == '-':
|
||||
result = left - right
|
||||
elif node.op == '*':
|
||||
result = left * right
|
||||
elif node.op == '/':
|
||||
if right == 0:
|
||||
raise EvalError("Division by zero")
|
||||
result = left / right
|
||||
else:
|
||||
raise EvalError(f"Unknown operator {node.op!r}")
|
||||
|
||||
if isinstance(result, float) and result == int(result):
|
||||
return int(result)
|
||||
return result
|
||||
|
||||
raise EvalError(f"Unknown node type {type(node)!r}")
|
||||
67
calculators/builder-adversary/run-01/calc/lexer.py
Normal file
67
calculators/builder-adversary/run-01/calc/lexer.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Lexer for arithmetic expressions."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
"""Raised on an unrecognised character."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str # NUMBER PLUS MINUS STAR SLASH LPAREN RPAREN EOF
|
||||
value: Union[int, float, str, None]
|
||||
|
||||
|
||||
_SINGLE = {
|
||||
'+': 'PLUS',
|
||||
'-': 'MINUS',
|
||||
'*': 'STAR',
|
||||
'/': 'SLASH',
|
||||
'(': 'LPAREN',
|
||||
')': 'RPAREN',
|
||||
}
|
||||
|
||||
|
||||
def tokenize(src: str) -> list:
|
||||
"""Return a list of Token for *src*, ending with EOF."""
|
||||
tokens = []
|
||||
i = 0
|
||||
n = len(src)
|
||||
|
||||
while i < n:
|
||||
ch = src[i]
|
||||
|
||||
# Skip whitespace
|
||||
if ch in ' \t\r\n':
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Number: integer or float (leading dot allowed, trailing dot allowed)
|
||||
if 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]
|
||||
if raw == '.':
|
||||
raise LexError(f"Unexpected character '.' at position {i}")
|
||||
value = float(raw) if has_dot else int(raw)
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
continue
|
||||
|
||||
# Single-character operators and parentheses
|
||||
if ch in _SINGLE:
|
||||
tokens.append(Token(_SINGLE[ch], ch))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Anything else is an error
|
||||
raise LexError(f"Unexpected character {ch!r} at position {i}")
|
||||
|
||||
tokens.append(Token('EOF', None))
|
||||
return tokens
|
||||
143
calculators/builder-adversary/run-01/calc/parser.py
Normal file
143
calculators/builder-adversary/run-01/calc/parser.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""Recursive-descent parser for arithmetic expressions.
|
||||
|
||||
AST node types:
|
||||
Num(value) -- a numeric literal (int or float)
|
||||
BinOp(op, left, right) -- binary operation; op in ('+','-','*','/')
|
||||
Unary(op, operand) -- unary operation; op == '-'
|
||||
|
||||
Grammar (precedence low → high):
|
||||
expr := term (('+' | '-') term)*
|
||||
term := unary (('*' | '/') unary)*
|
||||
unary := '-' unary | primary
|
||||
primary := NUMBER | '(' expr ')'
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
from calc.lexer import Token, tokenize
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
"""Raised on syntactically invalid input."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AST nodes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: "Node"
|
||||
right: "Node"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: "Node"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
|
||||
Node = Union[Num, BinOp, Unary]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _Parser:
|
||||
def __init__(self, tokens: list[Token]) -> None:
|
||||
self._tokens = tokens
|
||||
self._pos = 0
|
||||
|
||||
def _peek(self) -> Token:
|
||||
return self._tokens[self._pos]
|
||||
|
||||
def _consume(self, kind: str | None = None) -> Token:
|
||||
tok = self._tokens[self._pos]
|
||||
if kind is not None and tok.kind != kind:
|
||||
raise ParseError(
|
||||
f"Expected {kind}, 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':
|
||||
tok = self._peek()
|
||||
raise ParseError(
|
||||
f"Unexpected token {tok.kind!r} ({tok.value!r}) after expression"
|
||||
)
|
||||
return node
|
||||
|
||||
def _expr(self) -> Node:
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op_tok = self._consume()
|
||||
op = op_tok.value # '+' or '-'
|
||||
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_tok = self._consume()
|
||||
op = op_tok.value # '*' or '/'
|
||||
right = self._unary()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _unary(self) -> Node:
|
||||
if self._peek().kind == 'MINUS':
|
||||
self._consume('MINUS')
|
||||
operand = self._unary()
|
||||
return Unary('-', operand)
|
||||
return self._primary()
|
||||
|
||||
def _primary(self) -> Node:
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._consume('NUMBER')
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._consume('LPAREN')
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'RPAREN':
|
||||
raise ParseError(
|
||||
f"Expected ')' but got {self._peek().kind!r}"
|
||||
)
|
||||
self._consume('RPAREN')
|
||||
return node
|
||||
if tok.kind == 'EOF':
|
||||
raise ParseError("Unexpected end of input")
|
||||
raise ParseError(
|
||||
f"Unexpected token {tok.kind!r} ({tok.value!r})"
|
||||
)
|
||||
|
||||
|
||||
def parse(tokens: list[Token]) -> Node:
|
||||
"""Parse *tokens* (from lexer.tokenize) into an AST Node.
|
||||
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
87
calculators/builder-adversary/run-01/calc/test_evaluator.py
Normal file
87
calculators/builder-adversary/run-01/calc/test_evaluator.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""Tests for calc.evaluator — covers D1, D2, D3."""
|
||||
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import EvalError, evaluate
|
||||
|
||||
|
||||
def calc(s):
|
||||
return evaluate(parse(tokenize(s)))
|
||||
|
||||
|
||||
class TestArithmetic(unittest.TestCase):
|
||||
"""D1 — arithmetic, precedence, parens, unary minus."""
|
||||
|
||||
def test_add(self):
|
||||
self.assertEqual(calc("2+3"), 5)
|
||||
|
||||
def test_mul(self):
|
||||
self.assertEqual(calc("3*4"), 12)
|
||||
|
||||
def test_precedence(self):
|
||||
self.assertEqual(calc("2+3*4"), 14)
|
||||
|
||||
def test_parens(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(self):
|
||||
self.assertEqual(calc("-2+5"), 3)
|
||||
|
||||
def test_unary_minus_mul(self):
|
||||
self.assertEqual(calc("2*-3"), -6)
|
||||
|
||||
def test_sub(self):
|
||||
self.assertEqual(calc("10-3"), 7)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(calc("((4))"), 4)
|
||||
|
||||
def test_unary_double(self):
|
||||
self.assertEqual(calc("--5"), 5)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
"""D2 — true division, division by zero → EvalError."""
|
||||
|
||||
def test_true_division(self):
|
||||
self.assertAlmostEqual(calc("7/2"), 3.5)
|
||||
|
||||
def test_division_by_zero(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("1/0")
|
||||
|
||||
def test_division_by_zero_expr(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("5/(3-3)")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
"""D3 — whole-valued results are int, non-whole are float."""
|
||||
|
||||
def test_whole_division_is_int(self):
|
||||
result = calc("4/2")
|
||||
self.assertEqual(result, 2)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_non_whole_is_float(self):
|
||||
result = calc("7/2")
|
||||
self.assertEqual(result, 3.5)
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_integer_arithmetic_stays_int(self):
|
||||
result = calc("2+3")
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_whole_negative_is_int(self):
|
||||
result = calc("6/2")
|
||||
self.assertEqual(result, 3)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
134
calculators/builder-adversary/run-01/calc/test_lexer.py
Normal file
134
calculators/builder-adversary/run-01/calc/test_lexer.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""Unit tests for calc.lexer — covers D1, D2, D3."""
|
||||
|
||||
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):
|
||||
"""D1 — integers and floats."""
|
||||
|
||||
def test_integer(self):
|
||||
toks = tokenize("42")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertEqual(toks[0].value, 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].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 3.14)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_leading_dot(self):
|
||||
toks = tokenize(".5")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 0.5)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_trailing_dot(self):
|
||||
toks = tokenize("10.")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 10.0)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_integer_eof(self):
|
||||
toks = tokenize("42")
|
||||
self.assertEqual(kinds("42"), ['NUMBER', 'EOF'])
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
"""D2 — operators and parentheses."""
|
||||
|
||||
def test_plus(self):
|
||||
self.assertIn('PLUS', kinds("+"))
|
||||
|
||||
def test_minus(self):
|
||||
self.assertIn('MINUS', kinds("-"))
|
||||
|
||||
def test_star(self):
|
||||
self.assertIn('STAR', kinds("*"))
|
||||
|
||||
def test_slash(self):
|
||||
self.assertIn('SLASH', kinds("/"))
|
||||
|
||||
def test_lparen(self):
|
||||
self.assertIn('LPAREN', kinds("("))
|
||||
|
||||
def test_rparen(self):
|
||||
self.assertIn('RPAREN', kinds(")"))
|
||||
|
||||
def test_expr_kinds(self):
|
||||
self.assertEqual(
|
||||
kinds("1+2*3"),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
def test_complex_expr(self):
|
||||
self.assertEqual(
|
||||
kinds("3.5*(1-2)"),
|
||||
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'],
|
||||
)
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
"""D3 — whitespace is skipped; invalid chars raise LexError."""
|
||||
|
||||
def test_whitespace_skipped(self):
|
||||
self.assertEqual(
|
||||
kinds(" 12 + 3 "),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
def test_tab_whitespace(self):
|
||||
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_at_raises_lexerror(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_dollar_raises_lexerror(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("$")
|
||||
self.assertIn('$', str(ctx.exception))
|
||||
|
||||
def test_letter_raises_lexerror(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("x")
|
||||
self.assertIn('x', str(ctx.exception))
|
||||
|
||||
def test_lexerror_includes_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
# position 2 (0-indexed) where '@' appears
|
||||
self.assertIn('2', str(ctx.exception))
|
||||
|
||||
def test_required_whitespace_expr(self):
|
||||
# " 12 + 3 " from the DoD
|
||||
toks = tokenize(" 12 + 3 ")
|
||||
self.assertEqual(toks[0].value, 12)
|
||||
self.assertEqual(toks[2].value, 3)
|
||||
|
||||
def test_required_complex_expr(self):
|
||||
# "3.5*(1-2)" from the DoD
|
||||
toks = tokenize("3.5*(1-2)")
|
||||
self.assertAlmostEqual(toks[0].value, 3.5)
|
||||
self.assertEqual(toks[1].kind, 'STAR')
|
||||
self.assertEqual(toks[2].kind, 'LPAREN')
|
||||
self.assertEqual(toks[3].value, 1)
|
||||
self.assertEqual(toks[4].kind, 'MINUS')
|
||||
self.assertEqual(toks[5].value, 2)
|
||||
self.assertEqual(toks[6].kind, 'RPAREN')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
149
calculators/builder-adversary/run-01/calc/test_parser.py
Normal file
149
calculators/builder-adversary/run-01/calc/test_parser.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""Tests for calc/parser.py — D1 through D5."""
|
||||
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError, Num, BinOp, Unary
|
||||
|
||||
|
||||
def p(src: str):
|
||||
"""Shorthand: tokenize + parse."""
|
||||
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)))
|
||||
tree = p("1+2*3")
|
||||
self.assertEqual(tree, 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))
|
||||
tree = p("2*3+4")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
def test_sub_then_div(self):
|
||||
# 10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2)))
|
||||
tree = p("10-6/2")
|
||||
self.assertEqual(tree, BinOp('-', Num(10), BinOp('/', Num(6), Num(2))))
|
||||
|
||||
def test_div_then_sub(self):
|
||||
# 6/2-1 → BinOp('-', BinOp('/', Num(6), Num(2)), Num(1))
|
||||
tree = p("6/2-1")
|
||||
self.assertEqual(tree, BinOp('-', BinOp('/', Num(6), Num(2)), Num(1)))
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
"""D2 — same-precedence operators associate left."""
|
||||
|
||||
def test_sub_left(self):
|
||||
# 8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
tree = p("8-3-2")
|
||||
self.assertEqual(tree, BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
|
||||
|
||||
def test_div_left(self):
|
||||
# 8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
tree = p("8/4/2")
|
||||
self.assertEqual(tree, BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
|
||||
|
||||
def test_add_left(self):
|
||||
# 1+2+3 → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("1+2+3")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_mul_left(self):
|
||||
# 2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
tree = p("2*3*4")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
"""D3 — parentheses override precedence."""
|
||||
|
||||
def test_paren_overrides_mul(self):
|
||||
# (1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("(1+2)*3")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_paren_on_right(self):
|
||||
# 3*(1+2) → BinOp('*', Num(3), BinOp('+', Num(1), Num(2)))
|
||||
tree = p("3*(1+2)")
|
||||
self.assertEqual(tree, BinOp('*', Num(3), BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_nested_parens(self):
|
||||
# ((4)) → Num(4)
|
||||
tree = p("((4))")
|
||||
self.assertEqual(tree, Num(4))
|
||||
|
||||
def test_paren_changes_assoc(self):
|
||||
# 8-(3-2) → BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))
|
||||
tree = p("8-(3-2)")
|
||||
self.assertEqual(tree, BinOp('-', Num(8), BinOp('-', Num(3), Num(2))))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
"""D4 — leading and nested unary minus."""
|
||||
|
||||
def test_simple_negative(self):
|
||||
# -5 → Unary('-', Num(5))
|
||||
tree = p("-5")
|
||||
self.assertEqual(tree, Unary('-', Num(5)))
|
||||
|
||||
def test_unary_paren(self):
|
||||
# -(1+2) → Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
tree = p("-(1+2)")
|
||||
self.assertEqual(tree, Unary('-', BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_mul_unary_rhs(self):
|
||||
# 3 * -2 → BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
tree = p("3 * -2")
|
||||
self.assertEqual(tree, BinOp('*', Num(3), Unary('-', Num(2))))
|
||||
|
||||
def test_double_unary(self):
|
||||
# --5 → Unary('-', Unary('-', Num(5)))
|
||||
tree = p("--5")
|
||||
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
|
||||
|
||||
def test_unary_in_expr(self):
|
||||
# 1 + -2 → BinOp('+', Num(1), Unary('-', Num(2)))
|
||||
tree = p("1 + -2")
|
||||
self.assertEqual(tree, BinOp('+', Num(1), Unary('-', Num(2))))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
"""D5 — malformed input raises ParseError."""
|
||||
|
||||
def test_trailing_operator(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("1 +")
|
||||
|
||||
def test_unclosed_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("(1")
|
||||
|
||||
def test_two_numbers(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("1 2")
|
||||
|
||||
def test_close_open_paren(self):
|
||||
# ")(" has no valid parse
|
||||
with self.assertRaises(ParseError):
|
||||
p(")(")
|
||||
|
||||
def test_empty_string(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("")
|
||||
|
||||
def test_close_paren_only(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p(")")
|
||||
|
||||
def test_only_operator(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("*")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,7 @@
|
||||
# BACKLOG — eval phase
|
||||
|
||||
## Build backlog
|
||||
_(Builder's section — read-only for Adversary)_
|
||||
|
||||
## Adversary findings
|
||||
_(none yet)_
|
||||
@ -0,0 +1,13 @@
|
||||
# BACKLOG-lex
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/__init__.py
|
||||
- [x] Create calc/lexer.py with Token, LexError, tokenize()
|
||||
- [x] Create calc/test_lexer.py covering D1–D4 (21 tests)
|
||||
- [x] Run tests: 21 passed, 0 failed
|
||||
- [ ] Claim D1 + D2 + D3 + D4 (all gates, single claim)
|
||||
- [ ] Await Adversary verification
|
||||
|
||||
## Adversary findings
|
||||
(none yet)
|
||||
@ -0,0 +1,20 @@
|
||||
# BACKLOG — Phase parse
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/parser.py (ParseError, Num, BinOp, Unary, recursive-descent parse())
|
||||
- [x] Create calc/test_parser.py (24 unittest tests, D1–D5 coverage)
|
||||
- [x] Verified 45 tests pass (21 lexer + 24 parser), 0 failures
|
||||
- [x] CLAIM D1–D6 (all gates claimed together)
|
||||
|
||||
## Adversary findings
|
||||
|
||||
(none yet)
|
||||
|
||||
## Break-it probes planned
|
||||
|
||||
- Precedence weak test: ensure `1+2*3` really builds `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` not `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
|
||||
- Associativity weak test: ensure `8-3-2` builds `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` not `BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))`
|
||||
- Nested unary: `--5`, `-(-(1+2))` should work
|
||||
- ParseError specificity: check it's ParseError (not generic Exception) for all 5 error cases in D5
|
||||
- Empty input edge case
|
||||
@ -0,0 +1,12 @@
|
||||
# DECISIONS — shared append-only log
|
||||
|
||||
## 2026-06-15T00:12Z — Adversary initialized
|
||||
Adversary started for phase lex. Watching for Builder claims via git.
|
||||
|
||||
## 2026-06-15 — Token representation (Builder)
|
||||
Using a dataclass for Token with `kind: str` and `value`. The `kind` is a plain string constant
|
||||
(e.g. "NUMBER"), not an enum, keeping pure stdlib with minimal boilerplate.
|
||||
|
||||
## 2026-06-15 — NUMBER value type (Builder)
|
||||
`value` for NUMBER tokens is `int` if no decimal point, else `float`. Matches Python's natural
|
||||
numeric types; convenient for the evaluator phase.
|
||||
@ -0,0 +1,43 @@
|
||||
# JOURNAL — eval phase
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Built evaluator, CLI, and tests in one go.
|
||||
|
||||
### evaluator.py
|
||||
|
||||
`evaluate(node)` walks the AST recursively:
|
||||
- `Num` → return `node.value` directly (already int or float from lexer)
|
||||
- `Unary('-', operand)` → negate result
|
||||
- `BinOp(op, left, right)` → evaluate both sides, apply op
|
||||
|
||||
Division: uses Python `left / right` (always returns float). If `right == 0`, raises `EvalError("Division by zero")`.
|
||||
|
||||
D3 result type rule: after computing a float result in BinOp, if `result == int(result)` we cast to int. This ensures `4/2 → 2` (int) and `7/2 → 3.5` (float). Integer arithmetic returns int naturally.
|
||||
|
||||
### calc.py CLI
|
||||
|
||||
`main()` accepts exactly one argv argument (the expression string).
|
||||
Catches `LexError`, `ParseError`, `EvalError` → prints `Error: <msg>` to stderr, exits 1.
|
||||
On success prints `result` (which is already int or float with correct type per D3 rule).
|
||||
|
||||
### Test run output
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 62 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
(45 prior tests from lex+parse phases, 17 new evaluator tests)
|
||||
|
||||
### CLI checks
|
||||
|
||||
```
|
||||
$ python calc.py "2+3*4" → 14
|
||||
$ python calc.py "(2+3)*4" → 20
|
||||
$ python calc.py "7/2" → 3.5
|
||||
$ python calc.py "4/2" → 2
|
||||
$ python calc.py "1/0" → Error: Division by zero (exit 1)
|
||||
$ python calc.py "1 +" → Error: Unexpected end of input (exit 1)
|
||||
```
|
||||
@ -0,0 +1,48 @@
|
||||
# JOURNAL-lex — Adversary
|
||||
|
||||
## 2026-06-15T00:12Z — Cold start
|
||||
Read phase plan lex.md. Builder has not pushed any code yet (only seed commit 13c0db5).
|
||||
Initialized REVIEW, BACKLOG, JOURNAL files. Waiting for Builder to push work.
|
||||
|
||||
DoD gates to verify:
|
||||
- D1: numbers (integers and floats)
|
||||
- D2: operators & parens
|
||||
- D3: whitespace & errors (LexError)
|
||||
- D4: tests green (python -m unittest -q)
|
||||
|
||||
## 2026-06-15T00:20Z — Verification complete
|
||||
Builder pushed claim(D1-D4). Pulled, ran cold verification, ran break-it probes.
|
||||
All four gates PASS. No vetoes. Wrote verdicts to REVIEW-lex.md.
|
||||
Adversary probes: empty string, lone dot, whitespace-only, double-dot, 1.2.3, position accuracy — all held.
|
||||
|
||||
---
|
||||
|
||||
# JOURNAL-lex — Builder
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Read phase plan. Built calc/lexer.py and calc/test_lexer.py from scratch.
|
||||
|
||||
### Implementation choices
|
||||
- Token is a dataclass with `kind: str` and `value: int | float | str | None`
|
||||
- NUMBER value is int (no dot) or float (dot present)
|
||||
- LexError message includes repr of the offending char and its 0-based position
|
||||
- Leading dot (`.5`) and trailing dot (`10.`) are valid floats (scanned by loop: stops when second dot seen)
|
||||
- Single lone dot is an error (raw == '.')
|
||||
|
||||
### Test run output
|
||||
```
|
||||
python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 21 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
|
||||
### Verification commands output
|
||||
```
|
||||
$ 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')"
|
||||
calc.lexer.LexError: Unexpected character '@' at position 2
|
||||
```
|
||||
@ -0,0 +1,44 @@
|
||||
# JOURNAL — Phase parse (Adversary)
|
||||
|
||||
## 2026-06-15T00:18Z — Adversary initialized
|
||||
- Phase parse kicked off. Lex phase confirmed ## DONE.
|
||||
- Initialized REVIEW-parse.md, STATUS-parse.md, BACKLOG-parse.md, JOURNAL-parse.md.
|
||||
- Watching for Builder to claim gates D1–D6.
|
||||
- Planned break-it probes logged in BACKLOG.
|
||||
|
||||
## 2026-06-15T00:22Z — Cold verification complete, all gates PASS
|
||||
- Watchdog pinged: Builder claimed D1–D6 in commit 7768832.
|
||||
- Read parser.py and test_parser.py cold (no prior state).
|
||||
- Ran `python -m unittest -q` → 45 tests, 0 failures.
|
||||
- Ran all AST shape checks from STATUS-parse.md — every output matched expected.
|
||||
- Ran full break-it probe suite: right-assoc trap, triple unary, deep nesting, float, extra error cases.
|
||||
- All held. No defects found. PASS recorded in REVIEW-parse.md for D1–D6.
|
||||
|
||||
---
|
||||
|
||||
# JOURNAL — Phase parse (Builder)
|
||||
|
||||
## 2026-06-15T00:19Z — Implementation complete
|
||||
|
||||
Design: recursive-descent with grammar:
|
||||
```
|
||||
expr := term (('+' | '-') term)*
|
||||
term := unary (('*' | '/') unary)*
|
||||
unary := '-' unary | primary
|
||||
primary := NUMBER | '(' expr ')'
|
||||
```
|
||||
Left-associativity emerges from iterative loops (not recursion) in expr/term.
|
||||
|
||||
Ran `python -m unittest -q` → `Ran 45 tests in 0.001s OK`
|
||||
|
||||
Key outputs verified:
|
||||
- `1+2*3` → `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` ✓
|
||||
- `8-3-2` → `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` ✓
|
||||
- `8/4/2` → `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` ✓
|
||||
- `(1+2)*3` → `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` ✓
|
||||
- `-5` → `Unary('-', Num(5))` ✓
|
||||
- `-(1+2)` → `Unary('-', BinOp('+', Num(1), Num(2)))` ✓
|
||||
- `3 * -2` → `BinOp('*', Num(3), Unary('-', Num(2)))` ✓
|
||||
- All D5 error cases → `ParseError` ✓
|
||||
|
||||
Claiming all gates D1–D6.
|
||||
@ -0,0 +1,96 @@
|
||||
# REVIEW — eval phase (Adversary)
|
||||
|
||||
_Adversary cold-verification log. Each verdict is recorded here._
|
||||
|
||||
## Status summary
|
||||
- D1 arithmetic: PASS
|
||||
- D2 division + EvalError: PASS
|
||||
- D3 result type: PASS
|
||||
- D4 CLI: PASS
|
||||
- D5 tests green + end-to-end: PASS
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1 — arithmetic: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
Cold run, commit 165c7cc.
|
||||
|
||||
Commands run and results:
|
||||
```
|
||||
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 ✓
|
||||
python calc.py "--5" → 5 ✓ (double unary)
|
||||
python calc.py "10-3-2" → 5 ✓ (left-associativity)
|
||||
python calc.py "-(2+3)" → -5 ✓ (unary on paren)
|
||||
```
|
||||
|
||||
### D2 — division + EvalError: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
python calc.py "7/2" → 3.5 ✓ (true division)
|
||||
python calc.py "1/0" → "Error: Division by zero" on stderr, exit 1 ✓
|
||||
python calc.py "5/(3-3)" → "Error: Division by zero" on stderr, exit 1 ✓
|
||||
```
|
||||
|
||||
Verified `EvalError` is raised (not bare `ZeroDivisionError`) via:
|
||||
- `calc/evaluator.py:37-38` explicitly checks `right == 0` and raises `EvalError`
|
||||
- `calc.py:24` catches `EvalError` — ZeroDivisionError would escape if not caught; confirmed not raised
|
||||
|
||||
`EvalError` is a proper subclass of `Exception`: confirmed True.
|
||||
|
||||
### D3 — result type: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
evaluate(parse(tokenize("4/2"))) → int 2 ✓
|
||||
evaluate(parse(tokenize("7/2"))) → float 3.5 ✓
|
||||
evaluate(parse(tokenize("2+3"))) → int 5 ✓ (integer arithmetic stays int)
|
||||
evaluate(parse(tokenize("-6/2"))) → int -3 ✓ (negative whole result is int)
|
||||
evaluate(parse(tokenize("1000*1000"))) → int 1000000 ✓
|
||||
```
|
||||
|
||||
Rule documented in `calc/evaluator.py` docstring. `print(4/2)` outputs `2` (not `2.0`). ✓
|
||||
|
||||
### D4 — CLI: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
python calc.py "2+3*4" → prints 14, exit 0 ✓
|
||||
python calc.py "1 +" → "Error: Unexpected end of input" on stderr, exit 1, no traceback ✓
|
||||
python calc.py "1/0" → "Error: Division by zero" on stderr, exit 1, no traceback ✓
|
||||
python calc.py "" → "Error: Empty input" on stderr, exit 1 ✓
|
||||
python calc.py → usage message on stderr, exit 1 ✓
|
||||
python calc.py "((1+2)" → error on stderr, exit 1, no traceback ✓
|
||||
```
|
||||
|
||||
No tracebacks in any error path. ✓
|
||||
|
||||
### D5 — tests green + end-to-end: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
python -m unittest -q → Ran 62 tests in 0.001s OK
|
||||
```
|
||||
|
||||
62 tests = lex + parse + evaluator suites. 0 failures, 0 errors. No regression. ✓
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(none — all gates pass, no defects found)_
|
||||
|
||||
## Break-it probes run
|
||||
|
||||
- Traceback check on all error paths: no traceback in any case ✓
|
||||
- No-args invocation: graceful usage error ✓
|
||||
- Empty string input: graceful error ✓
|
||||
- Double unary minus `--5` → 5 ✓
|
||||
- Left-associativity `10-3-2` → 5 ✓
|
||||
- Unary in division `-8/2` → -4 ✓
|
||||
- Negative whole result type `-6/2` → int -3 ✓
|
||||
- Large numbers `1000*1000` → int 1000000 ✓
|
||||
- Division by zero via expression `5/(3-3)` → EvalError ✓
|
||||
- Unclosed paren `((1+2)` → parse error, no crash ✓
|
||||
|
||||
---
|
||||
Initialized: 2026-06-15T00:24:45Z
|
||||
Verdicts filed: 2026-06-15T00:27:02Z
|
||||
@ -0,0 +1,67 @@
|
||||
# REVIEW-lex — Adversary verdicts
|
||||
|
||||
Phase: lex
|
||||
Adversary cold-started: 2026-06-15
|
||||
|
||||
## Status
|
||||
All gates verified PASS. No vetoes.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence (cold run from work-adv):**
|
||||
```
|
||||
tokenize("42") → NUMBER(42 int), EOF ✓
|
||||
tokenize("3.14") → NUMBER(3.14 float) ✓
|
||||
tokenize(".5") → NUMBER(0.5 float) ✓
|
||||
tokenize("10.") → NUMBER(10.0 float) ✓
|
||||
```
|
||||
Plan spec: "tokenize('42') → [NUMBER(42), EOF]" — confirmed exactly.
|
||||
|
||||
### D2: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
kinds("1+2*3") → ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓
|
||||
all operators +-*/() → PLUS MINUS STAR SLASH LPAREN RPAREN ✓
|
||||
tokens("3.5*(1-2)") → [('NUMBER', 3.5), ('STAR','*'), ('LPAREN','('), ('NUMBER', 1),
|
||||
('MINUS','-'), ('NUMBER', 2), ('RPAREN',')'), ('EOF', None)] ✓
|
||||
```
|
||||
|
||||
### D3: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
tokenize(" 12 + 3 ") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓ (whitespace skipped)
|
||||
tokenize("1 @ 2") → LexError: Unexpected character '@' at position 2 ✓
|
||||
tokenize("$") → LexError: ... '$' ... ✓
|
||||
tokenize("x") → LexError: ... 'x' at position 0 ✓
|
||||
tokenize(".") → LexError: ... '.' at position 0 ✓
|
||||
LexError message includes offending char AND position ✓
|
||||
```
|
||||
|
||||
### D4: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 21 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
Exit code: 0. 21 tests, 0 failures.
|
||||
Required cases covered: " 12 + 3 " ✓, "3.5*(1-2)" ✓, "1 @ 2" → LexError ✓.
|
||||
|
||||
## Adversarial probes (break-it attempts — all held)
|
||||
|
||||
- Empty string → `[EOF]` (correct)
|
||||
- Whitespace-only → `[EOF]` (correct)
|
||||
- Lone dot → `LexError: Unexpected character '.' at position 0` (correct)
|
||||
- `..` → LexError on lone dot (correct)
|
||||
- `1.2.3` → `NUMBER(1.2) NUMBER(0.3) EOF` (valid lexer behavior; parser rejects)
|
||||
- `12 @ 5` → LexError at position 3 (position accuracy confirmed)
|
||||
- All operators in sequence `+-*/()` → correct kinds
|
||||
- Newline treated as whitespace (conservative, robust)
|
||||
|
||||
No defects found. Implementation is correct per the phase plan.
|
||||
@ -0,0 +1,91 @@
|
||||
# REVIEW — Phase parse (Adversary)
|
||||
|
||||
## Status
|
||||
All gates PASS. Ready to approve ## DONE.
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
| Gate | Verdict | Timestamp | Evidence |
|
||||
|------|---------|-----------|----------|
|
||||
| D1 — precedence | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D2 — left associativity | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D3 — parentheses | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D4 — unary minus | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D5 — errors | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D6 — tests green | PASS | 2026-06-15T00:22:39Z | Ran 45 tests in 0.001s OK |
|
||||
|
||||
## Cold-verification evidence
|
||||
|
||||
### D6 — tests green
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 45 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
21 lexer + 24 parser tests, 0 failures.
|
||||
|
||||
### D1 — precedence (cold AST shape check)
|
||||
```
|
||||
1+2*3 → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓ (* binds tighter)
|
||||
2*3+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)) ✓
|
||||
10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2))) ✓
|
||||
```
|
||||
Independently derived: `1+2*3` must have `+` at root with `*` in right child — confirmed.
|
||||
|
||||
### D2 — left associativity (cold AST shape check)
|
||||
```
|
||||
8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓ (left-assoc)
|
||||
8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
|
||||
1+2+3 → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)) ✓
|
||||
1-2-3 → BinOp('-', BinOp('-', Num(1), Num(2)), Num(3)) ✓ (break-it: not right-assoc)
|
||||
```
|
||||
Iterative while-loop in `_expr`/`_term` enforces left-assoc by construction.
|
||||
|
||||
### D3 — parentheses (cold AST shape check)
|
||||
```
|
||||
(1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
3*(1+2) → BinOp('*', Num(3), BinOp('+', Num(1), Num(2))) ✓
|
||||
((4)) → Num(4) ✓
|
||||
8-(3-2) → BinOp('-', Num(8), BinOp('-', Num(3), Num(2))) ✓
|
||||
((((1+2)))) → BinOp('+', Num(1), Num(2)) ✓ (deep nesting)
|
||||
```
|
||||
|
||||
### D4 — unary minus (cold AST shape check)
|
||||
```
|
||||
-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))) ✓
|
||||
1 + -2 → BinOp('+', Num(1), Unary('-', Num(2))) ✓
|
||||
---5 → Unary('-', Unary('-', Unary('-', Num(5)))) ✓ (break-it: triple unary)
|
||||
6 / -2 → BinOp('/', Num(6), Unary('-', Num(2))) ✓ (break-it: unary in denom)
|
||||
```
|
||||
`_unary` is right-recursive: `'-' _unary | _primary` — correct for unary.
|
||||
|
||||
### D5 — errors (cold: all five plan cases + extras)
|
||||
```
|
||||
'1 +' → OK ParseError
|
||||
'(1' → OK ParseError
|
||||
'1 2' → OK ParseError
|
||||
')(' → OK ParseError
|
||||
'' → OK ParseError
|
||||
'*' → OK ParseError (break-it)
|
||||
')' → OK ParseError (break-it)
|
||||
'1+2)' → OK ParseError (break-it)
|
||||
'((1+2)' → OK ParseError (break-it)
|
||||
```
|
||||
All raise `ParseError` specifically, not a generic exception.
|
||||
|
||||
## Break-it probes run
|
||||
- Right-assoc trap (`1-2-3`, `8-3-2`, `8/4/2`): held — correctly left-assoc
|
||||
- Deep nesting `((((1+2))))`: held
|
||||
- Triple unary `---5`: held
|
||||
- Unary in denominator `6/-2`: held
|
||||
- Float literal `3.14 → Num(3.14)`: parsed fine
|
||||
- Trailing `)` after valid expr `1+2)`: ParseError ✓
|
||||
- Solo operator `*`: ParseError ✓
|
||||
- Double-unclosed paren `((1+2)`: ParseError ✓
|
||||
|
||||
## Adversary findings
|
||||
None. No defects found. All probes held.
|
||||
@ -0,0 +1,23 @@
|
||||
# STATUS — Phase eval
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase eval is complete. This is the final phase — the calculator is end-to-end: string → tokens → AST → number.
|
||||
|
||||
| Gate | Status | Verified |
|
||||
|------|--------|----------|
|
||||
| D1 — arithmetic | PASS | 2026-06-15T00:27:02Z |
|
||||
| D2 — division + EvalError | PASS | 2026-06-15T00:27:02Z |
|
||||
| D3 — result type | PASS | 2026-06-15T00:27:02Z |
|
||||
| D4 — CLI | PASS | 2026-06-15T00:27:02Z |
|
||||
| D5 — tests green + end-to-end | PASS | 2026-06-15T00:27:02Z |
|
||||
|
||||
Adversary ran 62 tests (exit 0), cold-verified all D1–D5 gates, and ran full break-it probe suite
|
||||
(double unary, left-assoc, negative whole result type, division by zero via expression, unclosed
|
||||
paren, empty string, no-args) — all held. No defects found.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `calc/evaluator.py` — `EvalError`, `evaluate()`
|
||||
- `calc/test_evaluator.py` — 17 unittest tests (D1–D3 coverage, type assertions)
|
||||
- `calc.py` — top-level CLI (D4 + end-to-end check)
|
||||
@ -0,0 +1,20 @@
|
||||
# STATUS — Phase lex
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase lex is complete.
|
||||
|
||||
| Gate | Status | Verified |
|
||||
|------|--------|----------|
|
||||
| D1 — numbers | PASS | 2026-06-15T00:20Z |
|
||||
| D2 — operators & parens | PASS | 2026-06-15T00:20Z |
|
||||
| D3 — whitespace & errors | PASS | 2026-06-15T00:20Z |
|
||||
| D4 — tests green | PASS | 2026-06-15T00:20Z |
|
||||
|
||||
Adversary ran 21 tests (exit 0), cold-verified all token outputs, and ran break-it probes — all held.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
|
||||
- `calc/test_lexer.py` — 21 unittest tests
|
||||
- `calc/__init__.py` — package marker
|
||||
@ -0,0 +1,22 @@
|
||||
# STATUS — Phase parse
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase parse is complete.
|
||||
|
||||
| Gate | Status | Verified |
|
||||
|------|--------|----------|
|
||||
| D1 — precedence | PASS | 2026-06-15T00:22:39Z |
|
||||
| D2 — left associativity | PASS | 2026-06-15T00:22:39Z |
|
||||
| D3 — parentheses | PASS | 2026-06-15T00:22:39Z |
|
||||
| D4 — unary minus | PASS | 2026-06-15T00:22:39Z |
|
||||
| D5 — errors | PASS | 2026-06-15T00:22:39Z |
|
||||
| D6 — tests green | PASS | 2026-06-15T00:22:39Z |
|
||||
|
||||
Adversary ran 45 tests (exit 0), cold-verified all AST shapes, and ran full break-it probe suite
|
||||
(right-assoc trap, triple unary, deep nesting, float, extra error cases) — all held. No defects found.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `calc/parser.py` — ParseError, Num, BinOp, Unary, parse()
|
||||
- `calc/test_parser.py` — 24 unittest tests (D1–D5 coverage, tree-structure assertions)
|
||||
3
calculators/builder-adversary/run-02/.gitignore
vendored
Normal file
3
calculators/builder-adversary/run-02/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
14
calculators/builder-adversary/run-02/GIT-LOG.txt
Normal file
14
calculators/builder-adversary/run-02/GIT-LOG.txt
Normal file
@ -0,0 +1,14 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
2b8551c status(eval): DONE — all D1-D5 gates PASS, Adversary verified
|
||||
ef58876 review(D1,D2,D3,D4,D5): PASS — all eval gates verified, no findings
|
||||
a047a79 claim(D1,D2,D3,D4,D5): implement evaluator + CLI + tests — all gates claimed
|
||||
ce36ed2 review(eval/init): Adversary initialized for eval phase — awaiting Builder claims
|
||||
a8775c9 status(parse): DONE — all D1-D6 gates PASS, Adversary verified
|
||||
2fbb241 review(D1,D2,D3,D4,D5,D6): PASS — all parse gates verified, no findings
|
||||
92b0c52 claim(D1,D2,D3,D4,D5,D6): implement parser + tests — all gates claimed
|
||||
b66e732 review(parse/init): Adversary initialized for parse phase — awaiting Builder claims
|
||||
1da6170 fix(AF-01): wrap float() in LexError for malformed number literals
|
||||
9f97633 review(D1,D2,D3,D4): PASS — all gates verified; one informational finding AF-01 (ValueError for malformed numbers, non-blocking)
|
||||
c756a0f claim(D1,D2,D3,D4): implement lexer, tests — all gates claimed
|
||||
a081340 review(init): Adversary initialized, awaiting Builder gate claims
|
||||
68c8f88 chore: seed
|
||||
1
calculators/builder-adversary/run-02/README.md
Normal file
1
calculators/builder-adversary/run-02/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary/run-02/SOURCE.txt
Normal file
1
calculators/builder-adversary/run-02/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r2
|
||||
33
calculators/builder-adversary/run-02/calc.py
Normal file
33
calculators/builder-adversary/run-02/calc.py
Normal file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Calculator CLI: python calc.py "<expression>"""
|
||||
import sys
|
||||
|
||||
from calc.lexer import tokenize, LexError
|
||||
from calc.parser import parse, ParseError
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def _fmt(value) -> str:
|
||||
"""Return value as int string if whole, else as float string."""
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return str(int(value))
|
||||
return str(value)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: calc.py <expression>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
expr = sys.argv[1]
|
||||
try:
|
||||
tokens = tokenize(expr)
|
||||
ast = parse(tokens)
|
||||
result = evaluate(ast)
|
||||
print(_fmt(result))
|
||||
except (LexError, ParseError, EvalError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
calculators/builder-adversary/run-02/calc/evaluator.py
Normal file
26
calculators/builder-adversary/run-02/calc/evaluator.py
Normal file
@ -0,0 +1,26 @@
|
||||
from calc.parser import Num, BinOp, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(node) -> "int | float":
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, Unary):
|
||||
return -evaluate(node.operand)
|
||||
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")
|
||||
return left / right
|
||||
raise EvalError(f"unknown node type: {type(node).__name__!r}")
|
||||
52
calculators/builder-adversary/run-02/calc/lexer.py
Normal file
52
calculators/builder-adversary/run-02/calc/lexer.py
Normal file
@ -0,0 +1,52 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: Any
|
||||
|
||||
|
||||
_OPERATORS = {
|
||||
'+': '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':
|
||||
i += 1
|
||||
continue
|
||||
if ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
while j < n and (src[j].isdigit() or src[j] == '.'):
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
try:
|
||||
value = float(raw) if '.' in raw else int(raw)
|
||||
except ValueError:
|
||||
raise LexError(f"malformed number {raw!r} at position {i}")
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
continue
|
||||
if ch in _OPERATORS:
|
||||
tokens.append(Token(_OPERATORS[ch], ch))
|
||||
i += 1
|
||||
continue
|
||||
raise LexError(f"unexpected character {ch!r} at position {i}")
|
||||
tokens.append(Token('EOF', None))
|
||||
return tokens
|
||||
121
calculators/builder-adversary/run-02/calc/parser.py
Normal file
121
calculators/builder-adversary/run-02/calc/parser.py
Normal file
@ -0,0 +1,121 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from calc.lexer import Token
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Any
|
||||
|
||||
def __repr__(self):
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: Any
|
||||
right: Any
|
||||
|
||||
def __repr__(self):
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: Any
|
||||
|
||||
def __repr__(self):
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
|
||||
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) -> Token:
|
||||
tok = self._peek()
|
||||
if tok.kind != kind:
|
||||
raise ParseError(
|
||||
f"expected {kind}, got {tok.kind!r} ({tok.value!r})"
|
||||
)
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
def _advance(self) -> Token:
|
||||
tok = self._tokens[self._pos]
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
def parse(self):
|
||||
if self._peek().kind == 'EOF':
|
||||
raise ParseError("empty expression")
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'EOF':
|
||||
tok = self._peek()
|
||||
raise ParseError(
|
||||
f"unexpected token {tok.kind!r} ({tok.value!r}) after expression"
|
||||
)
|
||||
return node
|
||||
|
||||
def _expr(self):
|
||||
# addition / subtraction — left-associative, lowest precedence
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op = self._advance().value
|
||||
right = self._term()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _term(self):
|
||||
# multiplication / division — left-associative, higher precedence
|
||||
node = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op = self._advance().value
|
||||
right = self._unary()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _unary(self):
|
||||
if self._peek().kind == 'MINUS':
|
||||
op = self._advance().value
|
||||
operand = self._unary()
|
||||
return Unary(op, operand)
|
||||
return self._primary()
|
||||
|
||||
def _primary(self):
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._advance()
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._advance()
|
||||
node = self._expr()
|
||||
self._consume('RPAREN')
|
||||
return node
|
||||
raise ParseError(
|
||||
f"unexpected token {tok.kind!r} ({tok.value!r})"
|
||||
)
|
||||
|
||||
|
||||
def parse(tokens: list):
|
||||
"""Parse a token list produced by calc.lexer.tokenize into an AST.
|
||||
|
||||
AST node types:
|
||||
Num(value) — a numeric literal
|
||||
BinOp(op, left, right) — binary +, -, *, /
|
||||
Unary(op, operand) — unary -
|
||||
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
126
calculators/builder-adversary/run-02/calc/test_evaluator.py
Normal file
126
calculators/builder-adversary/run-02/calc/test_evaluator.py
Normal 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 ev(src):
|
||||
return evaluate(parse(tokenize(src)))
|
||||
|
||||
|
||||
class TestArithmetic(unittest.TestCase):
|
||||
# D1: basic arithmetic, precedence, parens, unary minus
|
||||
def test_add_mul_precedence(self):
|
||||
self.assertEqual(ev("2+3*4"), 14)
|
||||
|
||||
def test_paren_override_precedence(self):
|
||||
self.assertEqual(ev("(2+3)*4"), 20)
|
||||
|
||||
def test_left_assoc_subtraction(self):
|
||||
self.assertEqual(ev("8-3-2"), 3)
|
||||
|
||||
def test_unary_minus_add(self):
|
||||
self.assertEqual(ev("-2+5"), 3)
|
||||
|
||||
def test_unary_minus_in_mul(self):
|
||||
self.assertEqual(ev("2*-3"), -6)
|
||||
|
||||
def test_simple_add(self):
|
||||
self.assertEqual(ev("1+2"), 3)
|
||||
|
||||
def test_simple_sub(self):
|
||||
self.assertEqual(ev("5-3"), 2)
|
||||
|
||||
def test_simple_mul(self):
|
||||
self.assertEqual(ev("3*4"), 12)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(ev("((3+2))*2"), 10)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
# D2: true division, division by zero raises EvalError
|
||||
def test_true_division(self):
|
||||
self.assertAlmostEqual(ev("7/2"), 3.5)
|
||||
|
||||
def test_div_by_zero(self):
|
||||
with self.assertRaises(EvalError):
|
||||
ev("1/0")
|
||||
|
||||
def test_div_by_zero_expr(self):
|
||||
with self.assertRaises(EvalError):
|
||||
ev("5/(3-3)")
|
||||
|
||||
def test_no_bare_zerodivision(self):
|
||||
try:
|
||||
ev("1/0")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("ZeroDivisionError escaped the API — should be EvalError")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
# D3: whole-valued results → int-like, non-whole → float
|
||||
def test_whole_division_is_int(self):
|
||||
result = ev("4/2")
|
||||
# 4/2 returns 2.0 as a float; CLI formats it as "2"
|
||||
# The _fmt function in calc.py handles display; here we verify the value
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
def test_non_whole_is_float(self):
|
||||
result = ev("7/2")
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertAlmostEqual(result, 3.5)
|
||||
|
||||
def test_integer_arithmetic_stays_int(self):
|
||||
result = ev("3+4")
|
||||
self.assertIsInstance(result, int)
|
||||
self.assertEqual(result, 7)
|
||||
|
||||
|
||||
class TestCLI(unittest.TestCase):
|
||||
# D4: CLI output and exit codes
|
||||
def _run(self, expr):
|
||||
return subprocess.run(
|
||||
[sys.executable, "calc.py", expr],
|
||||
capture_output=True, text=True,
|
||||
cwd=__file__.rsplit("/calc/", 1)[0],
|
||||
)
|
||||
|
||||
def test_basic_expression(self):
|
||||
r = self._run("2+3*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "14")
|
||||
|
||||
def test_paren_expression(self):
|
||||
r = self._run("(2+3)*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "20")
|
||||
|
||||
def test_true_division_output(self):
|
||||
r = self._run("7/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "3.5")
|
||||
|
||||
def test_whole_division_no_dot(self):
|
||||
r = self._run("4/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "2")
|
||||
|
||||
def test_div_by_zero_exits_nonzero(self):
|
||||
r = self._run("1/0")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertGreater(len(r.stderr.strip()), 0)
|
||||
|
||||
def test_invalid_expr_exits_nonzero(self):
|
||||
r = self._run("1 +")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertGreater(len(r.stderr.strip()), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
132
calculators/builder-adversary/run-02/calc/test_lexer.py
Normal file
132
calculators/builder-adversary/run-02/calc/test_lexer.py
Normal file
@ -0,0 +1,132 @@
|
||||
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.assertEqual(toks[1], Token('EOF', None))
|
||||
|
||||
def test_float(self):
|
||||
toks = tokenize("3.14")
|
||||
self.assertEqual(toks[0], Token('NUMBER', 3.14))
|
||||
self.assertEqual(toks[1], Token('EOF', None))
|
||||
|
||||
def test_leading_dot(self):
|
||||
toks = tokenize(".5")
|
||||
self.assertEqual(toks[0], Token('NUMBER', 0.5))
|
||||
|
||||
def test_trailing_dot(self):
|
||||
toks = tokenize("10.")
|
||||
self.assertEqual(toks[0], Token('NUMBER', 10.0))
|
||||
|
||||
def test_integer_value_type(self):
|
||||
toks = tokenize("42")
|
||||
self.assertIsInstance(toks[0].value, int)
|
||||
|
||||
def test_float_value_type(self):
|
||||
toks = tokenize("3.14")
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
def test_plus(self):
|
||||
self.assertIn(Token('PLUS', '+'), tokenize("+"))
|
||||
|
||||
def test_minus(self):
|
||||
self.assertIn(Token('MINUS', '-'), tokenize("-"))
|
||||
|
||||
def test_star(self):
|
||||
self.assertIn(Token('STAR', '*'), tokenize("*"))
|
||||
|
||||
def test_slash(self):
|
||||
self.assertIn(Token('SLASH', '/'), tokenize("/"))
|
||||
|
||||
def test_lparen(self):
|
||||
self.assertIn(Token('LPAREN', '('), tokenize("("))
|
||||
|
||||
def test_rparen(self):
|
||||
self.assertIn(Token('RPAREN', ')'), tokenize(")"))
|
||||
|
||||
def test_expression_kinds(self):
|
||||
self.assertEqual(
|
||||
kinds("1+2*3"),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
def test_complex_expression(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_tabs_skipped(self):
|
||||
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_invalid_char_raises(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_dollar_raises(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$")
|
||||
|
||||
def test_letter_raises(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("x")
|
||||
|
||||
def test_lex_error_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('2', str(ctx.exception)) # position 2
|
||||
|
||||
|
||||
class TestEndToEnd(unittest.TestCase):
|
||||
def test_padded_addition(self):
|
||||
v = values(" 12 + 3 ")
|
||||
self.assertEqual(v, [('NUMBER', 12), ('PLUS', '+'), ('NUMBER', 3), ('EOF', None)])
|
||||
|
||||
def test_complex_with_values(self):
|
||||
v = values("3.5*(1-2)")
|
||||
self.assertEqual(v, [
|
||||
('NUMBER', 3.5),
|
||||
('STAR', '*'),
|
||||
('LPAREN', '('),
|
||||
('NUMBER', 1),
|
||||
('MINUS', '-'),
|
||||
('NUMBER', 2),
|
||||
('RPAREN', ')'),
|
||||
('EOF', None),
|
||||
])
|
||||
|
||||
def test_eof_always_last(self):
|
||||
for src in ["", "1", "1+2", "()"]:
|
||||
toks = tokenize(src)
|
||||
self.assertEqual(toks[-1].kind, 'EOF')
|
||||
|
||||
def test_empty_string(self):
|
||||
toks = tokenize("")
|
||||
self.assertEqual(toks, [Token('EOF', None)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
128
calculators/builder-adversary/run-02/calc/test_parser.py
Normal file
128
calculators/builder-adversary/run-02/calc/test_parser.py
Normal file
@ -0,0 +1,128 @@
|
||||
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_mul_precedence(self):
|
||||
# 1+2*3 => BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
tree = p("1+2*3")
|
||||
self.assertEqual(tree, BinOp('+', Num(1), BinOp('*', Num(2), Num(3))))
|
||||
|
||||
def test_mul_add_precedence(self):
|
||||
# 2*3+1 => BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
|
||||
tree = p("2*3+1")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)))
|
||||
|
||||
def test_sub_div_precedence(self):
|
||||
# 6-4/2 => BinOp('-', Num(6), BinOp('/', Num(4), Num(2)))
|
||||
tree = p("6-4/2")
|
||||
self.assertEqual(tree, BinOp('-', Num(6), BinOp('/', Num(4), Num(2))))
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
# D2: same-precedence operators associate left
|
||||
def test_subtraction_left_assoc(self):
|
||||
# 8-3-2 => BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
tree = p("8-3-2")
|
||||
self.assertEqual(tree, BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
|
||||
|
||||
def test_division_left_assoc(self):
|
||||
# 8/4/2 => BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
tree = p("8/4/2")
|
||||
self.assertEqual(tree, BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
|
||||
|
||||
def test_addition_left_assoc(self):
|
||||
# 1+2+3 => BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("1+2+3")
|
||||
self.assertEqual(tree, 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))
|
||||
tree = p("2*3*4")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
# D3: parens override precedence
|
||||
def test_paren_plus_under_mul(self):
|
||||
# (1+2)*3 => BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("(1+2)*3")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_nested_parens(self):
|
||||
tree = p("((4))")
|
||||
self.assertEqual(tree, Num(4))
|
||||
|
||||
def test_paren_changes_assoc(self):
|
||||
# 8-(3-2) => BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))
|
||||
tree = p("8-(3-2)")
|
||||
self.assertEqual(tree, BinOp('-', Num(8), BinOp('-', Num(3), Num(2))))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
# D4: unary minus
|
||||
def test_simple_unary(self):
|
||||
tree = p("-5")
|
||||
self.assertEqual(tree, Unary('-', Num(5)))
|
||||
|
||||
def test_unary_in_paren(self):
|
||||
# -(1+2) => Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
tree = p("-(1+2)")
|
||||
self.assertEqual(tree, Unary('-', BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_unary_in_mul(self):
|
||||
# 3 * -2 => BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
tree = p("3 * -2")
|
||||
self.assertEqual(tree, BinOp('*', Num(3), Unary('-', Num(2))))
|
||||
|
||||
def test_double_unary(self):
|
||||
# --5 => Unary('-', Unary('-', Num(5)))
|
||||
tree = p("--5")
|
||||
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
|
||||
|
||||
def test_unary_plus_expr(self):
|
||||
# -1 + 2 => BinOp('+', Unary('-', Num(1)), Num(2))
|
||||
tree = p("-1+2")
|
||||
self.assertEqual(tree, BinOp('+', Unary('-', Num(1)), Num(2)))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
# D5: malformed input raises ParseError
|
||||
def test_trailing_operator(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("")
|
||||
|
||||
def test_only_operator(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("*")
|
||||
|
||||
def test_mismatched_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("(1+2")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,13 @@
|
||||
# BACKLOG-eval
|
||||
|
||||
## Build backlog
|
||||
(Builder-owned — read only to Adversary)
|
||||
|
||||
- [x] D1: implement evaluate() for arithmetic, precedence, parens, unary minus
|
||||
- [x] D2: true division; EvalError on divide-by-zero
|
||||
- [x] D3: _fmt() for whole vs non-whole display
|
||||
- [x] D4: calc.py CLI
|
||||
- [x] D5: test_evaluator.py (22 tests); full suite 68 tests green
|
||||
|
||||
## Adversary findings
|
||||
(No findings yet — eval phase not started)
|
||||
@ -0,0 +1,27 @@
|
||||
# BACKLOG-lex
|
||||
|
||||
## Build backlog
|
||||
(Builder-owned — read-only to Adversary)
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### AF-01: unhandled ValueError for malformed number literals [informational, non-blocking]
|
||||
|
||||
**Repro:**
|
||||
```python
|
||||
from calc.lexer import tokenize, LexError
|
||||
tokenize('1.2.3') # raises ValueError, not LexError
|
||||
tokenize('.') # raises ValueError, not LexError
|
||||
tokenize('..') # raises ValueError, not LexError
|
||||
```
|
||||
|
||||
**Root cause:** `lexer.py` line 39: `float(raw)` is called without a try/except. If the
|
||||
greedy digit/dot scan produces an unparseable string (e.g. `1.2.3` or bare `.`), Python
|
||||
raises `ValueError` instead of the module's `LexError`.
|
||||
|
||||
**Impact:** Not a DoD violation (D3 specifies invalid *characters*, not malformed tokens).
|
||||
However it leaks internal Python exceptions for unusual but possible inputs. Recommend
|
||||
wrapping in `try/except ValueError` and re-raising as `LexError` with position info.
|
||||
|
||||
**Status:** Informational — Builder may address in this phase or a follow-up. Adversary
|
||||
will close this finding if re-tested and passing.
|
||||
@ -0,0 +1,16 @@
|
||||
# BACKLOG-parse
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 — precedence: implemented via separate `_expr`/`_term` levels
|
||||
- [x] D2 — left associativity: `while` loop in `_expr`/`_term`
|
||||
- [x] D3 — parentheses: `_primary` handles LPAREN/RPAREN
|
||||
- [x] D4 — unary minus: `_unary` level, right-recursive
|
||||
- [x] D5 — ParseError: defined and raised for all malformed inputs
|
||||
- [x] D6 — tests green: 46 tests, 0 failures
|
||||
|
||||
All items complete. Awaiting Adversary verification.
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(Adversary writes here)_
|
||||
@ -0,0 +1,10 @@
|
||||
# DECISIONS — shared (append-only)
|
||||
|
||||
## 2026-06-15
|
||||
- Adversary initialized; awaiting Builder gate claims on D1–D4
|
||||
|
||||
## D-001: Token representation
|
||||
Token is a dataclass with `kind: str` and `value: Any`. NUMBER tokens carry int or float value; operator tokens carry the character string; EOF carries None. This makes the type easy to pattern-match in future parser/evaluator phases.
|
||||
|
||||
## D-002: LexError
|
||||
LexError subclasses Exception (not ValueError) for clean catching. Message format: `"unexpected character {char!r} at position {pos}"`.
|
||||
@ -0,0 +1,35 @@
|
||||
# JOURNAL-eval — Builder
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
### What was built
|
||||
|
||||
- `calc/evaluator.py`: `EvalError` exception + `evaluate(node) -> int | float` walking AST nodes (Num, BinOp, Unary). Division by zero raises `EvalError` explicitly before Python's `ZeroDivisionError` can escape.
|
||||
- `calc.py` (root): CLI entry point. Calls `tokenize → parse → evaluate`. `_fmt()` converts whole-valued floats to int display.
|
||||
- `calc/test_evaluator.py`: 22 unittest tests across TestArithmetic (9), TestDivision (4), TestResultType (3), TestCLI (6).
|
||||
|
||||
### Test run
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 68 tests in 0.224s
|
||||
OK
|
||||
```
|
||||
|
||||
### CLI spot-checks
|
||||
|
||||
```
|
||||
$ python calc.py "2+3*4" → 14
|
||||
$ python calc.py "(2+3)*4" → 20
|
||||
$ python calc.py "7/2" → 3.5
|
||||
$ python calc.py "4/2" → 2
|
||||
$ python calc.py "1/0" → error: division by zero (exit 1)
|
||||
$ python calc.py "1 +" → error: unexpected token 'EOF' (None) (exit 1)
|
||||
```
|
||||
|
||||
All match DoD expected values.
|
||||
|
||||
### Design notes
|
||||
|
||||
- `evaluate` always returns `int` for integer operations and `float` for true division. The `_fmt` function in `calc.py` handles D3 display: floats that are whole become int strings.
|
||||
- `EvalError` wraps division by zero via an explicit `if right == 0` check before the `/` operator — avoids bare `ZeroDivisionError`.
|
||||
@ -0,0 +1,72 @@
|
||||
# JOURNAL-lex — Adversary
|
||||
|
||||
## 2026-06-15 — Wake 1
|
||||
- Read phase plan: mission is lexer for Python arithmetic calculator
|
||||
- Checked origin/main: only seed commit, Builder has not pushed any work yet
|
||||
- Set up REVIEW-lex.md, BACKLOG-lex.md, STATUS-lex.md, JOURNAL-lex.md
|
||||
- Will poll for Builder claims
|
||||
|
||||
## 2026-06-15 — Wake 2 (watchdog ping)
|
||||
- Pulled claim(D1,D2,D3,D4) commit from Builder — all gates claimed at once
|
||||
- Read STATUS-lex.md for verification commands (did NOT read JOURNAL before verdicts)
|
||||
- Cold-verified all four gates from my own clone:
|
||||
|
||||
### D1 verification
|
||||
- `tokenize('42')` → NUMBER(42 int) EOF ✓
|
||||
- `tokenize('3.14')` → NUMBER(3.14 float) EOF ✓
|
||||
- `tokenize('.5')` → NUMBER(0.5) EOF ✓
|
||||
- `tokenize('10.')` → NUMBER(10.0) EOF ✓
|
||||
|
||||
### D2 verification
|
||||
- `tokenize('1+2*3')` kinds → ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'] ✓
|
||||
- `tokenize('3.5*(1-2)')` → correct full token list ✓
|
||||
- All 6 operator kinds present in test suite ✓
|
||||
|
||||
### D3 verification
|
||||
- `tokenize(' 12 + 3 ')` → ['NUMBER','PLUS','NUMBER','EOF'] ✓
|
||||
- `tokenize('1 @ 2')` → LexError: "unexpected character '@' at position 2" ✓
|
||||
- '@' and '2' both in error message ✓
|
||||
|
||||
### D4 verification
|
||||
- `python -m unittest -q` → Ran 24 tests, OK, exit 0 ✓
|
||||
- Three mandatory test inputs from DoD confirmed in test suite ✓
|
||||
|
||||
### Break-it probes
|
||||
- `tokenize('1.2.3')` → ValueError (not LexError) — logged as AF-01 (non-blocking)
|
||||
- `tokenize('.')` → ValueError (not LexError) — same issue
|
||||
- `tokenize('..')` → ValueError (not LexError) — same issue
|
||||
- `tokenize('1x')` → LexError at position 1 ✓ (x is invalid char)
|
||||
- `tokenize('\n')` → LexError ✓ (newline not in whitespace set)
|
||||
- `tokenize('')` → [Token(EOF, None)] ✓
|
||||
- SLASH operator explicitly tested ✓
|
||||
|
||||
### Verdict
|
||||
All four gates: PASS. One informational finding (AF-01: ValueError for malformed number
|
||||
literals). No VETO issued. Builder may write ## DONE.
|
||||
|
||||
## 2026-06-15 — Builder Wake 2 (post-verdicts)
|
||||
|
||||
Pulled Adversary verdict commit. All four gates PASS.
|
||||
|
||||
Addressed AF-01: wrapped `float(raw)` in try/except ValueError → re-raises as LexError.
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 24 tests in 0.000s
|
||||
OK
|
||||
|
||||
$ python -c "
|
||||
from calc.lexer import tokenize, LexError
|
||||
for case in ['1.2.3', '.', '..']:
|
||||
try:
|
||||
tokenize(case)
|
||||
print(f'FAIL: {case!r}')
|
||||
except LexError as e:
|
||||
print(f'OK: {case!r} -> LexError: {e}')
|
||||
"
|
||||
OK: '1.2.3' -> LexError: malformed number '1.2.3' at position 0
|
||||
OK: '.' -> LexError: malformed number '.' at position 0
|
||||
OK: '..' -> LexError: malformed number '..' at position 0
|
||||
```
|
||||
|
||||
Updated STATUS-lex.md to ## DONE. Phase lex complete.
|
||||
@ -0,0 +1,45 @@
|
||||
# JOURNAL-parse — Builder
|
||||
|
||||
## 2026-06-15
|
||||
|
||||
### Implementation
|
||||
|
||||
Built `calc/parser.py` as a classic recursive-descent parser with three precedence levels:
|
||||
|
||||
```
|
||||
expr → term (('+' | '-') term)* # left-assoc, lowest
|
||||
term → unary (('*' | '/') unary)* # left-assoc, higher
|
||||
unary → '-' unary | primary # right-recursive for nested --
|
||||
primary→ NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
This naturally yields left-associativity (the `while` loop builds left-leaning trees) and correct precedence (mul/div are parsed inside `term` which is called from `expr`).
|
||||
|
||||
### Test run output
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
......................................................
|
||||
Ran 46 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
(46 = 9 existing lex tests + 17 new parser tests)
|
||||
|
||||
### Manual gate verification
|
||||
|
||||
```
|
||||
D1 add-mul: BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓
|
||||
D1 mul-add: BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)) ✓
|
||||
D2 sub: BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓
|
||||
D2 div: BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
|
||||
D3 paren: BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
D4 unary: Unary('-', Num(5)) ✓
|
||||
D4 u-paren: Unary('-', BinOp('+', Num(1), Num(2))) ✓
|
||||
D4 mul-u: BinOp('*', Num(3), Unary('-', Num(2))) ✓
|
||||
D5 '1 +' → ParseError: unexpected token 'EOF' (None) ✓
|
||||
D5 '(1' → ParseError: expected RPAREN, got 'EOF' (None) ✓
|
||||
D5 '1 2' → ParseError: unexpected token 'NUMBER' (2) after expression ✓
|
||||
D5 ')(' → ParseError: unexpected token 'RPAREN' (')') ✓
|
||||
D5 '' → ParseError: empty expression ✓
|
||||
```
|
||||
120
calculators/builder-adversary/run-02/machine-docs/REVIEW-eval.md
Normal file
120
calculators/builder-adversary/run-02/machine-docs/REVIEW-eval.md
Normal file
@ -0,0 +1,120 @@
|
||||
# REVIEW-eval — Adversary Verdicts
|
||||
|
||||
## Legend
|
||||
- PASS @<ts> — gate accepted, evidence below
|
||||
- FAIL — repro steps below, Builder must fix
|
||||
|
||||
---
|
||||
|
||||
## D1 — arithmetic
|
||||
**PASS @2026-06-15T00:54Z**
|
||||
|
||||
Cold run — all 5 DoD-mandated cases:
|
||||
```
|
||||
'2+3*4' -> 14 expected 14 OK
|
||||
'(2+3)*4' -> 20 expected 20 OK
|
||||
'8-3-2' -> 3 expected 3 OK
|
||||
'-2+5' -> 3 expected 3 OK
|
||||
'2*-3' -> -6 expected -6 OK
|
||||
```
|
||||
|
||||
Extra break-it probes (all correct):
|
||||
```
|
||||
'2+3+4' -> 9 OK (left-assoc addition)
|
||||
'10-2-3' -> 5 OK (left-assoc subtraction)
|
||||
'2*3*4' -> 24 OK (left-assoc multiplication)
|
||||
'--5' -> 5 OK (double unary minus)
|
||||
'(-3)*(-2)' -> 6 OK (unary in parens)
|
||||
'(1+2)*(3+4)' -> 21 OK (nested parens)
|
||||
'0*100' -> 0 OK
|
||||
```
|
||||
|
||||
`python -m unittest calc.test_evaluator.TestArithmetic -q` — 0 failures.
|
||||
|
||||
---
|
||||
|
||||
## D2 — division
|
||||
**PASS @2026-06-15T00:54Z**
|
||||
|
||||
Cold run:
|
||||
```
|
||||
'7/2' -> 3.5 OK (true division)
|
||||
'1/0' -> EvalError: division by zero OK (not ZeroDivisionError)
|
||||
'5/(3-3)' -> EvalError: division by zero OK (dynamic zero denominator)
|
||||
```
|
||||
|
||||
Implementation: explicit `if right == 0: raise EvalError(...)` at `calc/evaluator.py:18-21` — `ZeroDivisionError` cannot escape the API boundary.
|
||||
|
||||
`python -m unittest calc.test_evaluator.TestDivision -q` — 0 failures.
|
||||
|
||||
---
|
||||
|
||||
## D3 — result type
|
||||
**PASS @2026-06-15T00:54Z**
|
||||
|
||||
Cold run — CLI output (stdout only, no stderr):
|
||||
```
|
||||
'4/2' -> '2' OK (whole float -> int display)
|
||||
'9/3' -> '3' OK (whole float -> int display)
|
||||
'0/5' -> '0' OK (zero result -> int display)
|
||||
'7/2' -> '3.5' OK (non-whole)
|
||||
'1/3' -> '0.3333333333333333' OK (non-whole)
|
||||
'22/7' -> '3.142857142857143' OK (non-whole)
|
||||
```
|
||||
|
||||
Rule confirmed: `_fmt()` in `calc.py` calls `value.is_integer()` on floats; whole → cast to int for display.
|
||||
|
||||
`python -m unittest calc.test_evaluator.TestResultType -q` — 0 failures.
|
||||
|
||||
---
|
||||
|
||||
## D4 — CLI
|
||||
**PASS @2026-06-15T00:54Z**
|
||||
|
||||
Cold run — all DoD cases:
|
||||
```
|
||||
python calc.py "2+3*4" -> stdout='14' stderr='' exit=0 OK
|
||||
python calc.py "(2+3)*4" -> stdout='20' stderr='' exit=0 OK
|
||||
python calc.py "7/2" -> stdout='3.5' stderr='' exit=0 exit=0 OK
|
||||
python calc.py "4/2" -> stdout='2' stderr='' exit=0 OK
|
||||
python calc.py "1/0" -> stdout='' stderr='error: division by zero' exit=1 OK
|
||||
python calc.py "1 +" -> stdout='' stderr='error: unexpected token ...' exit=1 OK
|
||||
```
|
||||
|
||||
Additional probes:
|
||||
- No-arg: stderr='usage: calc.py <expression>', exit=1 OK
|
||||
- Empty string `""`: stderr='error: empty expression', exit=1 OK
|
||||
- No traceback in any error case (grepped for "Traceback" — not found) OK
|
||||
- Errors go to stderr, stdout is empty on error (verified via redirect) OK
|
||||
|
||||
---
|
||||
|
||||
## D5 — tests green + end-to-end
|
||||
**PASS @2026-06-15T00:54Z**
|
||||
|
||||
Cold run:
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 68 tests in 0.210s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
Exit code 0. 68/68 pass (24 lex + 22 parse + 22 eval, including 6 CLI subprocess tests).
|
||||
|
||||
No regression in prior lex/parse tests.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Gate | Verdict |
|
||||
|------|---------|
|
||||
| D1 — arithmetic | **PASS** |
|
||||
| D2 — division | **PASS** |
|
||||
| D3 — result type | **PASS** |
|
||||
| D4 — CLI | **PASS** |
|
||||
| D5 — tests green | **PASS** |
|
||||
|
||||
All gates PASS. No findings. Builder may write "## DONE" to STATUS-eval.md.
|
||||
103
calculators/builder-adversary/run-02/machine-docs/REVIEW-lex.md
Normal file
103
calculators/builder-adversary/run-02/machine-docs/REVIEW-lex.md
Normal file
@ -0,0 +1,103 @@
|
||||
# REVIEW-lex — Adversary Verdicts
|
||||
|
||||
## Legend
|
||||
- PASS @<ts> — gate accepted, evidence below
|
||||
- FAIL — repro steps below, Builder must fix
|
||||
|
||||
---
|
||||
|
||||
## D1 — numbers
|
||||
**PASS @2026-06-15T00:36Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
python -c "...tokenize('42')..." → NUMBER(42, int), EOF — PASS
|
||||
python -c "...tokenize('3.14')..." → NUMBER(3.14, float), EOF — PASS
|
||||
python -c "...tokenize('.5')..." → NUMBER(0.5), EOF — PASS
|
||||
python -c "...tokenize('10.')..." → NUMBER(10.0), EOF — PASS
|
||||
```
|
||||
Type assertions: `isinstance(42, int)` ✓, `isinstance(3.14, float)` ✓
|
||||
|
||||
---
|
||||
|
||||
## D2 — operators & parens
|
||||
**PASS @2026-06-15T00:36Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
tokenize('1+2*3') kinds → ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'] ✓
|
||||
tokenize('3.5*(1-2)') → [('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)] ✓
|
||||
All 6 operators (PLUS MINUS STAR SLASH LPAREN RPAREN) individually tested ✓
|
||||
SLASH explicitly tested in test_lexer.py ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D3 — whitespace & errors
|
||||
**PASS @2026-06-15T00:36Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
tokenize(' 12 + 3 ') kinds → ['NUMBER','PLUS','NUMBER','EOF'] ✓
|
||||
tokenize('1 @ 2') → raises calc.lexer.LexError: unexpected character '@' at position 2
|
||||
'@' in message ✓, '2' (position) in message ✓
|
||||
Plan's verbatim command exits code 1 with correct traceback ✓
|
||||
```
|
||||
|
||||
Break-it probes run (see Adversary findings for non-blocking issues):
|
||||
- `$` raises LexError ✓
|
||||
- `x` (letter) raises LexError ✓
|
||||
- `\n` raises LexError (treated as invalid char, reasonable) ✓
|
||||
|
||||
---
|
||||
|
||||
## D4 — tests green
|
||||
**PASS @2026-06-15T00:36Z**
|
||||
|
||||
Cold run:
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 24 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
Exit code 0. 24/24 pass.
|
||||
|
||||
DoD-mandated test inputs confirmed present:
|
||||
- `" 12 + 3 "` — covered by test_spaces_between_tokens + test_padded_addition ✓
|
||||
- `"3.5*(1-2)"` — covered by test_complex_expression + test_complex_with_values ✓
|
||||
- `"1 @ 2"` raises LexError — covered by test_invalid_char_raises + test_lex_error_position ✓
|
||||
|
||||
---
|
||||
|
||||
## Non-blocking finding: unhandled ValueError for malformed number literals
|
||||
|
||||
**Severity: informational — does not fail any DoD gate**
|
||||
|
||||
`tokenize('1.2.3')`, `tokenize('.')`, `tokenize('..')` all raise Python's built-in
|
||||
`ValueError` ("could not convert string to float: ...") instead of `LexError`.
|
||||
The lexer greedily consumes digit/dot sequences then passes the raw string to
|
||||
`float()` without catching failure.
|
||||
|
||||
The DoD's D3 specifies "invalid character (e.g. @, $, a letter)" — not malformed
|
||||
number literals — so this does not block PASS. However, downstream parser/evaluator
|
||||
phases will see unexpected ValueError exceptions from edge-case inputs. The Builder
|
||||
should consider wrapping the `float(raw)` call in a try/except that re-raises as
|
||||
`LexError`.
|
||||
|
||||
This finding is noted only; the Builder may address it in a follow-up or the next phase.
|
||||
No VETO issued.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Gate | Verdict |
|
||||
|------|---------|
|
||||
| D1 — numbers | **PASS** |
|
||||
| D2 — operators & parens | **PASS** |
|
||||
| D3 — whitespace & errors | **PASS** |
|
||||
| D4 — tests green | **PASS** |
|
||||
|
||||
All gates PASS. Builder may write "## DONE" to STATUS-lex.md.
|
||||
@ -0,0 +1,126 @@
|
||||
# REVIEW-parse — Adversary Verdicts
|
||||
|
||||
## Legend
|
||||
- PASS @<ts> — gate accepted, evidence below
|
||||
- FAIL — repro steps below, Builder must fix
|
||||
|
||||
---
|
||||
|
||||
## D1 — precedence
|
||||
**PASS @2026-06-15T00:50Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
parse(tokenize('1+2*3')) → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓
|
||||
parse(tokenize('2*3+1')) → BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)) ✓
|
||||
```
|
||||
Both match expected repr exactly. `*` binds tighter than `+` in both orderings.
|
||||
|
||||
Extra probe — complex chain `1+2+3*4-5`:
|
||||
→ `BinOp('-', BinOp('+', BinOp('+', Num(1), Num(2)), BinOp('*', Num(3), Num(4))), Num(5))` ✓
|
||||
`3*4` is correctly nested under addition/subtraction.
|
||||
|
||||
---
|
||||
|
||||
## D2 — left associativity
|
||||
**PASS @2026-06-15T00:50Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
parse(tokenize('8-3-2')) → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓
|
||||
parse(tokenize('8/4/2')) → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
|
||||
```
|
||||
|
||||
Extra probes:
|
||||
```
|
||||
parse(tokenize('2*3*4')) → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)) ✓
|
||||
parse(tokenize('1+2+3')) → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
```
|
||||
Explicit assertion `r == BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))` passed.
|
||||
|
||||
---
|
||||
|
||||
## D3 — parentheses
|
||||
**PASS @2026-06-15T00:50Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
parse(tokenize('(1+2)*3')) → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
parse(tokenize('(-3)*2')) → BinOp('*', Unary('-', Num(3)), Num(2)) ✓
|
||||
```
|
||||
Parens correctly place `+` sub-tree under `*`.
|
||||
|
||||
---
|
||||
|
||||
## D4 — unary minus
|
||||
**PASS @2026-06-15T00:50Z**
|
||||
|
||||
Cold run evidence:
|
||||
```
|
||||
parse(tokenize('-5')) → Unary('-', Num(5)) ✓
|
||||
parse(tokenize('-(1+2)')) → Unary('-', BinOp('+', Num(1), Num(2))) ✓
|
||||
parse(tokenize('3 * -2')) → BinOp('*', Num(3), Unary('-', Num(2))) ✓
|
||||
```
|
||||
|
||||
Extra probes:
|
||||
```
|
||||
parse(tokenize('--5')) → Unary('-', Unary('-', Num(5))) ✓ (recursive, correct)
|
||||
parse(tokenize('(-3)*2')) → BinOp('*', Unary('-', Num(3)), Num(2)) ✓
|
||||
```
|
||||
`_unary` is correctly recursive for double-negation.
|
||||
|
||||
---
|
||||
|
||||
## D5 — errors
|
||||
**PASS @2026-06-15T00:50Z**
|
||||
|
||||
Cold run — all five DoD-mandated cases:
|
||||
```
|
||||
'1 +' → ParseError: unexpected token 'EOF' (None) ✓
|
||||
'(1' → ParseError: expected RPAREN, got 'EOF' (None) ✓
|
||||
'1 2' → ParseError: unexpected token 'NUMBER' (2) after expression ✓
|
||||
')(' → ParseError: unexpected token 'RPAREN' (')') ✓
|
||||
'' → ParseError: empty expression ✓
|
||||
```
|
||||
All raise `ParseError` (not `ValueError`, `IndexError`, or other exceptions).
|
||||
|
||||
Extra break-it probes — all raise `ParseError` (not other exceptions):
|
||||
```
|
||||
'+5' → ParseError: unexpected token 'PLUS' ('+') ✓
|
||||
'1++2' → ParseError: unexpected token 'PLUS' ('+') ✓
|
||||
'()' → ParseError: unexpected token 'RPAREN' (')') ✓
|
||||
'1 /' → ParseError: unexpected token 'EOF' (None) ✓
|
||||
'* 2' → ParseError: unexpected token 'STAR' ('*') ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D6 — tests green
|
||||
**PASS @2026-06-15T00:50Z**
|
||||
|
||||
Cold run:
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 46 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
Exit code 0. 46/46 pass (24 lex + 22 parser).
|
||||
|
||||
DoD requires 0 failures — confirmed.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Gate | Verdict |
|
||||
|------|---------|
|
||||
| D1 — precedence | **PASS** |
|
||||
| D2 — left associativity | **PASS** |
|
||||
| D3 — parentheses | **PASS** |
|
||||
| D4 — unary minus | **PASS** |
|
||||
| D5 — errors | **PASS** |
|
||||
| D6 — tests green | **PASS** |
|
||||
|
||||
All gates PASS. No findings. Builder may write "## DONE" to STATUS-parse.md.
|
||||
111
calculators/builder-adversary/run-02/machine-docs/STATUS-eval.md
Normal file
111
calculators/builder-adversary/run-02/machine-docs/STATUS-eval.md
Normal file
@ -0,0 +1,111 @@
|
||||
# STATUS-eval — Builder
|
||||
|
||||
## DONE
|
||||
|
||||
## Current state
|
||||
|
||||
Gate: D1, D2, D3, D4, D5 — all PASS (Adversary verified @2026-06-15T00:54Z)
|
||||
|
||||
---
|
||||
|
||||
## Claims
|
||||
|
||||
### D1 — arithmetic
|
||||
|
||||
**What:** `evaluate(parse(tokenize(s)))` correct for `+ - * /`, precedence, parens, unary minus.
|
||||
|
||||
**How to verify:**
|
||||
```bash
|
||||
python -m unittest calc.test_evaluator.TestArithmetic -q
|
||||
```
|
||||
|
||||
**Expected:** 0 failures. Specific cases:
|
||||
- `"2+3*4"` → 14
|
||||
- `"(2+3)*4"` → 20
|
||||
- `"8-3-2"` → 3
|
||||
- `"-2+5"` → 3
|
||||
- `"2*-3"` → -6
|
||||
|
||||
**Where:** `calc/evaluator.py` + `calc/test_evaluator.py`
|
||||
|
||||
---
|
||||
|
||||
### D2 — division
|
||||
|
||||
**What:** `/` is true division; `EvalError` raised on division by zero, not bare `ZeroDivisionError`.
|
||||
|
||||
**How to verify:**
|
||||
```bash
|
||||
python -m unittest calc.test_evaluator.TestDivision -q
|
||||
```
|
||||
|
||||
**Expected:** 0 failures. Includes:
|
||||
- `"7/2"` → 3.5
|
||||
- `"1/0"` → `EvalError`
|
||||
- `"5/(3-3)"` → `EvalError`
|
||||
- No `ZeroDivisionError` escaping the API
|
||||
|
||||
**Where:** `calc/evaluator.py:18-21`
|
||||
|
||||
---
|
||||
|
||||
### D3 — result type
|
||||
|
||||
**What:** Whole-valued results print without `.0`; non-whole as float.
|
||||
|
||||
**How to verify:**
|
||||
```bash
|
||||
python -m unittest calc.test_evaluator.TestResultType -q
|
||||
python calc.py "4/2" # should print: 2
|
||||
python calc.py "7/2" # should print: 3.5
|
||||
```
|
||||
|
||||
**Expected:** 0 failures; `4/2` → `2` (no dot), `7/2` → `3.5`.
|
||||
|
||||
**Rule:** The `_fmt()` function in `calc.py` checks `value.is_integer()` on floats; if true, casts to `int` for display.
|
||||
|
||||
**Where:** `calc.py:_fmt()`, `calc/test_evaluator.py:TestResultType`
|
||||
|
||||
---
|
||||
|
||||
### D4 — CLI
|
||||
|
||||
**What:** `python calc.py "2+3*4"` prints `14` and exits 0; invalid expression prints error to stderr and exits non-zero (no traceback).
|
||||
|
||||
**How to verify:**
|
||||
```bash
|
||||
python calc.py "2+3*4" # stdout: 14, exit 0
|
||||
python calc.py "(2+3)*4" # stdout: 20, exit 0
|
||||
python calc.py "7/2" # stdout: 3.5, exit 0
|
||||
python calc.py "4/2" # stdout: 2, exit 0
|
||||
python calc.py "1/0" # stderr: error: ..., exit 1
|
||||
python calc.py "1 +" # stderr: error: ..., exit 1
|
||||
```
|
||||
|
||||
**Expected:** Exact outputs as above. All errors caught by `(LexError, ParseError, EvalError)` — no Python traceback.
|
||||
|
||||
**Where:** `calc.py`
|
||||
|
||||
---
|
||||
|
||||
### D5 — tests green + end-to-end
|
||||
|
||||
**What:** Full unittest suite (lex + parse + eval) passes with 0 failures; CLI checks cover D4.
|
||||
|
||||
**How to verify:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
```
|
||||
Ran 68 tests in ~0.2s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
All 68 tests pass: 24 lex + 22 parse + 22 eval (including 6 CLI subprocess tests).
|
||||
|
||||
**Commit sha:** (see latest commit after push)
|
||||
|
||||
**Where:** `calc/test_lexer.py`, `calc/test_parser.py`, `calc/test_evaluator.py`
|
||||
103
calculators/builder-adversary/run-02/machine-docs/STATUS-lex.md
Normal file
103
calculators/builder-adversary/run-02/machine-docs/STATUS-lex.md
Normal file
@ -0,0 +1,103 @@
|
||||
# STATUS — phase lex (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase complete.
|
||||
|
||||
## Gates
|
||||
|
||||
| Gate | Status |
|
||||
|------|--------|
|
||||
| D1 — numbers | **PASS** (Adversary @2026-06-15T00:36Z) |
|
||||
| D2 — operators & parens | **PASS** (Adversary @2026-06-15T00:36Z) |
|
||||
| D3 — whitespace & errors | **PASS** (Adversary @2026-06-15T00:36Z) |
|
||||
| D4 — tests green | **PASS** (Adversary @2026-06-15T00:36Z) |
|
||||
|
||||
## Post-verification fix
|
||||
|
||||
**AF-01 addressed:** Wrapped `float(raw)` in `try/except ValueError` to re-raise as `LexError` for malformed number literals like `1.2.3`, `.`, `..`. 24 tests still pass.
|
||||
|
||||
---
|
||||
|
||||
## Claim: D1 — numbers
|
||||
|
||||
**WHAT:** `calc/lexer.py::tokenize` correctly tokenizes integers and floats to NUMBER tokens with numeric Python values (int for integers, float for floats). EOF is always the final token.
|
||||
|
||||
**HOW to verify:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; t=tokenize('42'); assert t[0].kind=='NUMBER' and t[0].value==42 and isinstance(t[0].value,int) and t[1].kind=='EOF', t"
|
||||
python -c "from calc.lexer import tokenize; t=tokenize('3.14'); assert t[0].kind=='NUMBER' and abs(t[0].value-3.14)<1e-9 and isinstance(t[0].value,float), t"
|
||||
python -c "from calc.lexer import tokenize; t=tokenize('.5'); assert t[0].value==0.5, t"
|
||||
python -c "from calc.lexer import tokenize; t=tokenize('10.'); assert t[0].value==10.0, t"
|
||||
```
|
||||
|
||||
**EXPECTED:** All assertions pass (exit 0).
|
||||
|
||||
**WHERE:** `calc/lexer.py`
|
||||
|
||||
---
|
||||
|
||||
## Claim: D2 — operators & parens
|
||||
|
||||
**WHAT:** `+`, `-`, `*`, `/`, `(`, `)` each tokenize to PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN respectively. `tokenize("1+2*3")` → NUMBER PLUS NUMBER STAR NUMBER EOF.
|
||||
|
||||
**HOW to verify:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; k=[t.kind for t in tokenize('1+2*3')]; assert k==['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'], k"
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- First command: exit 0 (assertion passes)
|
||||
- Second command prints: `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
|
||||
|
||||
**WHERE:** `calc/lexer.py`
|
||||
|
||||
---
|
||||
|
||||
## Claim: D3 — whitespace & errors
|
||||
|
||||
**WHAT:** Spaces and tabs between tokens are skipped. Invalid characters (letters, `@`, `$`, etc.) raise `LexError` with the offending char and its position in the message.
|
||||
|
||||
**HOW to verify:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; k=[t.kind for t in tokenize(' 12 + 3 ')]; assert k==['NUMBER','PLUS','NUMBER','EOF'], k"
|
||||
python -c "
|
||||
from calc.lexer import tokenize, LexError
|
||||
try:
|
||||
tokenize('1 @ 2')
|
||||
assert False, 'should have raised'
|
||||
except LexError as e:
|
||||
assert '@' in str(e), str(e)
|
||||
assert '2' in str(e), str(e)
|
||||
print('OK')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- First command: exit 0
|
||||
- Second command prints: `OK`
|
||||
|
||||
**WHERE:** `calc/lexer.py`
|
||||
|
||||
---
|
||||
|
||||
## Claim: D4 — tests green
|
||||
|
||||
**WHAT:** `calc/test_lexer.py` passes under `python -m unittest` with 0 failures (24 tests).
|
||||
|
||||
**HOW to verify:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
----------------------------------------------------------------------
|
||||
Ran 24 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
Exit code 0.
|
||||
|
||||
**WHERE:** `calc/test_lexer.py`, `calc/lexer.py`
|
||||
@ -0,0 +1,114 @@
|
||||
# STATUS-parse — Builder
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D6 verified PASS by Adversary @2026-06-15T00:50Z. See REVIEW-parse.md.
|
||||
|
||||
---
|
||||
|
||||
## Gate: D1–D6 CLAIMED → PASS
|
||||
|
||||
All six gates are implemented and verified locally.
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `calc/parser.py` | Recursive-descent parser exposing `parse(tokens) -> Node` |
|
||||
| `calc/test_parser.py` | unittest suite covering D1–D6 |
|
||||
|
||||
---
|
||||
|
||||
## AST Node Types (stable shape for evaluator)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Any # int or float from lexer
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str # '+', '-', '*', '/'
|
||||
left: Any # Node
|
||||
right: Any # Node
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str # '-'
|
||||
operand: Any # Node
|
||||
```
|
||||
|
||||
All three are dataclasses with `__repr__` — equality comparison works via `==`.
|
||||
|
||||
---
|
||||
|
||||
## Verification commands (cold-runnable from any clone)
|
||||
|
||||
```bash
|
||||
# D6 — all tests green
|
||||
python -m unittest -q
|
||||
|
||||
# D1 — precedence: 1+2*3 => BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
|
||||
# expected: BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
|
||||
# D1 — precedence: 2*3+1 => BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('2*3+1')))"
|
||||
# expected: BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
|
||||
|
||||
# D2 — left assoc subtraction: 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-3-2')))"
|
||||
# expected: BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
|
||||
# D2 — left assoc division: 8/4/2 => BinOp('/', BinOp('/', Num(8), Num(4)), Num(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(4)), Num(2))
|
||||
|
||||
# D3 — parens override: (1+2)*3 => BinOp('*', BinOp('+', Num(1), Num(2)), Num(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))
|
||||
|
||||
# D4 — unary minus: -5 => Unary('-', Num(5))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
|
||||
# expected: Unary('-', Num(5))
|
||||
|
||||
# D4 — unary in paren: -(1+2) => Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
|
||||
# expected: Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
|
||||
# D4 — unary in mul: 3 * -2 => BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
|
||||
# expected: BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
|
||||
# D5 — each raises ParseError (must NOT raise any other exception)
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
for s in ['1 +', '(1', '1 2', ')(', '']:
|
||||
try: parse(tokenize(s)); print(f'FAIL no error for {s!r}')
|
||||
except ParseError as e: print(f'OK {s!r} => ParseError: {e}')
|
||||
except Exception as e: print(f'FAIL wrong exc for {s!r}: {type(e).__name__}: {e}')
|
||||
"
|
||||
# expected: 5 lines each starting "OK"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected outputs (exact)
|
||||
|
||||
| Gate | Expression | Expected repr |
|
||||
|---|---|---|
|
||||
| D1 | `1+2*3` | `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` |
|
||||
| D1 | `2*3+1` | `BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))` |
|
||||
| D2 | `8-3-2` | `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` |
|
||||
| D2 | `8/4/2` | `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` |
|
||||
| D3 | `(1+2)*3` | `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` |
|
||||
| D4 | `-5` | `Unary('-', Num(5))` |
|
||||
| D4 | `-(1+2)` | `Unary('-', BinOp('+', Num(1), Num(2)))` |
|
||||
| D4 | `3 * -2` | `BinOp('*', Num(3), Unary('-', Num(2)))` |
|
||||
| D5 | `1 +` | `ParseError: unexpected token 'EOF' (None)` |
|
||||
| D5 | `(1` | `ParseError: expected RPAREN, got 'EOF' (None)` |
|
||||
| D5 | `1 2` | `ParseError: unexpected token 'NUMBER' (2) after expression` |
|
||||
| D5 | `)(` | `ParseError: unexpected token 'RPAREN' (')')` |
|
||||
| D5 | `` | `ParseError: empty expression` |
|
||||
| D6 | `python -m unittest -q` | `Ran 46 tests in …s OK` |
|
||||
3
calculators/builder-adversary/run-03/.gitignore
vendored
Normal file
3
calculators/builder-adversary/run-03/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
17
calculators/builder-adversary/run-03/GIT-LOG.txt
Normal file
17
calculators/builder-adversary/run-03/GIT-LOG.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
984e0b7 status: mark eval phase DONE — all D1–D5 Adversary-verified PASS
|
||||
ebe1987 review(D1,D2,D3,D4,D5): PASS — all eval gates cold-verified from work-adv clone
|
||||
070dc92 claim(D1,D2,D3,D4,D5): eval phase complete — 68 tests green, all CLI checks pass
|
||||
9ffa803 review(eval): initialize Adversary tracking files for eval phase
|
||||
1914b53 status: mark parse phase DONE — all D1–D6 Adversary-verified PASS
|
||||
f444dc8 review(D1,D2,D3,D4,D5,D6): PASS — all parse gates cold-verified from work-adv clone
|
||||
79016f1 claim(D1,D2,D3,D4,D5,D6): parser complete — 44 tests green, all gates verified locally
|
||||
88df238 chore: finalize lex phase status and lexer dot-fix
|
||||
0371b28 review(parse): initialize Adversary tracking files for parse phase
|
||||
19b3673 review(D1,D2,D3,D4): PASS — all gates cold-verified from work-adv clone
|
||||
09bccc4 chore: initialize Adversary machine-docs for lex phase
|
||||
c7cd94e status: mark claim tasks done in backlog, awaiting Adversary review
|
||||
6ee0968 chore: add .gitignore for pycache
|
||||
a0745d4 claim(D1,D2,D3,D4): all gates complete — 18 tests green, lexer verified
|
||||
f67144b feat: implement calc/lexer.py and test_lexer.py (D1-D4 complete)
|
||||
f7a0f44 chore: seed
|
||||
1
calculators/builder-adversary/run-03/README.md
Normal file
1
calculators/builder-adversary/run-03/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary/run-03/SOURCE.txt
Normal file
1
calculators/builder-adversary/run-03/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r3
|
||||
23
calculators/builder-adversary/run-03/calc.py
Normal file
23
calculators/builder-adversary/run-03/calc.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""calc — evaluate an arithmetic expression from the command line."""
|
||||
import sys
|
||||
|
||||
from calc.lexer import LexError, tokenize
|
||||
from calc.parser import ParseError, parse
|
||||
from calc.evaluator import EvalError, evaluate
|
||||
|
||||
|
||||
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 exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
47
calculators/builder-adversary/run-03/calc/evaluator.py
Normal file
47
calculators/builder-adversary/run-03/calc/evaluator.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Evaluator for the arithmetic AST produced by calc.parser.
|
||||
|
||||
Result-type rule: if a computation yields a float that is whole-valued
|
||||
(e.g. 4/2 == 2.0), it is coerced to int before returning. Non-whole
|
||||
floats (e.g. 7/2 == 3.5) are returned as float.
|
||||
"""
|
||||
from .parser import BinOp, Num, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
"""Raised on a runtime evaluation error (e.g. division by zero)."""
|
||||
|
||||
|
||||
def _coerce(value):
|
||||
if isinstance(value, float) and value == int(value):
|
||||
return int(value)
|
||||
return value
|
||||
|
||||
|
||||
def evaluate(node):
|
||||
"""Walk an AST node and return int | float.
|
||||
|
||||
Raises:
|
||||
EvalError: on division by zero.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return _coerce(node.value)
|
||||
|
||||
if isinstance(node, Unary):
|
||||
return _coerce(-evaluate(node.operand))
|
||||
|
||||
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 node type: {type(node).__name__}")
|
||||
64
calculators/builder-adversary/run-03/calc/lexer.py
Normal file
64
calculators/builder-adversary/run-03/calc/lexer.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""Lexer for arithmetic expressions."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
"""Raised when the lexer encounters an invalid character."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: Union[int, float, None] = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.value is None:
|
||||
return self.kind
|
||||
return f"{self.kind}({self.value})"
|
||||
|
||||
|
||||
_SINGLE = {
|
||||
'+': 'PLUS',
|
||||
'-': 'MINUS',
|
||||
'*': 'STAR',
|
||||
'/': 'SLASH',
|
||||
'(': 'LPAREN',
|
||||
')': 'RPAREN',
|
||||
}
|
||||
|
||||
|
||||
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]))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
has_dot = False
|
||||
while j < len(src) and (src[j].isdigit() or (src[j] == '.' and not has_dot)):
|
||||
if src[j] == '.':
|
||||
has_dot = True
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
if raw == '.':
|
||||
raise LexError(f"invalid character '.' at position {i}")
|
||||
value = float(raw) if has_dot else int(raw)
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
continue
|
||||
|
||||
raise LexError(f"invalid character {ch!r} at position {i}")
|
||||
|
||||
tokens.append(Token('EOF'))
|
||||
return tokens
|
||||
123
calculators/builder-adversary/run-03/calc/parser.py
Normal file
123
calculators/builder-adversary/run-03/calc/parser.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""Recursive-descent parser for arithmetic expressions.
|
||||
|
||||
AST nodes:
|
||||
Num(value) — numeric literal
|
||||
BinOp(op, left, right) — binary op; op in {'+', '-', '*', '/'}
|
||||
Unary(op, operand) — unary op; op == '-'
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
from .lexer import Token
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
"""Raised on malformed input."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
def __repr__(self):
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: object
|
||||
right: object
|
||||
|
||||
def __repr__(self):
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: object
|
||||
|
||||
def __repr__(self):
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
|
||||
_KIND_TO_OP = {
|
||||
'PLUS': '+',
|
||||
'MINUS': '-',
|
||||
'STAR': '*',
|
||||
'SLASH': '/',
|
||||
}
|
||||
|
||||
|
||||
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 is not None and tok.kind != kind:
|
||||
raise ParseError(f"expected {kind!r}, got {tok.kind!r}")
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
def parse(self):
|
||||
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}")
|
||||
return node
|
||||
|
||||
def _expr(self):
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op = _KIND_TO_OP[self._consume().kind]
|
||||
right = self._term()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _term(self):
|
||||
node = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op = _KIND_TO_OP[self._consume().kind]
|
||||
right = self._unary()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _unary(self):
|
||||
if self._peek().kind == 'MINUS':
|
||||
self._consume()
|
||||
return Unary('-', self._unary())
|
||||
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("unclosed parenthesis, expected ')'")
|
||||
self._consume()
|
||||
return node
|
||||
if tok.kind == 'EOF':
|
||||
raise ParseError("unexpected end of input")
|
||||
raise ParseError(f"unexpected token {tok.kind!r}")
|
||||
|
||||
|
||||
def parse(tokens: list):
|
||||
"""Parse a token list into an AST.
|
||||
|
||||
Returns:
|
||||
Num | BinOp | Unary — root node.
|
||||
|
||||
Raises:
|
||||
ParseError: on any malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
140
calculators/builder-adversary/run-03/calc/test_evaluator.py
Normal file
140
calculators/builder-adversary/run-03/calc/test_evaluator.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""Tests for calc.evaluator — covers eval/D1 through eval/D4 (CLI)."""
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from .lexer import tokenize
|
||||
from .parser import parse
|
||||
from .evaluator import evaluate, EvalError
|
||||
|
||||
CLI = str(pathlib.Path(__file__).parent.parent / "calc.py")
|
||||
|
||||
|
||||
def ev(src):
|
||||
return evaluate(parse(tokenize(src)))
|
||||
|
||||
|
||||
class TestArithmetic(unittest.TestCase):
|
||||
"""D1 — correct arithmetic with precedence, parens, unary minus."""
|
||||
|
||||
def test_add_mul_precedence(self):
|
||||
self.assertEqual(ev("2+3*4"), 14)
|
||||
|
||||
def test_paren_overrides(self):
|
||||
self.assertEqual(ev("(2+3)*4"), 20)
|
||||
|
||||
def test_left_associative_sub(self):
|
||||
self.assertEqual(ev("8-3-2"), 3)
|
||||
|
||||
def test_unary_leading(self):
|
||||
self.assertEqual(ev("-2+5"), 3)
|
||||
|
||||
def test_unary_after_mul(self):
|
||||
self.assertEqual(ev("2*-3"), -6)
|
||||
|
||||
def test_simple_add(self):
|
||||
self.assertEqual(ev("1+2"), 3)
|
||||
|
||||
def test_simple_sub(self):
|
||||
self.assertEqual(ev("5-3"), 2)
|
||||
|
||||
def test_double_unary(self):
|
||||
self.assertEqual(ev("--5"), 5)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(ev("((3+2))*4"), 20)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
"""D2 — true division and EvalError on division by zero."""
|
||||
|
||||
def test_true_division(self):
|
||||
self.assertEqual(ev("7/2"), 3.5)
|
||||
|
||||
def test_division_by_zero(self):
|
||||
with self.assertRaises(EvalError):
|
||||
ev("1/0")
|
||||
|
||||
def test_division_by_zero_expr(self):
|
||||
with self.assertRaises(EvalError):
|
||||
ev("5/(3-3)")
|
||||
|
||||
def test_not_bare_zerodiv(self):
|
||||
try:
|
||||
ev("1/0")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("bare ZeroDivisionError escaped; expected EvalError")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
"""D3 — whole-valued results are int; non-whole are float."""
|
||||
|
||||
def test_whole_division_is_int(self):
|
||||
result = ev("4/2")
|
||||
self.assertEqual(result, 2)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_non_whole_division_is_float(self):
|
||||
result = ev("7/2")
|
||||
self.assertEqual(result, 3.5)
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_integer_arithmetic_stays_int(self):
|
||||
result = ev("2+3*4")
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_whole_float_literal(self):
|
||||
result = ev("4.0/2")
|
||||
self.assertIsInstance(result, int)
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
|
||||
class TestCLI(unittest.TestCase):
|
||||
"""D4 — CLI prints correct output, exits correctly, errors to stderr."""
|
||||
|
||||
def _run(self, expr):
|
||||
return subprocess.run(
|
||||
[sys.executable, CLI, expr],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
|
||||
def test_precedence(self):
|
||||
r = self._run("2+3*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "14")
|
||||
|
||||
def test_parens(self):
|
||||
r = self._run("(2+3)*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "20")
|
||||
|
||||
def test_true_division(self):
|
||||
r = self._run("7/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "3.5")
|
||||
|
||||
def test_whole_division(self):
|
||||
r = self._run("4/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "2")
|
||||
|
||||
def test_invalid_expr_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(), "expected error on stderr")
|
||||
|
||||
def test_no_traceback_on_error(self):
|
||||
r = self._run("1 +")
|
||||
self.assertNotIn("Traceback", r.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
111
calculators/builder-adversary/run-03/calc/test_lexer.py
Normal file
111
calculators/builder-adversary/run-03/calc/test_lexer.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""Tests for calc/lexer.py — covers D1, D2, D3."""
|
||||
import unittest
|
||||
from calc.lexer import tokenize, Token, LexError
|
||||
|
||||
|
||||
def kinds(src):
|
||||
return [t.kind for t in tokenize(src)]
|
||||
|
||||
|
||||
def vals(src):
|
||||
return [(t.kind, t.value) for t in tokenize(src)]
|
||||
|
||||
|
||||
class TestNumbers(unittest.TestCase):
|
||||
"""D1 — integers and floats tokenize to NUMBER with numeric value."""
|
||||
|
||||
def test_integer(self):
|
||||
toks = tokenize("42")
|
||||
self.assertEqual(len(toks), 2)
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertEqual(toks[0].value, 42)
|
||||
self.assertIsInstance(toks[0].value, int)
|
||||
self.assertEqual(toks[1].kind, 'EOF')
|
||||
|
||||
def test_float_standard(self):
|
||||
toks = tokenize("3.14")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 3.14)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_leading_dot(self):
|
||||
toks = tokenize(".5")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 0.5)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_trailing_dot(self):
|
||||
toks = tokenize("10.")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 10.0)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
"""D2 — operators and parens produce correct kinds."""
|
||||
|
||||
def test_plus(self):
|
||||
self.assertIn('PLUS', kinds('+'))
|
||||
|
||||
def test_minus(self):
|
||||
self.assertIn('MINUS', kinds('-'))
|
||||
|
||||
def test_star(self):
|
||||
self.assertIn('STAR', kinds('*'))
|
||||
|
||||
def test_slash(self):
|
||||
self.assertIn('SLASH', kinds('/'))
|
||||
|
||||
def test_lparen(self):
|
||||
self.assertIn('LPAREN', kinds('('))
|
||||
|
||||
def test_rparen(self):
|
||||
self.assertIn('RPAREN', kinds(')'))
|
||||
|
||||
def test_expression_1plus2star3(self):
|
||||
self.assertEqual(
|
||||
kinds("1+2*3"),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
"""D3 — whitespace skipped; invalid chars raise LexError."""
|
||||
|
||||
def test_whitespace_skipped(self):
|
||||
self.assertEqual(
|
||||
kinds(" 12 + 3 "),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
def test_tab_skipped(self):
|
||||
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_complex_expression(self):
|
||||
self.assertEqual(
|
||||
kinds("3.5*(1-2)"),
|
||||
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'],
|
||||
)
|
||||
|
||||
def test_invalid_at_sign(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_invalid_dollar(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$")
|
||||
|
||||
def test_invalid_letter(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("x")
|
||||
|
||||
def test_error_includes_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
# position 2 (0-indexed) is where '@' lives
|
||||
self.assertIn('2', str(ctx.exception))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
134
calculators/builder-adversary/run-03/calc/test_parser.py
Normal file
134
calculators/builder-adversary/run-03/calc/test_parser.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""Tests for calc.parser — covers D1 through D5."""
|
||||
import unittest
|
||||
from .lexer import tokenize
|
||||
from .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_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_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_sub_div(self):
|
||||
# 10-4/2 → BinOp('-', Num(10), BinOp('/', Num(4), Num(2)))
|
||||
self.assertEqual(p("10-4/2"), BinOp('-', Num(10), BinOp('/', Num(4), Num(2))))
|
||||
|
||||
def test_single_number(self):
|
||||
self.assertEqual(p("42"), Num(42))
|
||||
|
||||
def test_single_float(self):
|
||||
self.assertEqual(p("3.5"), Num(3.5))
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
"""D2 — same-precedence operators associate left."""
|
||||
|
||||
def test_sub_left(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(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(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(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_paren_overrides_mul(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_paren_overrides_div(self):
|
||||
# 6/(1+2) → BinOp('/', Num(6), BinOp('+', Num(1), Num(2)))
|
||||
self.assertEqual(p("6/(1+2)"), BinOp('/', Num(6), BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_nested_parens(self):
|
||||
# ((2+3)) → BinOp('+', Num(2), Num(3))
|
||||
self.assertEqual(p("((2+3))"), BinOp('+', Num(2), Num(3)))
|
||||
|
||||
def test_paren_single(self):
|
||||
self.assertEqual(p("(5)"), Num(5))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
"""D4 — unary minus."""
|
||||
|
||||
def test_leading(self):
|
||||
self.assertEqual(p("-5"), Unary('-', Num(5)))
|
||||
|
||||
def test_paren_group(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))))
|
||||
|
||||
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):
|
||||
"""D5 — malformed input raises ParseError."""
|
||||
|
||||
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_numbers(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("1 2")
|
||||
|
||||
def test_close_then_open(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p(")(")
|
||||
|
||||
def test_empty(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("")
|
||||
|
||||
def test_only_op(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("+")
|
||||
|
||||
def test_mismatched_parens(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("(1+2")
|
||||
|
||||
def test_parse_error_not_other(self):
|
||||
for src in ("1 +", "(1", "1 2", ")(", ""):
|
||||
with self.subTest(src=src):
|
||||
with self.assertRaises(ParseError):
|
||||
p(src)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@ -0,0 +1,12 @@
|
||||
# BACKLOG-eval
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 arithmetic — `evaluate()` correct for +/-/*//, precedence, parens, unary minus
|
||||
- [x] D2 division — true division; `EvalError` on zero; not bare `ZeroDivisionError`
|
||||
- [x] D3 result type — whole floats → int; non-whole → float; rule documented in evaluator.py
|
||||
- [x] D4 CLI — `calc.py` at work root; stdout+exit-0 on success; stderr+exit-1 on error; no traceback
|
||||
- [x] D5 tests — 24 new tests in `calc/test_evaluator.py`; 68 total pass; prior suite unaffected
|
||||
|
||||
## Adversary findings
|
||||
_(none yet)_
|
||||
@ -0,0 +1,21 @@
|
||||
# BACKLOG-lex
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/ package with __init__.py
|
||||
- [x] Implement calc/lexer.py (Token, LexError, tokenize)
|
||||
- [x] Implement calc/test_lexer.py covering D1-D3
|
||||
- [x] Claim D1 (committed in a0745d4)
|
||||
- [x] Claim D2 (committed in a0745d4)
|
||||
- [x] Claim D3 (committed in a0745d4)
|
||||
- [x] Claim D4 (committed in a0745d4)
|
||||
- [x] Await Adversary REVIEW-lex.md PASS for all gates
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### AF-1 — Missing test file (2026-06-15T00:58Z) — RESOLVED
|
||||
Initially observed calc/test_lexer.py missing from Builder's untracked files.
|
||||
Builder subsequently committed and pushed the file. **Status: CLOSED** (resolved in commit f67144b)
|
||||
|
||||
### AF-2 — Gates not yet committed to origin (2026-06-15T00:58Z) — RESOLVED
|
||||
Builder pushed all gates in commit a0745d4. **Status: CLOSED**
|
||||
@ -0,0 +1,9 @@
|
||||
# BACKLOG-parse
|
||||
|
||||
## Build backlog
|
||||
|
||||
_This section is owned by the Builder. Adversary does not edit here._
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_No findings yet._
|
||||
@ -0,0 +1,11 @@
|
||||
# DECISIONS (append-only, shared)
|
||||
|
||||
## D-001: Token representation
|
||||
Token is a dataclass with `kind: str` and `value` (int | float | None).
|
||||
NUMBER tokens carry their parsed numeric value; operator/paren tokens carry None.
|
||||
EOF carries None value.
|
||||
Rationale: parser phases can pattern-match on kind and directly use value without re-parsing.
|
||||
|
||||
## D-002: Integer vs float distinction
|
||||
If a number string contains '.' it is parsed as float, otherwise int.
|
||||
Edge cases: '.5' → float(0.5), '10.' → float(10.0), '42' → int(42).
|
||||
@ -0,0 +1,47 @@
|
||||
# JOURNAL-eval — Builder
|
||||
|
||||
## Build log
|
||||
|
||||
### Approach
|
||||
|
||||
AST walker in `calc/evaluator.py`:
|
||||
- `Num` → return `_coerce(node.value)`
|
||||
- `Unary('-', ...)` → `_coerce(-evaluate(operand))`
|
||||
- `BinOp` → evaluate both sides; for `/`, check `right == 0` before dividing; apply `_coerce` to result
|
||||
|
||||
`_coerce(value)`: if `isinstance(value, float) and value == int(value)` → `int(value)`, else pass-through.
|
||||
This keeps the API return clean (no `2.0` leaking out) and is applied consistently at every node evaluation site.
|
||||
|
||||
### Test run (local)
|
||||
|
||||
```
|
||||
python -m unittest -v 2>&1
|
||||
...
|
||||
Ran 68 tests in 0.270s
|
||||
OK
|
||||
```
|
||||
|
||||
All 68 tests pass:
|
||||
- 18 lexer tests (unchanged)
|
||||
- 26 parser tests (unchanged)
|
||||
- 24 evaluator + CLI tests (new)
|
||||
|
||||
### CLI spot-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 "1/0" → error: division by zero (stderr, exit 1)
|
||||
python calc.py "1 +" → error: unexpected end of input (stderr, exit 1)
|
||||
```
|
||||
|
||||
### D3 rule rationale
|
||||
|
||||
Python `/` always returns `float`. Applying `_coerce` at every evaluate site means:
|
||||
- `4/2` → `2.0` → `int(2)` = `2`
|
||||
- `7/2` → `3.5` (not whole → stays float)
|
||||
- `2+3` → `5` (int arithmetic → already int, _coerce is a no-op)
|
||||
|
||||
This is documented in `calc/evaluator.py` module docstring.
|
||||
@ -0,0 +1,34 @@
|
||||
# JOURNAL-lex
|
||||
|
||||
## Session 1
|
||||
|
||||
Starting implementation of calc/lexer.py per lex.md plan.
|
||||
|
||||
Design choices:
|
||||
- Token is a dataclass with `kind: str` and `value` (str | int | float | None)
|
||||
- NUMBER tokens carry numeric value (int for integers, float for floats)
|
||||
- All other tokens carry None value
|
||||
- LexError subclasses Exception, message includes offending char and position
|
||||
|
||||
## Implementation results
|
||||
|
||||
Ran tests:
|
||||
```
|
||||
$ python -m unittest -q
|
||||
....................
|
||||
Ran 18 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
|
||||
Verification commands from plan:
|
||||
```
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
calc.lexer.LexError: invalid character '@' at position 2
|
||||
```
|
||||
|
||||
All DoD items satisfied. Claiming all gates D1-D4 together.
|
||||
@ -0,0 +1,73 @@
|
||||
# JOURNAL-parse
|
||||
|
||||
## 2026-06-15 — Initial implementation
|
||||
|
||||
### Design choices
|
||||
|
||||
Grammar used:
|
||||
```
|
||||
expr = term (('+' | '-') term)*
|
||||
term = unary (('*' | '/') unary)*
|
||||
unary = '-' unary | primary
|
||||
primary = NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
This naturally encodes precedence (* and / via term, + and - via expr) and left-associativity (via the while loop that builds left-deep trees in _expr and _term). Unary minus is right-recursive via _unary → _unary, which handles chaining (--5) correctly.
|
||||
|
||||
### Operator representation
|
||||
|
||||
The Adversary's pre-claim probes in REVIEW-parse.md used symbol format ('+', '-', '*', '/') rather than token kind names ('PLUS', 'MINUS', etc.). I aligned the implementation to use symbols to match their expected cold-verification output.
|
||||
|
||||
### Test run output
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 44 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
### D1 shape verification
|
||||
|
||||
```
|
||||
$ 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 shape verification
|
||||
|
||||
```
|
||||
$ 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 shape verification
|
||||
|
||||
```
|
||||
$ 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 shape verification
|
||||
|
||||
```
|
||||
$ 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 error verification
|
||||
|
||||
All five required error cases raise ParseError specifically:
|
||||
- "1 +" → ParseError: unexpected end of input
|
||||
- "(1" → ParseError: unclosed parenthesis, expected ')'
|
||||
- "1 2" → ParseError: unexpected token 'NUMBER'
|
||||
- ")(" → ParseError: unexpected token 'RPAREN'
|
||||
- "" → ParseError: empty expression
|
||||
@ -0,0 +1,65 @@
|
||||
# REVIEW-eval — Adversary Verdicts
|
||||
|
||||
Phase: eval
|
||||
Plan SSOT: /home/loops/project-orchestrator/projects/agent-orchestrator-benchmark/plans/calc/eval.md
|
||||
|
||||
## Gates
|
||||
|
||||
- D1 — arithmetic: PASS @2026-06-15T01:12:53Z
|
||||
- D2 — division / EvalError: PASS @2026-06-15T01:12:53Z
|
||||
- D3 — result type (no trailing .0): PASS @2026-06-15T01:12:53Z
|
||||
- D4 — CLI: PASS @2026-06-15T01:12:53Z
|
||||
- D5 — tests green + end-to-end: PASS @2026-06-15T01:12:53Z
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1 — arithmetic: PASS @2026-06-15T01:12:53Z
|
||||
|
||||
Cold-verified from work-adv clone (commit after pull: 070dc92).
|
||||
|
||||
Evidence (all outputs match expected):
|
||||
- `python calc.py "2+3*4"` → `14` exit 0 ✓
|
||||
- `python calc.py "(2+3)*4"` → `20` exit 0 ✓
|
||||
- `python calc.py "8-3-2"` → `3` exit 0 ✓
|
||||
- `python calc.py "-2+5"` → `3` exit 0 ✓
|
||||
- `python calc.py "2*-3"` → `-6` exit 0 ✓
|
||||
- `python calc.py "--5"` → `5` exit 0 ✓ (double unary)
|
||||
- `python calc.py "3-3"` → `0` exit 0 ✓
|
||||
|
||||
### D2 — division / EvalError: PASS @2026-06-15T01:12:53Z
|
||||
|
||||
Evidence:
|
||||
- `python calc.py "7/2"` → `3.5` exit 0 ✓ (true division)
|
||||
- `1/0` raises `EvalError("division by zero")`, NOT bare `ZeroDivisionError` ✓
|
||||
- `5/(3-3)` also raises `EvalError` ✓
|
||||
|
||||
### D3 — result type: PASS @2026-06-15T01:12:53Z
|
||||
|
||||
Evidence (types confirmed via Python `isinstance` check):
|
||||
- `4/2` → `int(2)` (not `float(2.0)`) ✓
|
||||
- `7/2` → `float(3.5)` ✓
|
||||
- `2+3*4` → `int(14)` ✓
|
||||
- `0.0/1` → `int(0)` (whole-float coercion works for zero) ✓
|
||||
- `1.5+1.5` → `3` exit 0 (coerces 3.0 → int) ✓
|
||||
- Rule documented in evaluator.py docstring ✓
|
||||
|
||||
### D4 — CLI: PASS @2026-06-15T01:12:53Z
|
||||
|
||||
Evidence:
|
||||
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
|
||||
- `python calc.py "1 +"` → stderr error, exit 1, no "Traceback" ✓
|
||||
- `python calc.py "1/0"` → stderr error, exit 1, no "Traceback" ✓
|
||||
- `python calc.py` (no args) → stderr usage msg, exit 1 ✓
|
||||
- Error output confirmed routed to stderr (stdout suppressed, still exits 1) ✓
|
||||
|
||||
### D5 — tests green + end-to-end: PASS @2026-06-15T01:12:53Z
|
||||
|
||||
Evidence:
|
||||
- `python -m unittest -q` → `Ran 68 tests in ...s` / `OK` ✓
|
||||
- Breakdown: 18 lex + 26 parse + 24 eval = 68 total ✓
|
||||
- Prior 44 tests (lex + parse) still pass — no regression ✓
|
||||
- `python -m unittest calc.test_lexer calc.test_parser -q` → 44 tests OK ✓
|
||||
|
||||
## Adversary findings
|
||||
|
||||
None. No defects found. No VETO.
|
||||
@ -0,0 +1,53 @@
|
||||
# REVIEW-lex — Adversary Verdicts
|
||||
|
||||
## Gate Verdicts (cold-verified from work-adv clone, commit a0745d4)
|
||||
|
||||
### lex/D1: PASS @2026-06-15T01:00Z
|
||||
|
||||
Cold run from work-adv clone:
|
||||
- `tokenize("42")` → `[NUMBER(42), EOF]`, value is `int(42)` ✓
|
||||
- `tokenize("3.14")` → `[NUMBER(3.14), EOF]`, value is `float` ✓
|
||||
- `tokenize(".5")` → `[NUMBER(0.5), EOF]`, value is `float` ✓
|
||||
- `tokenize("10.")` → `[NUMBER(10.0), EOF]`, value is `float` ✓
|
||||
|
||||
### lex/D2: PASS @2026-06-15T01:00Z
|
||||
|
||||
Cold run:
|
||||
- `tokenize("1+2*3")` → `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']` ✓
|
||||
- All six operators/parens (`+`,`-`,`*`,`/`,`(`,`)`) produce correct kinds ✓
|
||||
|
||||
### lex/D3: PASS @2026-06-15T01:00Z
|
||||
|
||||
Cold run:
|
||||
- `tokenize(" 12 + 3 ")` → `['NUMBER', 'PLUS', 'NUMBER', 'EOF']` ✓
|
||||
- `tokenize("1 @ 2")` → raises `LexError: invalid character '@' at position 2` ✓
|
||||
- Message contains offending char (`@`) and position (`2`) ✓
|
||||
- Letters raise LexError ✓
|
||||
|
||||
### lex/D4: PASS @2026-06-15T01:00Z
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
..................
|
||||
Ran 18 tests in 0.000s
|
||||
|
||||
OK
|
||||
```
|
||||
18 tests, 0 failures, exit 0 ✓
|
||||
Covers D1–D3 including `" 12 + 3 "`, `"3.5*(1-2)"`, and `"1 @ 2"` raises LexError ✓
|
||||
|
||||
## Minor out-of-spec finding (does NOT affect DoD)
|
||||
|
||||
**Solo dot (`.`) raises `ValueError` instead of `LexError`.**
|
||||
`tokenize(".")` crashes with `ValueError: could not convert string to float: '.'`
|
||||
The plan specifies `.5` (dot + digit) as valid; bare `.` is undefined in the spec.
|
||||
Not a DoD failure — filing as informational for future phases.
|
||||
|
||||
## Pre-claim probes (noted before Builder pushed)
|
||||
|
||||
Initially found test file missing from local untracked files; Builder then committed and pushed
|
||||
the complete implementation. Both issues AF-1 and AF-2 from BACKLOG are now closed.
|
||||
|
||||
## Summary
|
||||
|
||||
All four gates D1–D4 verified PASS from cold start in work-adv clone at commit a0745d4.
|
||||
@ -0,0 +1,73 @@
|
||||
# REVIEW-parse — Adversary Verdicts
|
||||
|
||||
## Gate Verdicts (cold-verified from work-adv clone, commit 79016f1)
|
||||
|
||||
### parse/D1: PASS @2026-06-15T01:15Z
|
||||
|
||||
Cold run — precedence verified structurally:
|
||||
- `parse(tokenize('1+2*3'))` → `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` ✓ (`*` deeper than `+`)
|
||||
- `parse(tokenize('2*3+1'))` → `BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))` ✓
|
||||
- `parse(tokenize('10-4/2'))` → `BinOp('-', Num(10), BinOp('/', Num(4), Num(2)))` ✓
|
||||
- Extra: `1+2*3+4` → `BinOp('+', BinOp('+', Num(1), BinOp('*', Num(2), Num(3))), Num(4))` ✓
|
||||
|
||||
### parse/D2: PASS @2026-06-15T01:15Z
|
||||
|
||||
Cold run — left associativity verified structurally:
|
||||
- `parse(tokenize('8-3-2'))` → `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` ✓ (first `-` is left child)
|
||||
- `parse(tokenize('8/4/2'))` → `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` ✓
|
||||
- Extra: `1+2+3` → `BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))` ✓
|
||||
|
||||
### parse/D3: PASS @2026-06-15T01:15Z
|
||||
|
||||
Cold run — parens override precedence:
|
||||
- `parse(tokenize('(1+2)*3'))` → `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` ✓ (`+` deeper than `*`)
|
||||
- Extra nested: `((((1))))` → `Num(1)` ✓
|
||||
- Extra mixed: `2*(3+4*5)` → `BinOp('*', Num(2), BinOp('+', Num(3), BinOp('*', Num(4), Num(5))))` ✓
|
||||
|
||||
### parse/D4: PASS @2026-06-15T01:15Z
|
||||
|
||||
Cold run — unary minus:
|
||||
- `parse(tokenize('-5'))` → `Unary('-', Num(5))` ✓
|
||||
- `parse(tokenize('-(1+2)'))` → `Unary('-', BinOp('+', Num(1), Num(2)))` ✓
|
||||
- `parse(tokenize('3 * -2'))` → `BinOp('*', Num(3), Unary('-', Num(2)))` ✓
|
||||
- Extra: `--5` → `Unary('-', Unary('-', Num(5)))` ✓ (right-recursive unary)
|
||||
- Extra: `-1+-2` → `BinOp('+', Unary('-', Num(1)), Unary('-', Num(2)))` ✓
|
||||
- Extra: `-3*2` → `BinOp('*', Unary('-', Num(3)), Num(2))` ✓ (unary higher-prec than `*`)
|
||||
- Extra: `3--2` → `BinOp('-', Num(3), Unary('-', Num(2)))` ✓
|
||||
|
||||
### parse/D5: PASS @2026-06-15T01:15Z
|
||||
|
||||
Cold run — all 5 plan-mandated cases raise `ParseError` (not any other exception type):
|
||||
- `"1 +"` → `ParseError: unexpected end of input` ✓
|
||||
- `"(1"` → `ParseError: unclosed parenthesis, expected ')'` ✓
|
||||
- `"1 2"` → `ParseError: unexpected token 'NUMBER'` ✓
|
||||
- `")("` → `ParseError: unexpected token 'RPAREN'` ✓
|
||||
- `""` → `ParseError: empty expression` ✓
|
||||
|
||||
Extra adversarial cases also raise `ParseError` correctly:
|
||||
- `"*2"`, `"/2"`, `"()"`, `"1+"`, `"1/"` — all `ParseError` ✓
|
||||
|
||||
### parse/D6: PASS @2026-06-15T01:15Z
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
............................................
|
||||
Ran 44 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
44 tests (18 lexer + 26 parser), 0 failures, exit 0 ✓
|
||||
|
||||
Tests assert structural equality using dataclass `__eq__` — not weak string comparison. Covers all D1-D5 cases including boundary and combinatorial inputs.
|
||||
|
||||
## Adversary Findings
|
||||
|
||||
No findings. Implementation is clean.
|
||||
|
||||
- `ParseError` is a proper subclass of `Exception` (not `SyntaxError` or other built-in) ✓
|
||||
- AST nodes use operator symbols (`'+'`, `'-'`, etc.) not token kind names ✓
|
||||
- Stable documented shape: `Num(value)`, `BinOp(op, left, right)`, `Unary(op, operand)` ✓
|
||||
|
||||
## Summary
|
||||
|
||||
All six gates D1–D6 verified PASS from cold start in work-adv clone at commit 79016f1.
|
||||
@ -0,0 +1,64 @@
|
||||
# STATUS-eval — Builder
|
||||
|
||||
## DONE
|
||||
|
||||
All five eval gates D1–D5 Adversary-verified PASS @2026-06-15T01:12:53Z (commit 070dc92). No findings, no VETO. This is the last phase — sequence complete.
|
||||
|
||||
---
|
||||
|
||||
## Gate: D1–D5 CLAIMED (closed — all PASS)
|
||||
|
||||
Commit: see `git log --oneline -1` after push
|
||||
|
||||
### What is claimed
|
||||
|
||||
All five eval phase gates (D1–D5):
|
||||
|
||||
- **D1** arithmetic — correct results for `+`, `-`, `*`, `/`, precedence, parens, unary minus
|
||||
- **D2** division — true division; `EvalError` (not `ZeroDivisionError`) on divide-by-zero
|
||||
- **D3** result type — whole-valued floats returned as `int`; non-whole as `float`
|
||||
- **D4** CLI — `calc.py` prints result to stdout/exit-0 on success; error to stderr/exit-1 on failure; no traceback
|
||||
- **D5** tests green — 68 tests pass (18 lex + 26 parse + 24 eval), 0 failures; CLI checks included
|
||||
|
||||
### How to verify (exact commands, run from work-adv clone root)
|
||||
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
Expected: `Ran 68 tests in ...s` / `OK` / exit 0
|
||||
|
||||
```bash
|
||||
python calc.py "2+3*4"
|
||||
```
|
||||
Expected stdout: `14` / exit 0
|
||||
|
||||
```bash
|
||||
python calc.py "(2+3)*4"
|
||||
```
|
||||
Expected stdout: `20` / exit 0
|
||||
|
||||
```bash
|
||||
python calc.py "7/2"
|
||||
```
|
||||
Expected stdout: `3.5` / exit 0
|
||||
|
||||
```bash
|
||||
python calc.py "4/2"
|
||||
```
|
||||
Expected stdout: `2` / exit 0
|
||||
|
||||
```bash
|
||||
python calc.py "1/0"
|
||||
```
|
||||
Expected: error message on stderr / exit non-zero / no traceback
|
||||
|
||||
```bash
|
||||
python calc.py "1 +"
|
||||
```
|
||||
Expected: error message on stderr / exit non-zero / no traceback
|
||||
|
||||
### Where
|
||||
|
||||
- `calc/evaluator.py` — `EvalError`, `evaluate(node) -> int | float`
|
||||
- `calc/test_evaluator.py` — 24 new unittest tests covering D1–D4
|
||||
- `calc.py` — top-level CLI (work root)
|
||||
@ -0,0 +1,48 @@
|
||||
# STATUS-lex
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D4 verified PASS by Adversary at 2026-06-15T01:00Z (commit a0745d4).
|
||||
|
||||
## Gates
|
||||
- D1 — numbers: PASS (Adversary verified)
|
||||
- D2 — operators & parens: PASS (Adversary verified)
|
||||
- D3 — whitespace & errors: PASS (Adversary verified)
|
||||
- D4 — tests green: PASS (Adversary verified)
|
||||
|
||||
## Commit
|
||||
SHA: f67144b
|
||||
Files: calc/lexer.py, calc/test_lexer.py, calc/__init__.py
|
||||
|
||||
## Verification commands (re-run from a fresh clone)
|
||||
|
||||
```bash
|
||||
# D4 — all tests pass
|
||||
python -m unittest -q
|
||||
# Expected: 18 tests, 0 failures, exit 0
|
||||
|
||||
# D2 — operator/paren sequence
|
||||
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
|
||||
# Expected: ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
|
||||
|
||||
# D1 + D2 + D3 combined
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
# Expected: [('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]
|
||||
|
||||
# D3 — LexError on invalid char
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
# Expected: raises calc.lexer.LexError: invalid character '@' at position 2
|
||||
```
|
||||
|
||||
## What is claimed (DoD items)
|
||||
|
||||
- **D1**: Integers and floats → NUMBER token with numeric value (int or float).
|
||||
- `tokenize("42")` → `[NUMBER(42), EOF]` (int)
|
||||
- `tokenize("3.14")` → `[NUMBER(3.14), EOF]` (float)
|
||||
- `tokenize(".5")` → `[NUMBER(0.5), EOF]` (float)
|
||||
- `tokenize("10.")` → `[NUMBER(10.0), EOF]` (float)
|
||||
- **D2**: `+ - * / ( )` tokenize to `PLUS MINUS STAR SLASH LPAREN RPAREN`; `tokenize("1+2*3")` → `NUMBER PLUS NUMBER STAR NUMBER EOF`.
|
||||
- **D3**: Spaces/tabs skipped; invalid chars raise `LexError` with offending char and position in message.
|
||||
- `tokenize(" 12 + 3 ")` → `NUMBER PLUS NUMBER EOF`
|
||||
- `tokenize("1 @ 2")` raises `LexError: invalid character '@' at position 2`
|
||||
- **D4**: `python -m unittest -q` → 18 tests, 0 failures.
|
||||
@ -0,0 +1,113 @@
|
||||
# STATUS-parse
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D6 verified PASS by Adversary at 2026-06-15T01:15Z (commit 79016f1).
|
||||
|
||||
## Gates
|
||||
- D1 — precedence: PASS (Adversary verified)
|
||||
- D2 — left associativity: PASS (Adversary verified)
|
||||
- D3 — parentheses: PASS (Adversary verified)
|
||||
- D4 — unary minus: PASS (Adversary verified)
|
||||
- D5 — errors: PASS (Adversary verified)
|
||||
- D6 — tests green: PASS (Adversary verified)
|
||||
|
||||
## Commit
|
||||
SHA: 88df238
|
||||
Files: calc/parser.py, calc/test_parser.py
|
||||
|
||||
## What is claimed (DoD items)
|
||||
|
||||
### AST node shapes
|
||||
All nodes use operator symbols (not token kind names):
|
||||
- `Num(value)` — numeric literal, value is int or float
|
||||
- `BinOp(op, left, right)` — binary op; op ∈ {'+', '-', '*', '/'}
|
||||
- `Unary(op, operand)` — unary op; op == '-'
|
||||
|
||||
### D1 — precedence
|
||||
`*` and `/` bind tighter than `+` and `-`.
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
|
||||
# Expected: BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('2*3+1')))"
|
||||
# Expected: BinOp('+', BinOp('*', Num(2), Num(3)), Num(1))
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('10-4/2')))"
|
||||
# Expected: BinOp('-', Num(10), BinOp('/', Num(4), Num(2)))
|
||||
```
|
||||
|
||||
### D2 — left associativity
|
||||
Same-precedence operators associate left (left child is the deeper node).
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
|
||||
# Expected: BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
|
||||
# Expected: BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
```
|
||||
|
||||
### D3 — parentheses
|
||||
Parens override default precedence.
|
||||
|
||||
```bash
|
||||
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))
|
||||
# Note: '+' node is DEEPER (left child) of '*' — opposite of D1's case
|
||||
```
|
||||
|
||||
### D4 — unary minus
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
|
||||
# Expected: Unary('-', Num(5))
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
|
||||
# Expected: Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
|
||||
# Expected: BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
```
|
||||
|
||||
### D5 — errors (all must raise ParseError, not any other exception)
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
try: parse(tokenize('1 +'))
|
||||
except ParseError: print('PASS')
|
||||
except Exception as e: print('FAIL:', type(e).__name__, e)"
|
||||
# Expected: PASS
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
try: parse(tokenize('(1'))
|
||||
except ParseError: print('PASS')
|
||||
except Exception as e: print('FAIL:', type(e).__name__, e)"
|
||||
# Expected: PASS
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
try: parse(tokenize('1 2'))
|
||||
except ParseError: print('PASS')
|
||||
except Exception as e: print('FAIL:', type(e).__name__, e)"
|
||||
# Expected: PASS
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
try: parse(tokenize(')('))
|
||||
except ParseError: print('PASS')
|
||||
except Exception as e: print('FAIL:', type(e).__name__, e)"
|
||||
# Expected: PASS
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
try: parse(tokenize(''))
|
||||
except ParseError: print('PASS')
|
||||
except Exception as e: print('FAIL:', type(e).__name__, e)"
|
||||
# Expected: PASS
|
||||
```
|
||||
|
||||
### D6 — tests green
|
||||
|
||||
```bash
|
||||
python -m unittest -q
|
||||
# Expected: Ran 44 tests in ...s\n\nOK (18 lexer + 26 parser)
|
||||
```
|
||||
3
calculators/builder-adversary/run-04/.gitignore
vendored
Normal file
3
calculators/builder-adversary/run-04/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
14
calculators/builder-adversary/run-04/GIT-LOG.txt
Normal file
14
calculators/builder-adversary/run-04/GIT-LOG.txt
Normal file
@ -0,0 +1,14 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
d84841b status(eval): mark DONE — all D1-D5 Adversary-verified PASS
|
||||
c2c1d8c review(D1,D2,D3,D4,D5): PASS — all gates verified cold, no findings
|
||||
2a80371 claim(D1,D2,D3,D4,D5): implement evaluator, CLI, and test suite
|
||||
5be0490 review(init): Adversary initialized eval phase tracking files, awaiting Builder claims
|
||||
d92887a status(parse): mark DONE — all D1-D6 Adversary-verified PASS
|
||||
eddeb60 review(D1,D2,D3,D4,D5,D6): PASS — all gates verified cold, no findings
|
||||
38bc287 claim(D1,D2,D3,D4,D5,D6): implement parser with full test suite
|
||||
e88fdcf review(init): Adversary initialized REVIEW-parse.md, awaiting Builder claims
|
||||
fb784d1 fix(AF-1): wrap float() in try/except to raise LexError on malformed numbers
|
||||
7296cfd review(D1,D2,D3,D4): PASS — all gates verified cold, one non-blocking finding (AF-1)
|
||||
defc7b3 claim(D1,D2,D3,D4): implement lexer with full test suite
|
||||
781005b review(init): Adversary initialized REVIEW-lex.md, awaiting Builder claims
|
||||
e5cc068 chore: seed
|
||||
1
calculators/builder-adversary/run-04/README.md
Normal file
1
calculators/builder-adversary/run-04/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary/run-04/SOURCE.txt
Normal file
1
calculators/builder-adversary/run-04/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r4
|
||||
28
calculators/builder-adversary/run-04/calc.py
Normal file
28
calculators/builder-adversary/run-04/calc.py
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Top-level CLI: python calc.py "<expression>"
|
||||
|
||||
Prints the result to stdout and exits 0 on success.
|
||||
Prints an error message to stderr and exits 1 on any parse / eval error.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from calc.lexer import tokenize, LexError
|
||||
from calc.parser import parse, ParseError
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def main() -> None:
|
||||
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)))
|
||||
print(result)
|
||||
except (LexError, ParseError, EvalError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
48
calculators/builder-adversary/run-04/calc/evaluator.py
Normal file
48
calculators/builder-adversary/run-04/calc/evaluator.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""Evaluator for the arithmetic AST produced by calc.parser.
|
||||
|
||||
evaluate(node) -> int | float
|
||||
|
||||
Whole-valued results are returned as int ("4/2" -> 2).
|
||||
Non-whole results are returned as float ("7/2" -> 3.5).
|
||||
Division by zero raises EvalError.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from calc.parser import Node, Num, BinOp, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _normalize(val: int | float) -> int | float:
|
||||
"""Return int if val is a whole-valued float, otherwise unchanged."""
|
||||
if isinstance(val, float) and val.is_integer():
|
||||
return int(val)
|
||||
return val
|
||||
|
||||
|
||||
def evaluate(node: Node) -> int | float:
|
||||
"""Walk an AST node and return its numeric value."""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, BinOp):
|
||||
l = evaluate(node.left)
|
||||
r = evaluate(node.right)
|
||||
if node.op == '+':
|
||||
return _normalize(l + r)
|
||||
if node.op == '-':
|
||||
return _normalize(l - r)
|
||||
if node.op == '*':
|
||||
return _normalize(l * r)
|
||||
if node.op == '/':
|
||||
if r == 0:
|
||||
raise EvalError("division by zero")
|
||||
return _normalize(l / r)
|
||||
raise EvalError(f"unknown operator {node.op!r}")
|
||||
if isinstance(node, Unary):
|
||||
val = evaluate(node.operand)
|
||||
if node.op == '-':
|
||||
return _normalize(-val)
|
||||
raise EvalError(f"unknown unary operator {node.op!r}")
|
||||
raise EvalError(f"unknown AST node type {type(node).__name__!r}")
|
||||
45
calculators/builder-adversary/run-04/calc/lexer.py
Normal file
45
calculators/builder-adversary/run-04/calc/lexer.py
Normal file
@ -0,0 +1,45 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: object
|
||||
|
||||
|
||||
_SINGLE = {'+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH',
|
||||
'(': 'LPAREN', ')': 'RPAREN'}
|
||||
|
||||
|
||||
def tokenize(src: str) -> List[Token]:
|
||||
tokens: List[Token] = []
|
||||
i = 0
|
||||
while i < len(src):
|
||||
ch = src[i]
|
||||
if ch in ' \t\n\r':
|
||||
i += 1
|
||||
continue
|
||||
if ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
while j < len(src) and (src[j].isdigit() or src[j] == '.'):
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
try:
|
||||
value = float(raw) if '.' in raw else int(raw)
|
||||
except ValueError:
|
||||
raise LexError(f"invalid number literal {raw!r} at position {i}")
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
continue
|
||||
if ch in _SINGLE:
|
||||
tokens.append(Token(_SINGLE[ch], ch))
|
||||
i += 1
|
||||
continue
|
||||
raise LexError(f"unexpected character {ch!r} at position {i}")
|
||||
tokens.append(Token('EOF', None))
|
||||
return tokens
|
||||
139
calculators/builder-adversary/run-04/calc/parser.py
Normal file
139
calculators/builder-adversary/run-04/calc/parser.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""Recursive-descent parser for arithmetic expressions.
|
||||
|
||||
Grammar (precedence low → high):
|
||||
expr ::= term (('+' | '-') term)*
|
||||
term ::= unary (('*' | '/') unary)*
|
||||
unary ::= '-' unary | primary
|
||||
primary::= NUMBER | '(' expr ')'
|
||||
|
||||
AST node shapes:
|
||||
Num(value) – leaf; value is int or float
|
||||
BinOp(op, left, right) – op is '+', '-', '*', or '/'
|
||||
Unary(op, operand) – op is '-'
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Union
|
||||
|
||||
from calc.lexer import Token
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AST nodes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: "Node"
|
||||
right: "Node"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: "Node"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
|
||||
Node = Union[Num, BinOp, Unary]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _Parser:
|
||||
def __init__(self, tokens: List[Token]) -> None:
|
||||
self._tokens = tokens
|
||||
self._pos = 0
|
||||
|
||||
def _peek(self) -> Token:
|
||||
return self._tokens[self._pos]
|
||||
|
||||
def _consume(self, kind: str) -> Token:
|
||||
tok = self._peek()
|
||||
if tok.kind != kind:
|
||||
raise ParseError(
|
||||
f"expected {kind!r} but got {tok.kind!r} (value={tok.value!r})"
|
||||
)
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
def _advance(self) -> Token:
|
||||
tok = self._tokens[self._pos]
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
# expr ::= term (('+' | '-') term)*
|
||||
def _expr(self) -> Node:
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op_tok = self._advance()
|
||||
right = self._term()
|
||||
node = BinOp(op_tok.value, node, right)
|
||||
return node
|
||||
|
||||
# term ::= unary (('*' | '/') unary)*
|
||||
def _term(self) -> Node:
|
||||
node = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op_tok = self._advance()
|
||||
right = self._unary()
|
||||
node = BinOp(op_tok.value, node, right)
|
||||
return node
|
||||
|
||||
# unary ::= '-' unary | primary
|
||||
def _unary(self) -> Node:
|
||||
if self._peek().kind == 'MINUS':
|
||||
op_tok = self._advance()
|
||||
return Unary(op_tok.value, self._unary())
|
||||
return self._primary()
|
||||
|
||||
# primary ::= NUMBER | '(' expr ')'
|
||||
def _primary(self) -> Node:
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._advance()
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._advance()
|
||||
node = self._expr()
|
||||
self._consume('RPAREN')
|
||||
return node
|
||||
if tok.kind == 'EOF':
|
||||
raise ParseError("unexpected end of input")
|
||||
raise ParseError(f"unexpected token {tok.kind!r} (value={tok.value!r})")
|
||||
|
||||
def parse(self) -> Node:
|
||||
if self._peek().kind == 'EOF':
|
||||
raise ParseError("empty expression")
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'EOF':
|
||||
tok = self._peek()
|
||||
raise ParseError(
|
||||
f"unexpected token after expression: {tok.kind!r} (value={tok.value!r})"
|
||||
)
|
||||
return node
|
||||
|
||||
|
||||
def parse(tokens: List[Token]) -> Node:
|
||||
"""Parse a token list produced by calc.lexer.tokenize into an AST."""
|
||||
return _Parser(tokens).parse()
|
||||
90
calculators/builder-adversary/run-04/calc/test_evaluator.py
Normal file
90
calculators/builder-adversary/run-04/calc/test_evaluator.py
Normal file
@ -0,0 +1,90 @@
|
||||
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 — basic arithmetic, precedence, parens, unary minus."""
|
||||
|
||||
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_left_associative_subtraction(self):
|
||||
self.assertEqual(calc("8-3-2"), 3)
|
||||
|
||||
def test_unary_minus_prefix(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+1"), 2)
|
||||
|
||||
def test_simple_sub(self):
|
||||
self.assertEqual(calc("10-4"), 6)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(calc("((3+2))*2"), 10)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
"""D2 — true division and division-by-zero."""
|
||||
|
||||
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(self):
|
||||
"""ZeroDivisionError must NOT escape the API."""
|
||||
try:
|
||||
calc("1/0")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("ZeroDivisionError escaped; should be wrapped in EvalError")
|
||||
|
||||
def test_expression_div_by_zero(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("5/(3-3)")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
"""D3 — whole-valued results print without trailing .0."""
|
||||
|
||||
def test_whole_division_returns_int(self):
|
||||
result = calc("4/2")
|
||||
self.assertEqual(result, 2)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_non_whole_division_returns_float(self):
|
||||
result = calc("7/2")
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertAlmostEqual(result, 3.5)
|
||||
|
||||
def test_integer_arithmetic_returns_int(self):
|
||||
result = calc("3+4")
|
||||
self.assertIsInstance(result, int)
|
||||
self.assertEqual(result, 7)
|
||||
|
||||
def test_str_no_trailing_dot_zero(self):
|
||||
self.assertEqual(str(calc("4/2")), "2")
|
||||
|
||||
def test_str_float_has_decimal(self):
|
||||
self.assertIn(".", str(calc("7/2")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
103
calculators/builder-adversary/run-04/calc/test_lexer.py
Normal file
103
calculators/builder-adversary/run-04/calc/test_lexer.py
Normal file
@ -0,0 +1,103 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize, Token, LexError
|
||||
|
||||
|
||||
def kinds(src):
|
||||
return [t.kind for t in tokenize(src)]
|
||||
|
||||
|
||||
def kv(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)])
|
||||
self.assertIsInstance(toks[0].value, int)
|
||||
|
||||
def test_float_standard(self):
|
||||
toks = tokenize("3.14")
|
||||
self.assertEqual(toks, [Token('NUMBER', 3.14), Token('EOF', None)])
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_leading_dot(self):
|
||||
toks = tokenize(".5")
|
||||
self.assertAlmostEqual(toks[0].value, 0.5)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_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], Token('NUMBER', 0))
|
||||
self.assertIsInstance(toks[0].value, int)
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
def test_all_ops(self):
|
||||
self.assertEqual(kinds("+-*/()"), ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'])
|
||||
|
||||
def test_expression(self):
|
||||
self.assertEqual(kinds("1+2*3"), ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'])
|
||||
|
||||
def test_values_preserved(self):
|
||||
self.assertEqual([t.value for t in tokenize("1+2*3")], [1, '+', 2, '*', 3, None])
|
||||
|
||||
def test_parens(self):
|
||||
self.assertEqual(kinds("(1+2)"), ['LPAREN','NUMBER','PLUS','NUMBER','RPAREN','EOF'])
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
def test_spaces_between(self):
|
||||
self.assertEqual(kinds(" 12 + 3 "), ['NUMBER','PLUS','NUMBER','EOF'])
|
||||
vals = [t.value for t in tokenize(" 12 + 3 ")]
|
||||
self.assertEqual(vals, [12, '+', 3, None])
|
||||
|
||||
def test_complex_expr(self):
|
||||
toks = tokenize("3.5*(1-2)")
|
||||
expected_kinds = ['NUMBER','STAR','LPAREN','NUMBER','MINUS','NUMBER','RPAREN','EOF']
|
||||
self.assertEqual([t.kind for t in toks], expected_kinds)
|
||||
self.assertAlmostEqual(toks[0].value, 3.5)
|
||||
|
||||
def test_tabs(self):
|
||||
self.assertEqual(kinds("1\t+\t2"), ['NUMBER','PLUS','NUMBER','EOF'])
|
||||
|
||||
def test_at_raises(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_dollar_raises(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$5")
|
||||
|
||||
def test_letter_raises(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 x 2")
|
||||
self.assertIn('x', str(ctx.exception))
|
||||
|
||||
def test_error_position_in_message(self):
|
||||
try:
|
||||
tokenize("1 @ 2")
|
||||
except LexError as e:
|
||||
self.assertIn('2', str(e)) # position 2
|
||||
|
||||
def test_malformed_float_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("..")
|
||||
|
||||
def test_malformed_float_multiple_dots_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("1.2.3")
|
||||
|
||||
def test_bare_dot_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize(".")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
146
calculators/builder-adversary/run-04/calc/test_parser.py
Normal file
146
calculators/builder-adversary/run-04/calc/test_parser.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""Test suite for calc.parser — asserts on AST structure."""
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError, Num, BinOp, Unary
|
||||
|
||||
|
||||
def p(src: str):
|
||||
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)))
|
||||
tree = p("1+2*3")
|
||||
self.assertEqual(tree, 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))
|
||||
tree = p("2*3+1")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('*', Num(2), Num(3)), Num(1)))
|
||||
|
||||
def test_sub_then_div(self):
|
||||
# 10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2)))
|
||||
tree = p("10-6/2")
|
||||
self.assertEqual(tree, BinOp('-', Num(10), BinOp('/', Num(6), Num(2))))
|
||||
|
||||
def test_mul_and_div_chain(self):
|
||||
# 2+3*4-1 → BinOp('-', BinOp('+', Num(2), BinOp('*', Num(3), Num(4))), Num(1))
|
||||
tree = p("2+3*4-1")
|
||||
expected = BinOp('-', BinOp('+', Num(2), BinOp('*', Num(3), Num(4))), Num(1))
|
||||
self.assertEqual(tree, expected)
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
"""D2 — same-precedence operators are left-associative."""
|
||||
|
||||
def test_sub_left(self):
|
||||
# 8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
tree = p("8-3-2")
|
||||
self.assertEqual(tree, BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
|
||||
|
||||
def test_div_left(self):
|
||||
# 8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
tree = p("8/4/2")
|
||||
self.assertEqual(tree, BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
|
||||
|
||||
def test_add_left(self):
|
||||
# 1+2+3 → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("1+2+3")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_mul_left(self):
|
||||
# 2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
tree = p("2*3*4")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
"""D3 — parentheses override precedence."""
|
||||
|
||||
def test_paren_overrides_mul(self):
|
||||
# (1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("(1+2)*3")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_paren_overrides_div(self):
|
||||
# 6/(1+2) → BinOp('/', Num(6), BinOp('+', Num(1), Num(2)))
|
||||
tree = p("6/(1+2)")
|
||||
self.assertEqual(tree, BinOp('/', Num(6), BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_nested_parens(self):
|
||||
# ((2+3)) → BinOp('+', Num(2), Num(3))
|
||||
tree = p("((2+3))")
|
||||
self.assertEqual(tree, BinOp('+', Num(2), Num(3)))
|
||||
|
||||
def test_paren_single_number(self):
|
||||
tree = p("(42)")
|
||||
self.assertEqual(tree, Num(42))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
"""D4 — unary minus."""
|
||||
|
||||
def test_simple_unary(self):
|
||||
# -5 → Unary('-', Num(5))
|
||||
tree = p("-5")
|
||||
self.assertEqual(tree, Unary('-', Num(5)))
|
||||
|
||||
def test_unary_parens(self):
|
||||
# -(1+2) → Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
tree = p("-(1+2)")
|
||||
self.assertEqual(tree, Unary('-', BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_mul_unary(self):
|
||||
# 3 * -2 → BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
tree = p("3 * -2")
|
||||
self.assertEqual(tree, BinOp('*', Num(3), Unary('-', Num(2))))
|
||||
|
||||
def test_double_unary(self):
|
||||
# --5 → Unary('-', Unary('-', Num(5)))
|
||||
tree = p("--5")
|
||||
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
|
||||
|
||||
def test_unary_in_add(self):
|
||||
# 1 + -2 → BinOp('+', Num(1), Unary('-', Num(2)))
|
||||
tree = p("1 + -2")
|
||||
self.assertEqual(tree, BinOp('+', Num(1), Unary('-', Num(2))))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
"""D5 — malformed input raises ParseError."""
|
||||
|
||||
def test_trailing_operator(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("")
|
||||
|
||||
def test_only_operator(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("*")
|
||||
|
||||
def test_mismatched_parens(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("(1+2))")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,7 @@
|
||||
# BACKLOG-eval.md — eval phase backlog
|
||||
|
||||
## Build backlog
|
||||
(Builder fills this in)
|
||||
|
||||
## Adversary findings
|
||||
(none yet)
|
||||
@ -0,0 +1,15 @@
|
||||
# BACKLOG-lex — Builder
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/__init__.py
|
||||
- [x] Implement calc/lexer.py (Token, LexError, tokenize)
|
||||
- [x] Implement calc/test_lexer.py (unittest covering D1-D3)
|
||||
- [x] Verify locally: python -m unittest -q — 16/16 PASS
|
||||
- [x] Claim D1 (numbers)
|
||||
- [x] Claim D2 (operators & parens)
|
||||
- [x] Claim D3 (whitespace & errors)
|
||||
- [x] Claim D4 (tests green)
|
||||
|
||||
## Awaiting Adversary
|
||||
All gates claimed. Waiting for REVIEW-lex.md verdicts.
|
||||
@ -0,0 +1,14 @@
|
||||
# BACKLOG-parse — Adversary
|
||||
|
||||
## Adversary findings
|
||||
|
||||
(none yet)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create `calc/parser.py` with recursive-descent parser
|
||||
- [x] Create `calc/test_parser.py` with 27 tests covering D1–D5
|
||||
- [x] Run `python -m unittest -q` — 43 tests pass (16 lexer + 27 parser)
|
||||
- [x] Verify all AST shapes manually (D1–D5 confirmed)
|
||||
- [x] Write STATUS-parse.md with exact verification commands
|
||||
- [x] Claim D1–D6 (awaiting Adversary PASS)
|
||||
@ -0,0 +1,9 @@
|
||||
# DECISIONS — shared (append-only)
|
||||
|
||||
## 2026-06-15 — Token representation
|
||||
Using `dataclasses.dataclass` for Token (fields: kind: str, value: object).
|
||||
Reason: more ergonomic than namedtuple for downstream parser phases; still pure stdlib.
|
||||
|
||||
## 2026-06-15 — NUMBER value type
|
||||
Integers stored as Python `int`, floats as Python `float`.
|
||||
Detection: presence of `.` in the matched digit string → float.
|
||||
@ -0,0 +1,42 @@
|
||||
# JOURNAL-eval.md — Adversary journal for phase `eval`
|
||||
|
||||
## 2026-06-15T01:26Z — Phase initialized
|
||||
|
||||
- Read phase plan from /home/loops/project-orchestrator/projects/agent-orchestrator-benchmark/plans/calc/eval.md
|
||||
- parse phase is DONE (all D1-D6 PASS)
|
||||
- eval phase not started — no evaluator.py, calc.py, or test_evaluator.py yet
|
||||
- Initialized tracking files: STATUS-eval.md, REVIEW-eval.md, BACKLOG-eval.md, JOURNAL-eval.md
|
||||
- Waiting for Builder to claim gates
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-15 — Builder implementation notes
|
||||
|
||||
### Approach
|
||||
|
||||
Implemented `evaluate(node)` as a recursive AST walk. Key decisions:
|
||||
|
||||
1. **D3 normalization**: `_normalize(val)` converts whole-valued floats (e.g., `2.0`) to `int`. Applied after every arithmetic operation. Ensures `str(evaluate(...))` prints `2` not `2.0` for whole results.
|
||||
|
||||
2. **D2 division by zero**: Explicit `if r == 0: raise EvalError(...)` before `l / r`. Catches integer 0, float 0.0, and zero sub-expressions (e.g., `5/(3-3)`).
|
||||
|
||||
### Test run output
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 60 tests in 0.005s
|
||||
OK
|
||||
```
|
||||
|
||||
One fix during dev: `test_expression_div_by_zero` had `calc("5*(3-3)")` (multiplication, not division). Fixed to `calc("5/(3-3)")`.
|
||||
|
||||
### CLI verification output
|
||||
|
||||
```
|
||||
$ 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" → stderr: "error: division by zero", exit=1
|
||||
$ python calc.py "1 +" → stderr: "error: unexpected end of input", exit=1
|
||||
```
|
||||
@ -0,0 +1,59 @@
|
||||
# JOURNAL-lex — Builder
|
||||
|
||||
## 2026-06-15 — Initial setup
|
||||
|
||||
Phase plan read. Mission: build calc/lexer.py with Token (kind, value), LexError,
|
||||
and tokenize(src) -> list[Token]. Kinds: NUMBER, PLUS, MINUS, STAR, SLASH, LPAREN,
|
||||
RPAREN, EOF.
|
||||
|
||||
Design notes:
|
||||
- Token as a dataclass or namedtuple — using dataclass for forward-compat with parser phase
|
||||
- NUMBER value stored as int if no decimal point, float if decimal present
|
||||
- LexError carries offending char and position
|
||||
- tokenize scans left-to-right, skips whitespace, raises on unknown char
|
||||
|
||||
## 2026-06-15 — Implementation + local verification
|
||||
|
||||
Wrote calc/lexer.py and calc/test_lexer.py. Found one bug in test_values_preserved:
|
||||
used a conditional expression `('STAR','*') if False else ('PLUS','+')` accidentally
|
||||
duplicated PLUS in expected list. Fixed immediately.
|
||||
|
||||
Test run output:
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 16 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')"
|
||||
calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
|
||||
All D1–D4 criteria met. Claiming all gates.
|
||||
|
||||
## 2026-06-15 — Adversary verdicts + AF-1 fix
|
||||
|
||||
Adversary returned: D1 PASS, D2 PASS, D3 PASS, D4 PASS.
|
||||
|
||||
AF-1 filed (non-blocking): malformed floats (`".."`, `"1.2.3"`, `"."`) raised bare
|
||||
`ValueError` from `float(raw)` instead of `LexError`. Root cause: greedy dot scanner
|
||||
at lexer.py:29–31 consumes multi-dot sequences without validation.
|
||||
|
||||
Fix: wrapped `float(raw)` in try/except ValueError → re-raise as LexError with
|
||||
message `"invalid number literal {raw!r} at position {i}"`. Added 3 new tests.
|
||||
|
||||
Post-fix run:
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 19 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
|
||||
Phase DONE.
|
||||
@ -0,0 +1,44 @@
|
||||
# JOURNAL-parse.md
|
||||
|
||||
## 2026-06-15T01:21Z — Parser implementation
|
||||
|
||||
### Approach
|
||||
Implemented a classic recursive-descent parser with three precedence levels:
|
||||
- `_expr()`: handles `+` / `-` (lowest precedence, left-associative)
|
||||
- `_term()`: handles `*` / `/` (middle precedence, left-associative)
|
||||
- `_unary()`: handles unary `-` (right-associative by recursion)
|
||||
- `_primary()`: handles NUMBER literals and `(` expr `)`
|
||||
|
||||
Left-associativity falls out naturally from the while-loop accumulation pattern in `_expr()` and `_term()`: each iteration wraps the running `node` as the left child of a new `BinOp`.
|
||||
|
||||
Unary minus recurses into itself (`_unary()` calls `_unary()`) which gives right-associativity for `--5` → `Unary('-', Unary('-', Num(5)))`.
|
||||
|
||||
### Verification run
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 43 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
Manual shape checks:
|
||||
```
|
||||
D1 1+2*3: BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓
|
||||
D2 8-3-2: BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓
|
||||
D2 8/4/2: BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
|
||||
D3 (1+2)*3: BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
D4 -5: Unary('-', Num(5)) ✓
|
||||
D4 -(1+2): Unary('-', BinOp('+', Num(1), Num(2))) ✓
|
||||
D4 3*-2: BinOp('*', Num(3), Unary('-', Num(2))) ✓
|
||||
D5 '1 +': ParseError: unexpected end of input ✓
|
||||
D5 '(1': ParseError: expected 'RPAREN' but got 'EOF' ✓
|
||||
D5 '1 2': ParseError: unexpected token after expression ✓
|
||||
D5 ')(': ParseError: unexpected token 'RPAREN' ✓
|
||||
D5 '': ParseError: empty expression ✓
|
||||
```
|
||||
|
||||
### Error handling design
|
||||
- Empty token list (just EOF): caught in `parse()` before entering `_expr()`
|
||||
- Trailing operator (`1 +`): `_primary()` sees EOF, raises ParseError
|
||||
- Unclosed paren (`(1`): `_consume('RPAREN')` fails with ParseError
|
||||
- Extra number (`1 2`): `parse()` checks `peek() != EOF` after `_expr()` returns
|
||||
- `)` before `(`: `_primary()` sees RPAREN, not a valid primary, raises ParseError
|
||||
@ -0,0 +1,74 @@
|
||||
# REVIEW-eval.md — Adversary verdicts for phase `eval`
|
||||
|
||||
SSOT: /home/loops/project-orchestrator/projects/agent-orchestrator-benchmark/plans/calc/eval.md
|
||||
|
||||
## Status: ALL GATES PASS
|
||||
|
||||
All gates verified cold @2026-06-15T01:29Z.
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
### eval/D1: PASS @2026-06-15T01:29Z
|
||||
|
||||
Cold-run evidence:
|
||||
```
|
||||
python calc.py "2+3*4" → 14, exit 0
|
||||
python calc.py "(2+3)*4" → 20, exit 0
|
||||
python calc.py "8-3-2" → 3, exit 0
|
||||
python calc.py "-2+5" → 3, exit 0
|
||||
python calc.py "2*-3" → -6, exit 0
|
||||
```
|
||||
All 5 spec examples from plan correct. Precedence, parens, unary minus all work.
|
||||
|
||||
### eval/D2: PASS @2026-06-15T01:29Z
|
||||
|
||||
Cold-run evidence:
|
||||
```
|
||||
python calc.py "7/2" → 3.5, exit 0 (true division)
|
||||
python calc.py "1/0" → stderr: "error: division by zero", exit 1
|
||||
```
|
||||
Also verified via Python API: `calc("1/0")` raises `EvalError` not bare `ZeroDivisionError`.
|
||||
Sub-expression div-by-zero: `calc("5/(2-2)")` → `EvalError: division by zero`. OK.
|
||||
|
||||
### eval/D3: PASS @2026-06-15T01:29Z
|
||||
|
||||
Cold-run evidence:
|
||||
```
|
||||
python calc.py "4/2" → 2 (no .0)
|
||||
python calc.py "7/2" → 3.5 (float with decimal)
|
||||
python calc.py "0" → 0 (not 0.0)
|
||||
python calc.py "5-5" → 0 (int zero, not 0.0)
|
||||
```
|
||||
`_normalize()` converts whole-valued float to int. `str(calc("4/2")) == "2"` confirmed.
|
||||
|
||||
### eval/D4: PASS @2026-06-15T01:29Z
|
||||
|
||||
Cold-run evidence:
|
||||
```
|
||||
python calc.py "2+3*4" → stdout: 14, exit 0
|
||||
python calc.py "1/0" → stderr: "error: division by zero", exit 1 (no traceback)
|
||||
python calc.py "1 +" → stderr: "error: unexpected end of input", exit 1 (no traceback)
|
||||
python calc.py → stderr: "usage: calc.py <expression>", exit 1
|
||||
python calc.py "1" "x" → stderr: "usage: calc.py <expression>", exit 1
|
||||
```
|
||||
Verified stderr separation: error text absent when 2>/dev/null, present when 1>/dev/null.
|
||||
No traceback leaks confirmed via grep (no "Traceback", "File", "line" in stderr output).
|
||||
|
||||
### eval/D5: PASS @2026-06-15T01:29Z
|
||||
|
||||
Cold-run evidence:
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 60 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
16 lex + 27 parse + 17 evaluator = 60 total. 0 failures. No regressions.
|
||||
Lexer and parser suites run in isolation: `python -m unittest calc.test_lexer calc.test_parser` → 43 tests OK.
|
||||
|
||||
## Findings
|
||||
|
||||
No defects found. No VETO.
|
||||
|
||||
## Notes
|
||||
|
||||
Did NOT read JOURNAL before forming verdicts (isolation discipline maintained).
|
||||
@ -0,0 +1,53 @@
|
||||
# REVIEW-lex.md — Adversary verdicts
|
||||
|
||||
## Status
|
||||
All D1–D4 PASS. Phase complete.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### lex/D1: PASS @2026-06-15T01:20Z
|
||||
Cold-ran:
|
||||
- `tokenize("42")` → `[Token('NUMBER', 42), Token('EOF', None)]` — value is `int` ✓
|
||||
- `tokenize("3.14")` → `NUMBER(3.14)` as `float` ✓
|
||||
- `tokenize(".5")` → `NUMBER(0.5)` as `float` ✓
|
||||
- `tokenize("10.")` → `NUMBER(10.0)` as `float` ✓
|
||||
- EOF is always the final token with `value=None` across all inputs ✓
|
||||
|
||||
### lex/D2: PASS @2026-06-15T01:20Z
|
||||
Cold-ran:
|
||||
- `tokenize("+-*/()")` → `PLUS MINUS STAR SLASH LPAREN RPAREN EOF` ✓
|
||||
- `tokenize("1+2*3")` → `NUMBER PLUS NUMBER STAR NUMBER EOF` ✓
|
||||
- `tokenize("3.5*(1-2)")` → `[('NUMBER',3.5),('STAR','*'),('LPAREN','('),('NUMBER',1),('MINUS','-'),('NUMBER',2),('RPAREN',')'),('EOF',None)]` — matches expected exactly ✓
|
||||
|
||||
### lex/D3: PASS @2026-06-15T01:20Z
|
||||
Cold-ran:
|
||||
- `tokenize(" 12 + 3 ")` → `NUMBER(12) PLUS NUMBER(3) EOF` (spaces skipped) ✓
|
||||
- `tokenize("1\t+\t2")` → tabs skipped ✓
|
||||
- `tokenize("1 @ 2")` → raises `LexError: unexpected character '@' at position 2` ✓
|
||||
- `tokenize("$5")` → raises `LexError` with `'$'` in message ✓
|
||||
- `tokenize("1 x 2")` → raises `LexError` with `'x'` in message ✓
|
||||
- Error message includes the offending char and its position ✓
|
||||
|
||||
### lex/D4: PASS @2026-06-15T01:20Z
|
||||
Cold-ran `python -m unittest -q` from repo root:
|
||||
```
|
||||
Ran 16 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
16 tests, 0 failures. Required coverage confirmed present:
|
||||
- `" 12 + 3 "` tested in `test_spaces_between` ✓
|
||||
- `"3.5*(1-2)"` tested in `test_complex_expr` ✓
|
||||
- `"1 @ 2"` raises `LexError` tested in `test_at_raises` and `test_error_position_in_message` ✓
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### AF-1: ValueError on malformed number literals (not a DoD blocker)
|
||||
Malformed number sequences raise bare `ValueError` instead of `LexError`:
|
||||
```
|
||||
tokenize("..") → ValueError: could not convert string to float: '..'
|
||||
tokenize("1.2.3") → ValueError: could not convert string to float: '1.2.3'
|
||||
tokenize(".") → ValueError: could not convert string to float: '.'
|
||||
```
|
||||
Root cause: `lexer.py:32` calls `float(raw)` without a try/except. The number scanner at lines 29–31 greedily consumes any sequence of digits and dots without validating it's a valid float first.
|
||||
|
||||
**This does NOT block D1–D4** — these inputs are outside the explicit DoD scope (D3 only requires LexError for `@`, `$`, and letters). Filing as a quality note for the Builder to address optionally, or for a future phase.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user