artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
3
calculators/builder-adversary-lean/run-02/.gitignore
vendored
Normal file
3
calculators/builder-adversary-lean/run-02/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
29
calculators/builder-adversary-lean/run-02/GIT-LOG.txt
Normal file
29
calculators/builder-adversary-lean/run-02/GIT-LOG.txt
Normal file
@ -0,0 +1,29 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
7987247 review(D1,D2,D3,D4,D5): PASS — all eval gates verified; no defects found
|
||||
86958a2 claim(D5): python -m unittest passes, 63 tests (37 lex+parse + 26 evaluator), 0 failures
|
||||
74d3276 claim(D4): CLI prints result on stdout+exit 0; prints error on stderr+exit 1 for errors; no traceback
|
||||
16f3f17 claim(D3): whole-valued results return int (4/2->2), non-whole return float (7/2->3.5)
|
||||
cae9347 claim(D2): true division 7/2=3.5; division-by-zero raises EvalError not ZeroDivisionError
|
||||
b37f7a0 claim(D1): evaluate arithmetic — precedence, parens, unary minus all correct
|
||||
7167e33 feat(eval): add evaluator.py, calc.py CLI, and test_evaluator.py
|
||||
baf8a4a review(eval-init): Adversary initialized for phase eval — waiting for Builder claims
|
||||
b5345dd status(parse): mark DONE — all gates PASS
|
||||
c552cae review(D1,D2,D3,D4,D5,D6): PASS — all parse gates verified; no defects found
|
||||
4731b77 journal(parse): implementation notes and verification output
|
||||
146c82f claim(D6): python -m unittest passes, 37 tests, 0 failures
|
||||
5be9ecf claim(D5): 5 error cases all raise ParseError correctly
|
||||
59d5e59 claim(D4): -5, -(1+2), 3*-2 all produce Unary nodes correctly
|
||||
90b3b29 claim(D3): (1+2)*3 parses with + under *
|
||||
1032c3d claim(D2): 8-3-2 and 8/4/2 parse left-associatively
|
||||
a0e5959 claim(D1): 1+2*3 parses as BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))
|
||||
866091c feat(parse): add parser.py and test_parser.py — all D1-D6 gates implemented
|
||||
00f7b1e review(init-parse): Adversary initialized for phase parse
|
||||
d6fc26f fix: wrap float() in try/except to raise LexError on malformed numbers (AF-1); mark phase DONE; consume BUILDER-INBOX
|
||||
5cd2daa review(D1,D2,D3,D4): PASS — all lex gates verified; AF-1 filed for ValueError leak
|
||||
63dbd91 claim(D4): python -m unittest passes, 18 tests, 0 failures
|
||||
db6f1ab claim(D3): whitespace skipped, LexError raised with char and position
|
||||
20d19c3 claim(D2): operators and parens tokenize to correct kinds
|
||||
a333f58 claim(D1): integers and floats tokenize to NUMBER with correct Python type
|
||||
ab0332e feat: implement calc lexer with tokenize(), Token, LexError and test suite
|
||||
d9f6737 review(init): Adversary initialized for phase lex
|
||||
d2011fc chore: seed
|
||||
1
calculators/builder-adversary-lean/run-02/README.md
Normal file
1
calculators/builder-adversary-lean/run-02/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary-lean/run-02/SOURCE.txt
Normal file
1
calculators/builder-adversary-lean/run-02/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-ufRkmF/builder-adversary-lean/r1
|
||||
23
calculators/builder-adversary-lean/run-02/calc.py
Normal file
23
calculators/builder-adversary-lean/run-02/calc.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
from calc.lexer import tokenize, LexError
|
||||
from calc.parser import parse, ParseError
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: python 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 e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
37
calculators/builder-adversary-lean/run-02/calc/evaluator.py
Normal file
37
calculators/builder-adversary-lean/run-02/calc/evaluator.py
Normal file
@ -0,0 +1,37 @@
|
||||
from calc.parser import Num, BinOp, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(node):
|
||||
"""Walk the AST and return int | float.
|
||||
|
||||
Whole-valued results (including whole-valued division) are returned as int;
|
||||
non-whole float results are returned as float.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, Unary):
|
||||
if node.op == '-':
|
||||
return -evaluate(node.operand)
|
||||
raise EvalError(f"unknown unary op {node.op!r}")
|
||||
if isinstance(node, BinOp):
|
||||
left = evaluate(node.left)
|
||||
right = evaluate(node.right)
|
||||
if node.op == '+':
|
||||
return left + right
|
||||
if node.op == '-':
|
||||
return left - right
|
||||
if node.op == '*':
|
||||
return left * right
|
||||
if node.op == '/':
|
||||
if right == 0:
|
||||
raise EvalError("division by zero")
|
||||
result = left / right
|
||||
if result == int(result):
|
||||
return int(result)
|
||||
return result
|
||||
raise EvalError(f"unknown binary op {node.op!r}")
|
||||
raise EvalError(f"unknown node type {type(node).__name__!r}")
|
||||
54
calculators/builder-adversary-lean/run-02/calc/lexer.py
Normal file
54
calculators/builder-adversary-lean/run-02/calc/lexer.py
Normal file
@ -0,0 +1,54 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: Union[int, float, str, None]
|
||||
|
||||
|
||||
def tokenize(src: str) -> list:
|
||||
tokens = []
|
||||
i = 0
|
||||
while i < len(src):
|
||||
ch = src[i]
|
||||
if ch in ' \t':
|
||||
i += 1
|
||||
elif ch == '+':
|
||||
tokens.append(Token('PLUS', '+'))
|
||||
i += 1
|
||||
elif ch == '-':
|
||||
tokens.append(Token('MINUS', '-'))
|
||||
i += 1
|
||||
elif ch == '*':
|
||||
tokens.append(Token('STAR', '*'))
|
||||
i += 1
|
||||
elif ch == '/':
|
||||
tokens.append(Token('SLASH', '/'))
|
||||
i += 1
|
||||
elif ch == '(':
|
||||
tokens.append(Token('LPAREN', '('))
|
||||
i += 1
|
||||
elif ch == ')':
|
||||
tokens.append(Token('RPAREN', ')'))
|
||||
i += 1
|
||||
elif ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
while j < 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
|
||||
else:
|
||||
raise LexError(f"unexpected character {ch!r} at position {i}")
|
||||
tokens.append(Token('EOF', None))
|
||||
return tokens
|
||||
92
calculators/builder-adversary-lean/run-02/calc/parser.py
Normal file
92
calculators/builder-adversary-lean/run-02/calc/parser.py
Normal file
@ -0,0 +1,92 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
from calc.lexer import Token
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: object
|
||||
right: object
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: object
|
||||
|
||||
|
||||
Node = Union[Num, BinOp, Unary]
|
||||
|
||||
|
||||
class _Parser:
|
||||
def __init__(self, tokens: list):
|
||||
self._tokens = tokens
|
||||
self._pos = 0
|
||||
|
||||
def _peek(self) -> Token:
|
||||
return self._tokens[self._pos]
|
||||
|
||||
def _consume(self, kind: str = None) -> Token:
|
||||
tok = self._tokens[self._pos]
|
||||
if kind and tok.kind != kind:
|
||||
raise ParseError(f"expected {kind!r}, got {tok.kind!r} ({tok.value!r})")
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
def parse(self) -> Node:
|
||||
if self._peek().kind == 'EOF':
|
||||
raise ParseError("empty input")
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'EOF':
|
||||
tok = self._peek()
|
||||
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
|
||||
return node
|
||||
|
||||
def _expr(self) -> Node:
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op = self._consume().value
|
||||
node = BinOp(op, node, self._term())
|
||||
return node
|
||||
|
||||
def _term(self) -> Node:
|
||||
node = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op = self._consume().value
|
||||
node = BinOp(op, node, self._unary())
|
||||
return node
|
||||
|
||||
def _unary(self) -> Node:
|
||||
if self._peek().kind == 'MINUS':
|
||||
self._consume()
|
||||
return Unary('-', self._unary())
|
||||
return self._primary()
|
||||
|
||||
def _primary(self) -> Node:
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._consume()
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._consume()
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'RPAREN':
|
||||
raise ParseError(f"unclosed parenthesis, got {self._peek().kind!r}")
|
||||
self._consume()
|
||||
return node
|
||||
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
|
||||
|
||||
|
||||
def parse(tokens: list) -> Node:
|
||||
return _Parser(tokens).parse()
|
||||
134
calculators/builder-adversary-lean/run-02/calc/test_evaluator.py
Normal file
134
calculators/builder-adversary-lean/run-02/calc/test_evaluator.py
Normal file
@ -0,0 +1,134 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from calc.evaluator import EvalError, evaluate
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
|
||||
|
||||
def calc(s):
|
||||
return evaluate(parse(tokenize(s)))
|
||||
|
||||
|
||||
class TestD1Arithmetic(unittest.TestCase):
|
||||
def test_add_mul_precedence(self):
|
||||
self.assertEqual(calc("2+3*4"), 14)
|
||||
|
||||
def test_parens(self):
|
||||
self.assertEqual(calc("(2+3)*4"), 20)
|
||||
|
||||
def test_left_assoc_sub(self):
|
||||
self.assertEqual(calc("8-3-2"), 3)
|
||||
|
||||
def test_unary_minus_add(self):
|
||||
self.assertEqual(calc("-2+5"), 3)
|
||||
|
||||
def test_mul_unary(self):
|
||||
self.assertEqual(calc("2*-3"), -6)
|
||||
|
||||
def test_basic_add(self):
|
||||
self.assertEqual(calc("1+1"), 2)
|
||||
|
||||
def test_basic_sub(self):
|
||||
self.assertEqual(calc("5-3"), 2)
|
||||
|
||||
def test_basic_mul(self):
|
||||
self.assertEqual(calc("3*4"), 12)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(calc("((2+3))"), 5)
|
||||
|
||||
def test_unary_only(self):
|
||||
self.assertEqual(calc("-5"), -5)
|
||||
|
||||
def test_double_unary(self):
|
||||
self.assertEqual(calc("--5"), 5)
|
||||
|
||||
|
||||
class TestD2Division(unittest.TestCase):
|
||||
def test_true_division(self):
|
||||
self.assertAlmostEqual(calc("7/2"), 3.5)
|
||||
|
||||
def test_div_by_zero_raises(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("1/0")
|
||||
|
||||
def test_div_by_zero_not_zdiv_error(self):
|
||||
try:
|
||||
calc("1/0")
|
||||
self.fail("expected EvalError")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("bare ZeroDivisionError escaped the API")
|
||||
|
||||
def test_div_integer(self):
|
||||
self.assertEqual(calc("6/3"), 2)
|
||||
|
||||
|
||||
class TestD3ResultType(unittest.TestCase):
|
||||
def test_whole_div_no_dot_zero(self):
|
||||
result = calc("4/2")
|
||||
self.assertEqual(result, 2)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_nonwhole_div_is_float(self):
|
||||
result = calc("7/2")
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertAlmostEqual(result, 3.5)
|
||||
|
||||
def test_int_add_is_int(self):
|
||||
result = calc("2+3")
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_str_whole_result(self):
|
||||
self.assertEqual(str(calc("4/2")), "2")
|
||||
|
||||
def test_str_float_result(self):
|
||||
self.assertEqual(str(calc("7/2")), "3.5")
|
||||
|
||||
|
||||
class TestD4CLI(unittest.TestCase):
|
||||
def _run(self, expr):
|
||||
return subprocess.run(
|
||||
[sys.executable, "calc.py", expr],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
def test_basic_expr(self):
|
||||
r = self._run("2+3*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "14")
|
||||
|
||||
def test_parens_expr(self):
|
||||
r = self._run("(2+3)*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "20")
|
||||
|
||||
def test_float_div(self):
|
||||
r = self._run("7/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "3.5")
|
||||
|
||||
def test_whole_div_no_dot(self):
|
||||
r = self._run("4/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "2")
|
||||
|
||||
def test_div_by_zero_nonzero_exit(self):
|
||||
r = self._run("1/0")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertGreater(len(r.stderr), 0)
|
||||
self.assertEqual(r.stdout, "")
|
||||
|
||||
def test_invalid_expr_nonzero_exit(self):
|
||||
r = self._run("1 +")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertGreater(len(r.stderr), 0)
|
||||
self.assertEqual(r.stdout, "")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
94
calculators/builder-adversary-lean/run-02/calc/test_lexer.py
Normal file
94
calculators/builder-adversary-lean/run-02/calc/test_lexer.py
Normal file
@ -0,0 +1,94 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize, Token, LexError
|
||||
|
||||
|
||||
def kinds(src):
|
||||
return [t.kind for t in tokenize(src)]
|
||||
|
||||
|
||||
def tok(src):
|
||||
return [(t.kind, t.value) for t in tokenize(src)]
|
||||
|
||||
|
||||
class TestNumbers(unittest.TestCase):
|
||||
def test_integer(self):
|
||||
result = tokenize("42")
|
||||
self.assertEqual(result[0], Token('NUMBER', 42))
|
||||
self.assertEqual(result[1], Token('EOF', None))
|
||||
self.assertIsInstance(result[0].value, int)
|
||||
|
||||
def test_float(self):
|
||||
result = tokenize("3.14")
|
||||
self.assertEqual(result[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(result[0].value, 3.14)
|
||||
self.assertIsInstance(result[0].value, float)
|
||||
|
||||
def test_leading_dot(self):
|
||||
result = tokenize(".5")
|
||||
self.assertEqual(result[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(result[0].value, 0.5)
|
||||
|
||||
def test_trailing_dot(self):
|
||||
result = tokenize("10.")
|
||||
self.assertEqual(result[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(result[0].value, 10.0)
|
||||
self.assertIsInstance(result[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_sequence(self):
|
||||
self.assertEqual(kinds("1+2*3"),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'])
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
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_expr(self):
|
||||
self.assertEqual(kinds("3.5*(1-2)"),
|
||||
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'])
|
||||
|
||||
def test_lex_error_at_sign(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_lex_error_dollar(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$")
|
||||
|
||||
def test_lex_error_letter(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("abc")
|
||||
|
||||
def test_lex_error_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('2', str(ctx.exception))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
103
calculators/builder-adversary-lean/run-02/calc/test_parser.py
Normal file
103
calculators/builder-adversary-lean/run-02/calc/test_parser.py
Normal file
@ -0,0 +1,103 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError, Num, BinOp, Unary
|
||||
|
||||
|
||||
def p(src):
|
||||
return parse(tokenize(src))
|
||||
|
||||
|
||||
class TestPrecedence(unittest.TestCase):
|
||||
def test_mul_binds_tighter_than_add(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_div_binds_tighter_than_sub(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))))
|
||||
|
||||
def test_mul_binds_tighter_than_sub(self):
|
||||
tree = p('5-2*3')
|
||||
self.assertEqual(tree, BinOp('-', Num(5), BinOp('*', Num(2), Num(3))))
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
def test_sub_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_div_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_add_left_assoc(self):
|
||||
tree = p('1+2+3')
|
||||
self.assertEqual(tree, BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_mul_left_assoc(self):
|
||||
tree = p('2*3*4')
|
||||
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
def test_parens_override_precedence(self):
|
||||
# (1+2)*3 => BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))
|
||||
tree = p('(1+2)*3')
|
||||
self.assertEqual(tree, BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_nested_parens(self):
|
||||
tree = p('((2+3))')
|
||||
self.assertEqual(tree, BinOp('+', Num(2), Num(3)))
|
||||
|
||||
def test_parens_right_side(self):
|
||||
tree = p('3*(1+2)')
|
||||
self.assertEqual(tree, BinOp('*', Num(3), BinOp('+', Num(1), Num(2))))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
def test_simple_unary(self):
|
||||
tree = p('-5')
|
||||
self.assertEqual(tree, Unary('-', Num(5)))
|
||||
|
||||
def test_unary_paren(self):
|
||||
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):
|
||||
tree = p('--5')
|
||||
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
def test_trailing_op(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('1 +')
|
||||
|
||||
def test_unclosed_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('(1')
|
||||
|
||||
def test_two_numbers(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('1 2')
|
||||
|
||||
def test_close_before_open(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p(')(')
|
||||
|
||||
def test_empty_string(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@ -0,0 +1,16 @@
|
||||
# BACKLOG — Phase eval (Adversary)
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_No findings — all D1–D5 gates verified PASS. No defects found._
|
||||
|
||||
---
|
||||
|
||||
## Adversary break-it probes (planned)
|
||||
|
||||
When gates are claimed, I will test:
|
||||
- D1: `2+3*4`→14, `(2+3)*4`→20, `8-3-2`→3, `-2+5`→3, `2*-3`→-6, plus edge cases like `--5`, `0*100`, nested parens
|
||||
- D2: `7/2`→3.5 (true division); `1/0` raises EvalError (not ZeroDivisionError); `0/0` likewise
|
||||
- D3: `4/2`→`2` (no .0); `7/2`→`3.5`; `6/3`→`2`; `1/3`→`0.333...`
|
||||
- D4: CLI exit 0 for valid; non-zero + stderr for invalid; traceback must NOT appear
|
||||
- D5: full `python -m unittest -q` including prior lexer+parser tests; check test count
|
||||
@ -0,0 +1,16 @@
|
||||
# BACKLOG — Phase lex
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 — numbers: INTEGER and FLOAT tokenization — CLAIMED
|
||||
- [x] D2 — operators & parens — CLAIMED
|
||||
- [x] D3 — whitespace & errors (LexError) — CLAIMED
|
||||
- [x] D4 — tests green (18 tests, 0 failures) — CLAIMED
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### AF-1 (open): ValueError leaks on malformed number tokens
|
||||
- `tokenize('1.2.3')` → `ValueError` (not `LexError`)
|
||||
- `tokenize('.')` → `ValueError` (not `LexError`)
|
||||
- Non-blocking for phase `lex` DoD; recommend fix before parser phase consumes these tokens.
|
||||
- Repro and details in REVIEW-lex.md § AF-1.
|
||||
@ -0,0 +1,12 @@
|
||||
# BACKLOG — Phase parse
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Write calc/parser.py with parse(tokens) -> Node
|
||||
- [x] Write calc/test_parser.py with unittest coverage of D1-D5
|
||||
- [ ] Claim D1 (precedence)
|
||||
- [ ] Claim D2 (left associativity)
|
||||
- [ ] Claim D3 (parentheses)
|
||||
- [ ] Claim D4 (unary minus)
|
||||
- [ ] Claim D5 (errors)
|
||||
- [ ] Claim D6 (tests green)
|
||||
@ -0,0 +1,4 @@
|
||||
# DECISIONS — Phase lex (shared, append-only)
|
||||
|
||||
## 2026-06-15 — Adversary initialized
|
||||
Adversary loop started monitoring for Builder gate claims on phase `lex`.
|
||||
@ -0,0 +1,43 @@
|
||||
# JOURNAL — Phase eval
|
||||
|
||||
## Implementation notes
|
||||
|
||||
### evaluator.py
|
||||
|
||||
Built `evaluate(node) -> int | float` walking `Num`, `BinOp`, `Unary` nodes.
|
||||
|
||||
- `Num`: returns `node.value` (already int or float from lexer)
|
||||
- `Unary('-')`: returns `-evaluate(operand)` recursively
|
||||
- `BinOp(+,-,*)`: straightforward arithmetic (Python int+int=int)
|
||||
- `BinOp(/)`: true division via `left / right`; checks `right == 0` → `EvalError`; if `result == int(result)` → return `int(result)` (D3 rule), else return float
|
||||
|
||||
D3 rule: after true division, `4/2 = 2.0`; `2.0 == int(2.0)` is True, so return `int(2.0) = 2`. `7/2 = 3.5`; `3.5 != 3`, so return `3.5` (float).
|
||||
|
||||
### calc.py
|
||||
|
||||
Top-level CLI at repo root. Imports from `calc.*`. Catches `LexError`, `ParseError`, `EvalError` → prints to stderr, exits 1. Traceback never escapes.
|
||||
|
||||
### test_evaluator.py
|
||||
|
||||
26 new tests across D1–D4 categories:
|
||||
- D1 (arithmetic): 11 tests including all DoD examples plus edge cases
|
||||
- D2 (division): 4 tests including EvalError-not-ZeroDivisionError check
|
||||
- D3 (result type): 5 tests including `str()` formatting
|
||||
- D4 (CLI): 6 tests using subprocess
|
||||
|
||||
### Verification output (local)
|
||||
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 63 tests in 0.228s
|
||||
OK
|
||||
```
|
||||
|
||||
```
|
||||
python calc.py "2+3*4" → 14
|
||||
python calc.py "(2+3)*4" → 20
|
||||
python calc.py "7/2" → 3.5
|
||||
python calc.py "4/2" → 2
|
||||
python calc.py "1/0" → error: division by zero (stderr, exit 1)
|
||||
python calc.py "1 +" → error: unexpected token 'EOF' (None) (stderr, exit 1)
|
||||
```
|
||||
@ -0,0 +1,30 @@
|
||||
# JOURNAL — Phase lex (Adversary)
|
||||
|
||||
## 2026-06-15T00:00:00Z — Initialized
|
||||
Adversary loop started. Read phase plan. No Builder activity yet. Watching for gate claims.
|
||||
|
||||
---
|
||||
|
||||
# JOURNAL — Phase lex (Builder)
|
||||
|
||||
## 2026-06-15 — Session 1
|
||||
|
||||
Implemented `calc/lexer.py` with `Token` dataclass, `LexError`, and `tokenize()`.
|
||||
|
||||
Test run:
|
||||
```
|
||||
$ 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', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
|
||||
calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
|
||||
Committed as `ab0332e`. All D1–D4 conditions met in one session.
|
||||
@ -0,0 +1,33 @@
|
||||
# JOURNAL — Phase parse
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Built `calc/parser.py` using a classic recursive-descent approach with three precedence levels:
|
||||
|
||||
1. `_expr` handles `+`/`-` (lowest precedence, left-assoc via loop)
|
||||
2. `_term` handles `*`/`/` (medium precedence, left-assoc via loop)
|
||||
3. `_unary` handles leading `-` (right-assoc via recursion)
|
||||
4. `_primary` handles numbers and parenthesized expressions
|
||||
|
||||
Left-associativity falls out naturally from the `while` loops in `_expr` and `_term` — each iteration wraps the accumulated `node` as the left child of a new `BinOp`, building a left-leaning tree.
|
||||
|
||||
Test verification output:
|
||||
```
|
||||
D1: BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))
|
||||
D2: BinOp(op='-', left=BinOp(op='-', left=Num(value=8), right=Num(value=3)), right=Num(value=2))
|
||||
D3: BinOp(op='*', left=BinOp(op='+', left=Num(value=1), right=Num(value=2)), right=Num(value=3))
|
||||
D4a: Unary(op='-', operand=Num(value=5))
|
||||
D4b: Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))
|
||||
D4c: BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))
|
||||
|
||||
D5 errors:
|
||||
OK ParseError for '1 +': unexpected token 'EOF' (None)
|
||||
OK ParseError for '(1': unclosed parenthesis, got 'EOF'
|
||||
OK ParseError for '1 2': unexpected token 'NUMBER' (2)
|
||||
OK ParseError for ')(': unexpected token 'RPAREN' (')')
|
||||
OK ParseError for '': empty input
|
||||
|
||||
D6: Ran 37 tests in 0.001s OK
|
||||
```
|
||||
|
||||
All 6 gates claimed and pushed. Awaiting Adversary verification.
|
||||
@ -0,0 +1,68 @@
|
||||
# REVIEW — Phase eval (Adversary)
|
||||
|
||||
## D1: PASS @2026-06-15T05:01Z
|
||||
|
||||
Cold-ran all five DoD arithmetic checks from the plan:
|
||||
- `2+3*4` → 14 ✓ (precedence: `*` before `+`)
|
||||
- `(2+3)*4` → 20 ✓ (parens override precedence)
|
||||
- `8-3-2` → 3 ✓ (left-associativity)
|
||||
- `-2+5` → 3 ✓ (unary minus)
|
||||
- `2*-3` → -6 ✓ (unary minus in binary context)
|
||||
|
||||
Break-it probes:
|
||||
- `--5` → 5 ✓ (double unary, recursive)
|
||||
- `((2+3))` → 5 ✓ (nested parens)
|
||||
- `1+2+3+4` → 10 ✓ (chain addition)
|
||||
- `2*3+4/2` → 8 ✓ (mixed precedence, `4/2` → int 2, `6+2` → int 8)
|
||||
|
||||
### D2: PASS @2026-06-15T05:01Z
|
||||
|
||||
- `7/2` → 3.5 (true division) ✓
|
||||
- `1/0` → raises `EvalError("division by zero")`, NOT `ZeroDivisionError` ✓
|
||||
- Break-it: `0/0` → `EvalError` ✓ (zero-zero handled by same `right == 0` check)
|
||||
- Break-it: `-1/0` → `EvalError` ✓
|
||||
|
||||
Code review: `evaluator.py:30–31` — explicit `if right == 0: raise EvalError(...)` before Python's `/` operator, so `ZeroDivisionError` can never escape. Correct.
|
||||
|
||||
### D3: PASS @2026-06-15T05:01Z
|
||||
|
||||
- `4/2` → `2` (type `int`) ✓
|
||||
- `7/2` → `3.5` (type `float`) ✓
|
||||
- Break-it: `6/3` → `2` int ✓
|
||||
- Break-it: `0/1` → `0` int ✓
|
||||
- Break-it: `2+3` (no division) → `5` int ✓ (integer arithmetic always stays int)
|
||||
- Break-it: `1/3` → `0.333...` float ✓
|
||||
|
||||
Code review: `evaluator.py:33–35` — `if result == int(result): return int(result)` applied only in the `/` branch. Correct scope.
|
||||
|
||||
CLI check: `python calc.py '4/2'` → `2` (no `.0`) ✓; `python calc.py '7/2'` → `3.5` ✓
|
||||
|
||||
### D4: PASS @2026-06-15T05:01Z
|
||||
|
||||
Valid expressions:
|
||||
- `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 ✓
|
||||
|
||||
Error cases:
|
||||
- `python calc.py "1/0"` → stderr `error: division by zero`, exit 1, stdout empty ✓
|
||||
- `python calc.py "1 +"` → stderr `error: unexpected token 'EOF' (None)`, exit 1, stdout empty ✓
|
||||
|
||||
Break-it probes:
|
||||
- No `Traceback` in stderr for either error case ✓
|
||||
- No-arg case (`python calc.py`) → stderr `usage: python calc.py <expression>`, exit 1 ✓
|
||||
- `LexError` also caught (imported and handled in `calc.py:17`) ✓
|
||||
|
||||
### D5: PASS @2026-06-15T05:01Z
|
||||
|
||||
Cold-ran: `python -m unittest -q`
|
||||
Output: `Ran 63 tests in 0.232s OK` — 0 failures ✓
|
||||
|
||||
Prior suite (37 lex+parse tests) still passes; 26 new evaluator tests added. No regressions.
|
||||
|
||||
Plan's exact Verify-section commands all ran with matching expected outputs.
|
||||
|
||||
## No adversary findings — all DoD gates verified PASS
|
||||
|
||||
All D1–D5 gates independently verified with break-it probes. No defects found. Builder may mark DONE.
|
||||
@ -0,0 +1,58 @@
|
||||
# REVIEW — Phase lex (Adversary)
|
||||
|
||||
## Verdicts
|
||||
|
||||
### lex/D1: PASS @2026-06-15T05:05Z
|
||||
Cold-run evidence:
|
||||
- `tokenize('42')` → `NUMBER 42 int` ✓ (int type confirmed)
|
||||
- `tokenize('3.14')` → `NUMBER 3.14 float` ✓ (float type confirmed)
|
||||
- `tokenize('.5')` → `NUMBER 0.5` ✓ (leading dot)
|
||||
- `tokenize('10.')` → `NUMBER 10.0` ✓ (trailing dot → float)
|
||||
- EOF appended in all cases ✓
|
||||
|
||||
---
|
||||
|
||||
### lex/D2: PASS @2026-06-15T05:05Z
|
||||
Cold-run evidence:
|
||||
- `tokenize('1+2*3')` → `[('NUMBER', 1), ('PLUS', '+'), ('NUMBER', 2), ('STAR', '*'), ('NUMBER', 3), ('EOF', None)]`
|
||||
- Matches expected exactly ✓
|
||||
- All 6 operator/paren kinds verified in test suite ✓
|
||||
|
||||
---
|
||||
|
||||
### lex/D3: PASS @2026-06-15T05:05Z
|
||||
Cold-run evidence:
|
||||
- `tokenize(' 12 + 3 ')` → `['NUMBER', 'PLUS', 'NUMBER', 'EOF']` ✓ (spaces skipped)
|
||||
- `tokenize('1 @ 2')` raises `calc.lexer.LexError: unexpected character '@' at position 2` ✓
|
||||
- Offending character `'@'` in message ✓
|
||||
- Position `2` in message ✓
|
||||
- Letters (`abc`), `$` also raise `LexError` per test suite ✓
|
||||
|
||||
---
|
||||
|
||||
### lex/D4: PASS @2026-06-15T05:05Z
|
||||
Cold-run evidence:
|
||||
- `python -m unittest -q` → `Ran 18 tests in 0.000s OK` ✓
|
||||
- `tokenize('3.5*(1-2)')` → `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]` ✓ (exact plan match)
|
||||
- `tokenize('1 @ 2')` raises `LexError` (exit 1) ✓
|
||||
- Required test cases present: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"` ✓
|
||||
- 18 tests, 0 failures ✓
|
||||
|
||||
---
|
||||
|
||||
## Adversary findings (non-blocking for this phase)
|
||||
|
||||
### AF-1: `ValueError` leaks on malformed number tokens
|
||||
**Repro:**
|
||||
```
|
||||
python -c "from calc.lexer import tokenize; tokenize('1.2.3')"
|
||||
# → ValueError: could not convert string to float: '1.2.3'
|
||||
|
||||
python -c "from calc.lexer import tokenize; tokenize('.')"
|
||||
# → ValueError: could not convert string to float: '.'
|
||||
```
|
||||
The number-scanning loop (`ch.isdigit() or ch == '.'`) greedily consumes all digits and dots, then hands the raw span to `float()` which raises `ValueError` on malformed input like `1.2.3` or bare `.`. These should raise `LexError` for consistency — the caller can't distinguish a lexer malfunction from a Python type error.
|
||||
|
||||
**Severity:** Not blocking — the DoD only requires `LexError` for invalid *characters* (`@`, `$`, letters). `1.2.3` and `.` are outside the explicit D1/D3 test cases. However, the parser phase will likely encounter these and must handle them.
|
||||
|
||||
**Recommendation:** Wrap the `float(raw)` call in a `try/except ValueError` and re-raise as `LexError`. Flag for builder attention in BUILDER-INBOX.
|
||||
@ -0,0 +1,41 @@
|
||||
# REVIEW — Phase parse (Adversary)
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1: PASS @2026-06-15T05:00Z
|
||||
Cold-ran: `python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"`
|
||||
Got: `BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))`
|
||||
Matches expected exactly. Break-it: `3*2+1` → `BinOp(+, BinOp(*, 3, 2), 1)` (correct — `*` binds tighter from left too).
|
||||
|
||||
### D2: PASS @2026-06-15T05:00Z
|
||||
Cold-ran: `8-3-2` → `BinOp(-, BinOp(-, Num(8), Num(3)), Num(2))` ✓
|
||||
`8/4/2` → `BinOp(/, BinOp(/, Num(8), Num(4)), Num(2))` ✓
|
||||
Break-it: `10-3-2-1` → fully left-assoc triple nesting ✓. Grammar's while-loop guarantees left-assoc.
|
||||
|
||||
### D3: PASS @2026-06-15T05:00Z
|
||||
Cold-ran: `(1+2)*3` → `BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))` ✓
|
||||
Break-it: `((2+3))*4` → `BinOp(*, BinOp(+, Num(2), Num(3)), Num(4))` ✓ (nested parens unwrap cleanly).
|
||||
|
||||
### D4: PASS @2026-06-15T05:00Z
|
||||
Cold-ran all three required cases:
|
||||
- `-5` → `Unary(op='-', operand=Num(value=5))` ✓
|
||||
- `-(1+2)` → `Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))` ✓
|
||||
- `3 * -2` → `BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))` ✓
|
||||
Break-it: `--5` → `Unary('-', Unary('-', Num(5)))` (recursive unary works, double-negation parses correctly).
|
||||
|
||||
### D5: PASS @2026-06-15T05:00Z
|
||||
Cold-ran all five required error cases — each raised `ParseError`, none raised a different exception:
|
||||
- `'1 +'` → `ParseError: unexpected token 'EOF' (None)` ✓
|
||||
- `'(1'` → `ParseError: unclosed parenthesis, got 'EOF'` ✓
|
||||
- `'1 2'` → `ParseError: unexpected token 'NUMBER' (2)` ✓
|
||||
- `')('` → `ParseError: unexpected token 'RPAREN' (')')` ✓
|
||||
- `''` → `ParseError: empty input` ✓
|
||||
Break-it: `1+2)` → `ParseError: unexpected token 'RPAREN' (')')` ✓ (trailing paren caught by EOF check).
|
||||
|
||||
### D6: PASS @2026-06-15T05:00Z
|
||||
Cold-ran: `python -m unittest -q`
|
||||
Output: `Ran 37 tests in 0.001s OK` (18 lexer + 19 parser, 0 failures) ✓
|
||||
|
||||
## No adversary findings — all DoD gates verified PASS
|
||||
|
||||
All D1–D6 gates verified independently. No defects found. Builder may mark DONE.
|
||||
@ -0,0 +1,115 @@
|
||||
# STATUS — Phase eval
|
||||
|
||||
## Gate D1 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** `evaluate(parse(tokenize(s)))` is correct for `+ - * /`, precedence, parens, and unary minus.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate
|
||||
def c(s): return evaluate(parse(tokenize(s)))
|
||||
assert c('2+3*4') == 14
|
||||
assert c('(2+3)*4') == 20
|
||||
assert c('8-3-2') == 3
|
||||
assert c('-2+5') == 3
|
||||
assert c('2*-3') == -6
|
||||
print('D1 OK')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:** prints `D1 OK`, no assertion errors.
|
||||
|
||||
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
|
||||
|
||||
---
|
||||
|
||||
## Gate D2 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** `/` is true division (`"7/2"` → 3.5). Division by zero raises `EvalError`, not `ZeroDivisionError`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate, EvalError
|
||||
def c(s): return evaluate(parse(tokenize(s)))
|
||||
assert c('7/2') == 3.5
|
||||
try:
|
||||
c('1/0')
|
||||
assert False, 'no error raised'
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
assert False, 'bare ZeroDivisionError escaped'
|
||||
print('D2 OK')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:** prints `D2 OK`, no assertion errors.
|
||||
|
||||
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
|
||||
|
||||
---
|
||||
|
||||
## Gate D3 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** Whole-valued results print without `.0` (`"4/2"` → `2`), non-whole as float (`"7/2"` → `3.5`).
|
||||
|
||||
Rule: after division, if `result == int(result)`, return `int(result)`; otherwise return `float`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate
|
||||
def c(s): return evaluate(parse(tokenize(s)))
|
||||
r1 = c('4/2'); assert r1 == 2 and isinstance(r1, int), f'got {r1!r}'
|
||||
r2 = c('7/2'); assert isinstance(r2, float) and r2 == 3.5, f'got {r2!r}'
|
||||
print('D3 OK')
|
||||
"
|
||||
python calc.py '4/2' # must print: 2
|
||||
python calc.py '7/2' # must print: 3.5
|
||||
```
|
||||
|
||||
**EXPECTED:** `D3 OK`, then `2`, then `3.5`.
|
||||
|
||||
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
|
||||
|
||||
---
|
||||
|
||||
## Gate D4 CLAIMED — awaiting Adversary
|
||||
|
||||
**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:**
|
||||
```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 non-zero, stdout empty
|
||||
python calc.py "1 +" # stderr: error: ..., exit non-zero, stdout empty
|
||||
```
|
||||
|
||||
**EXPECTED:** exactly as above — no Python traceback in stderr, error message starts with `error:`.
|
||||
|
||||
**WHERE:** `calc.py` @ commit `7167e33`
|
||||
|
||||
---
|
||||
|
||||
## Gate D5 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** Full test suite passes: `python -m unittest -q`, 0 failures. New `calc/test_evaluator.py` covers D1–D4. Prior lex+parse suite (37 tests) still passes (no regression). Total: 63 tests.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Ran 63 tests in X.XXXs
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
**WHERE:** `calc/test_evaluator.py` (26 new tests) + `calc/test_lexer.py` + `calc/test_parser.py` @ commit `7167e33`
|
||||
@ -0,0 +1,90 @@
|
||||
# STATUS — Phase lex
|
||||
|
||||
## DONE
|
||||
|
||||
All D1–D4 gates PASS (Adversary-verified @2026-06-15T05:05Z).
|
||||
Post-DoD fix applied: AF-1 (`ValueError` on malformed numbers now raises `LexError`).
|
||||
|
||||
---
|
||||
|
||||
## Adversary
|
||||
Initialized. Watching for Builder gate claims.
|
||||
|
||||
## Builder
|
||||
|
||||
### Gate D1 CLAIMED — PASS
|
||||
|
||||
**WHAT:** Integers and floats tokenize to NUMBER tokens with correct Python type.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; r=tokenize('42'); print(r[0].kind, r[0].value, type(r[0].value).__name__)"
|
||||
python -c "from calc.lexer import tokenize; r=tokenize('3.14'); print(r[0].kind, r[0].value, type(r[0].value).__name__)"
|
||||
python -c "from calc.lexer import tokenize; r=tokenize('.5'); print(r[0].kind, r[0].value)"
|
||||
python -c "from calc.lexer import tokenize; r=tokenize('10.'); print(r[0].kind, r[0].value)"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `NUMBER 42 int`
|
||||
- `NUMBER 3.14 float`
|
||||
- `NUMBER 0.5`
|
||||
- `NUMBER 10.0`
|
||||
- Final token in all cases: `Token(kind='EOF', value=None)`
|
||||
|
||||
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
|
||||
|
||||
---
|
||||
|
||||
### Gate D2 CLAIMED — PASS
|
||||
|
||||
**WHAT:** Operators `+ - * / ( )` tokenize to correct kinds.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('1+2*3')])"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
[('NUMBER', 1), ('PLUS', '+'), ('NUMBER', 2), ('STAR', '*'), ('NUMBER', 3), ('EOF', None)]
|
||||
```
|
||||
|
||||
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
|
||||
|
||||
---
|
||||
|
||||
### Gate D3 CLAIMED — PASS
|
||||
|
||||
**WHAT:** Whitespace skipped; invalid chars raise `LexError` with offending char and position.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize(' 12 + 3 ')])"
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
|
||||
- `calc.lexer.LexError: unexpected character '@' at position 2`
|
||||
|
||||
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
|
||||
|
||||
---
|
||||
|
||||
### Gate D4 CLAIMED — PASS
|
||||
|
||||
**WHAT:** `calc/test_lexer.py` passes under `python -m unittest`, 0 failures, 18 tests covering D1–D3 including the three required cases.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- unittest: `Ran 18 tests in X.XXXs OK`
|
||||
- expression: `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
|
||||
- LexError raised (exit 1)
|
||||
|
||||
**WHERE:** `calc/test_lexer.py`, `calc/lexer.py` @ commit `ab0332e`
|
||||
@ -0,0 +1,164 @@
|
||||
# STATUS — Phase parse
|
||||
|
||||
## DONE
|
||||
|
||||
All D1–D6 gates PASS (Adversary-verified @2026-06-15T05:00Z).
|
||||
|
||||
---
|
||||
|
||||
## Gate D6 CLAIMED — PASS
|
||||
|
||||
**WHAT:** `calc/test_parser.py` passes under `python -m unittest`, 0 failures, 37 total tests (18 lexer + 19 parser), covering D1–D5.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Ran 37 tests in X.XXXs
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
**WHERE:** `calc/test_parser.py` @ commit `866091c`
|
||||
|
||||
---
|
||||
|
||||
## Gate D5 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** Each of `"1 +"`, `"(1"`, `"1 2"`, `")("`, and `""` raises `ParseError` (not any other exception).
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "
|
||||
from calc.lexer import tokenize; from calc.parser import parse, ParseError
|
||||
for src in ['1 +', '(1', '1 2', ')(', '']:
|
||||
try:
|
||||
parse(tokenize(src))
|
||||
print(f'FAIL no error for {src!r}')
|
||||
except ParseError as e:
|
||||
print(f'OK ParseError for {src!r}: {e}')
|
||||
except Exception as e:
|
||||
print(f'FAIL wrong exception for {src!r}: {type(e).__name__}: {e}')
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
OK ParseError for '1 +': unexpected token 'EOF' (None)
|
||||
OK ParseError for '(1': unclosed parenthesis, got 'EOF'
|
||||
OK ParseError for '1 2': unexpected token 'NUMBER' (2)
|
||||
OK ParseError for ')(': unexpected token 'RPAREN' (')')
|
||||
OK ParseError for '': empty input
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` @ commit `866091c`
|
||||
|
||||
---
|
||||
|
||||
## Gate D4 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** Unary minus parses correctly for `-5`, `-(1+2)`, `3 * -2`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
Unary(op='-', operand=Num(value=5))
|
||||
Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))
|
||||
BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` @ commit `866091c`
|
||||
|
||||
---
|
||||
|
||||
## Gate D3 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** Parens override precedence: `(1+2)*3` parses as `BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
BinOp(op='*', left=BinOp(op='+', left=Num(value=1), right=Num(value=2)), right=Num(value=3))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` @ commit `866091c`
|
||||
|
||||
---
|
||||
|
||||
## Gate D2 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** Same-precedence operators associate left: `8-3-2` → `BinOp(-, BinOp(-, Num(8), Num(3)), Num(2))`; `8/4/2` → `BinOp(/, BinOp(/, Num(8), Num(4)), Num(2))`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
BinOp(op='-', left=BinOp(op='-', left=Num(value=8), right=Num(value=3)), right=Num(value=2))
|
||||
BinOp(op='/', left=BinOp(op='/', left=Num(value=8), right=Num(value=4)), right=Num(value=2))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` @ commit `866091c`
|
||||
|
||||
---
|
||||
|
||||
## Gate D1 CLAIMED — awaiting Adversary
|
||||
|
||||
**WHAT:** `*` and `/` bind tighter than `+` and `-`: `1+2*3` parses as `BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
```
|
||||
BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))
|
||||
```
|
||||
|
||||
**WHERE:** `calc/parser.py` @ commit `866091c`
|
||||
|
||||
---
|
||||
|
||||
## AST Shape
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Num:
|
||||
value: int | float
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str # '+', '-', '*', '/'
|
||||
left: Node
|
||||
right: Node
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str # '-'
|
||||
operand: Node
|
||||
```
|
||||
|
||||
Grammar:
|
||||
```
|
||||
expr = term (('+' | '-') term)*
|
||||
term = unary (('*' | '/') unary)*
|
||||
unary = '-' unary | primary
|
||||
primary = NUMBER | '(' expr ')'
|
||||
```
|
||||
Reference in New Issue
Block a user