artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
17
calculators/builder-adversary-min/run-03/GIT-LOG.txt
Normal file
17
calculators/builder-adversary-min/run-03/GIT-LOG.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
75bd35c status: eval phase DONE — all D1-D5 gates PASS per Adversary review at fe7e562
|
||||
434cde5 review(D1,D2,D3,D4,D5): PASS — all gates verified cold at fe7e562
|
||||
df2f5da claim(D1,D2,D3,D4,D5): eval phase — evaluator + CLI + tests at fe7e562
|
||||
fe7e562 feat: implement evaluator, CLI, and tests (D1-D5) for eval phase
|
||||
710b731 review(init): Adversary online for eval phase, awaiting Builder commits
|
||||
0afab21 review(D1,D2,D3,D4,D5,D6): PASS — all gates verified cold at fa50146
|
||||
b61ad6d claim(D1-D6): parse phase — all gates verified, 36 tests green
|
||||
fa50146 status: add commit sha 14bbe57 to STATUS-parse
|
||||
14bbe57 feat: implement calc parser (D1-D6) — parse() with Num/BinOp/Unary AST
|
||||
fc470e0 status: phase lex DONE — all D1-D4 gates PASS per Adversary review
|
||||
1ca2b4c review(D1,D2,D3,D4): PASS — all gates verified cold at ba1f056
|
||||
35de6d4 journal: add lex phase build notes
|
||||
c465844 status: add commit sha ba1f056 to STATUS-lex
|
||||
ba1f056 feat: implement calc lexer (D1-D4) — tokenize() with Token/LexError
|
||||
4909b16 review(init): Adversary online, awaiting Builder commits
|
||||
cae8791 chore: seed
|
||||
1
calculators/builder-adversary-min/run-03/README.md
Normal file
1
calculators/builder-adversary-min/run-03/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary-min/run-03/SOURCE.txt
Normal file
1
calculators/builder-adversary-min/run-03/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary-min/r3
|
||||
22
calculators/builder-adversary-min/run-03/calc.py
Normal file
22
calculators/builder-adversary-min/run-03/calc.py
Normal file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from calc.lexer import tokenize, LexError
|
||||
from calc.parser import parse, ParseError
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: calc.py <expression>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
expr = sys.argv[1]
|
||||
try:
|
||||
result = evaluate(parse(tokenize(expr)))
|
||||
except (LexError, ParseError, EvalError) as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
42
calculators/builder-adversary-min/run-03/calc/evaluator.py
Normal file
42
calculators/builder-adversary-min/run-03/calc/evaluator.py
Normal file
@ -0,0 +1,42 @@
|
||||
from calc.parser import Num, BinOp, Unary, Node
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(node: Node):
|
||||
"""Walk the AST and return int | float.
|
||||
|
||||
Result type rule: if the result is mathematically an integer (no
|
||||
fractional part), return int; otherwise return float.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, Unary):
|
||||
val = evaluate(node.operand)
|
||||
if node.op == '-':
|
||||
return _coerce(-val)
|
||||
raise EvalError(f"unknown unary op {node.op!r}")
|
||||
if isinstance(node, BinOp):
|
||||
left = evaluate(node.left)
|
||||
right = evaluate(node.right)
|
||||
op = node.op
|
||||
if op == '+':
|
||||
return _coerce(left + right)
|
||||
if op == '-':
|
||||
return _coerce(left - right)
|
||||
if op == '*':
|
||||
return _coerce(left * right)
|
||||
if op == '/':
|
||||
if right == 0:
|
||||
raise EvalError("division by zero")
|
||||
return _coerce(left / right)
|
||||
raise EvalError(f"unknown binary op {op!r}")
|
||||
raise EvalError(f"unknown node type {type(node).__name__}")
|
||||
|
||||
|
||||
def _coerce(value):
|
||||
if isinstance(value, float) and value == int(value):
|
||||
return int(value)
|
||||
return value
|
||||
52
calculators/builder-adversary-min/run-03/calc/lexer.py
Normal file
52
calculators/builder-adversary-min/run-03/calc/lexer.py
Normal file
@ -0,0 +1,52 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: Union[int, float, str, None]
|
||||
|
||||
|
||||
def tokenize(src: str) -> list:
|
||||
tokens = []
|
||||
i = 0
|
||||
n = len(src)
|
||||
while i < n:
|
||||
ch = src[i]
|
||||
if ch in ' \t':
|
||||
i += 1
|
||||
elif ch == '+':
|
||||
tokens.append(Token('PLUS', '+'))
|
||||
i += 1
|
||||
elif ch == '-':
|
||||
tokens.append(Token('MINUS', '-'))
|
||||
i += 1
|
||||
elif ch == '*':
|
||||
tokens.append(Token('STAR', '*'))
|
||||
i += 1
|
||||
elif ch == '/':
|
||||
tokens.append(Token('SLASH', '/'))
|
||||
i += 1
|
||||
elif ch == '(':
|
||||
tokens.append(Token('LPAREN', '('))
|
||||
i += 1
|
||||
elif ch == ')':
|
||||
tokens.append(Token('RPAREN', ')'))
|
||||
i += 1
|
||||
elif ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
while j < n and (src[j].isdigit() or src[j] == '.'):
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
value = float(raw) if '.' in raw else int(raw)
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
else:
|
||||
raise LexError(f"unexpected character {ch!r} at position {i}")
|
||||
tokens.append(Token('EOF', None))
|
||||
return tokens
|
||||
118
calculators/builder-adversary-min/run-03/calc/parser.py
Normal file
118
calculators/builder-adversary-min/run-03/calc/parser.py
Normal file
@ -0,0 +1,118 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union, List
|
||||
from calc.lexer import Token
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
def __repr__(self):
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: 'Node'
|
||||
right: 'Node'
|
||||
|
||||
def __repr__(self):
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: 'Node'
|
||||
|
||||
def __repr__(self):
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
|
||||
Node = Union[Num, BinOp, Unary]
|
||||
|
||||
|
||||
class _Parser:
|
||||
def __init__(self, tokens: List[Token]):
|
||||
self.tokens = tokens
|
||||
self.pos = 0
|
||||
|
||||
def _peek(self) -> Token:
|
||||
return self.tokens[self.pos]
|
||||
|
||||
def _consume(self, kind: str = None) -> Token:
|
||||
tok = self.tokens[self.pos]
|
||||
if kind and tok.kind != kind:
|
||||
raise ParseError(f"expected {kind!r}, got {tok.kind!r} ({tok.value!r})")
|
||||
self.pos += 1
|
||||
return tok
|
||||
|
||||
def parse(self) -> Node:
|
||||
if self._peek().kind == 'EOF':
|
||||
raise ParseError("empty input")
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'EOF':
|
||||
raise ParseError(
|
||||
f"unexpected token {self._peek().kind!r} ({self._peek().value!r})"
|
||||
)
|
||||
return node
|
||||
|
||||
def _expr(self) -> Node:
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op = self._consume().value
|
||||
right = self._term()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _term(self) -> Node:
|
||||
node = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op = self._consume().value
|
||||
right = self._unary()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _unary(self) -> Node:
|
||||
if self._peek().kind == 'MINUS':
|
||||
op = self._consume().value
|
||||
return Unary(op, self._unary())
|
||||
return self._primary()
|
||||
|
||||
def _primary(self) -> Node:
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._consume()
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._consume()
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'RPAREN':
|
||||
raise ParseError(
|
||||
f"expected ')', got {self._peek().kind!r} ({self._peek().value!r})"
|
||||
)
|
||||
self._consume()
|
||||
return node
|
||||
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
|
||||
|
||||
|
||||
def parse(tokens: List[Token]) -> Node:
|
||||
"""Parse a flat token list into an AST Node.
|
||||
|
||||
AST shape
|
||||
---------
|
||||
Num(value) — numeric literal (int or float)
|
||||
BinOp(op, left, right) — binary op; op in {'+', '-', '*', '/'}
|
||||
Unary(op, operand) — unary op; op == '-'
|
||||
|
||||
Precedence (high to low): unary-minus > * / > + -
|
||||
Associativity: left for all binary operators.
|
||||
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
108
calculators/builder-adversary-min/run-03/calc/test_evaluator.py
Normal file
108
calculators/builder-adversary-min/run-03/calc/test_evaluator.py
Normal file
@ -0,0 +1,108 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
|
||||
|
||||
def calc(expr):
|
||||
return evaluate(parse(tokenize(expr)))
|
||||
|
||||
|
||||
class TestD1Arithmetic(unittest.TestCase):
|
||||
def test_add_mul_precedence(self):
|
||||
self.assertEqual(calc("2+3*4"), 14)
|
||||
|
||||
def test_parens(self):
|
||||
self.assertEqual(calc("(2+3)*4"), 20)
|
||||
|
||||
def test_left_assoc_sub(self):
|
||||
self.assertEqual(calc("8-3-2"), 3)
|
||||
|
||||
def test_unary_minus(self):
|
||||
self.assertEqual(calc("-2+5"), 3)
|
||||
|
||||
def test_mul_unary(self):
|
||||
self.assertEqual(calc("2*-3"), -6)
|
||||
|
||||
|
||||
class TestD2Division(unittest.TestCase):
|
||||
def test_true_division(self):
|
||||
self.assertEqual(calc("7/2"), 3.5)
|
||||
|
||||
def test_div_by_zero_raises_eval_error(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("1/0")
|
||||
|
||||
def test_div_by_zero_not_bare(self):
|
||||
try:
|
||||
calc("1/0")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("ZeroDivisionError escaped; should be EvalError")
|
||||
|
||||
|
||||
class TestD3ResultType(unittest.TestCase):
|
||||
def test_whole_division_returns_int(self):
|
||||
result = calc("4/2")
|
||||
self.assertIsInstance(result, int)
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
def test_non_whole_division_returns_float(self):
|
||||
result = calc("7/2")
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertEqual(result, 3.5)
|
||||
|
||||
def test_int_arithmetic_stays_int(self):
|
||||
self.assertIsInstance(calc("2+3*4"), int)
|
||||
|
||||
def test_negative_whole_is_int(self):
|
||||
result = calc("-4/2")
|
||||
self.assertIsInstance(result, int)
|
||||
self.assertEqual(result, -2)
|
||||
|
||||
|
||||
class TestD4CLI(unittest.TestCase):
|
||||
def _run(self, expr):
|
||||
return subprocess.run(
|
||||
[sys.executable, "calc.py", expr],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
def test_basic_expression(self):
|
||||
r = self._run("2+3*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "14")
|
||||
|
||||
def test_parens_cli(self):
|
||||
r = self._run("(2+3)*4")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "20")
|
||||
|
||||
def test_float_result(self):
|
||||
r = self._run("7/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "3.5")
|
||||
|
||||
def test_whole_float_no_decimal(self):
|
||||
r = self._run("4/2")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertEqual(r.stdout.strip(), "2")
|
||||
|
||||
def test_invalid_expression_exits_nonzero(self):
|
||||
r = self._run("1 +")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertTrue(r.stderr.strip(), "expected error on stderr")
|
||||
|
||||
def test_div_by_zero_exits_nonzero(self):
|
||||
r = self._run("1/0")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertTrue(r.stderr.strip())
|
||||
self.assertNotIn("Traceback", r.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
101
calculators/builder-adversary-min/run-03/calc/test_lexer.py
Normal file
101
calculators/builder-adversary-min/run-03/calc/test_lexer.py
Normal file
@ -0,0 +1,101 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize, Token, LexError
|
||||
|
||||
|
||||
def kinds(src):
|
||||
return [t.kind for t in tokenize(src)]
|
||||
|
||||
|
||||
def values(src):
|
||||
return [(t.kind, t.value) for t in tokenize(src)]
|
||||
|
||||
|
||||
class TestNumbers(unittest.TestCase):
|
||||
def test_integer(self):
|
||||
result = tokenize("42")
|
||||
self.assertEqual(result[0], Token('NUMBER', 42))
|
||||
self.assertEqual(result[1], Token('EOF', None))
|
||||
|
||||
def test_float(self):
|
||||
t = tokenize("3.14")[0]
|
||||
self.assertEqual(t.kind, 'NUMBER')
|
||||
self.assertAlmostEqual(t.value, 3.14)
|
||||
|
||||
def test_leading_dot(self):
|
||||
t = tokenize(".5")[0]
|
||||
self.assertEqual(t.kind, 'NUMBER')
|
||||
self.assertAlmostEqual(t.value, 0.5)
|
||||
|
||||
def test_trailing_dot(self):
|
||||
t = tokenize("10.")[0]
|
||||
self.assertEqual(t.kind, 'NUMBER')
|
||||
self.assertAlmostEqual(t.value, 10.0)
|
||||
|
||||
def test_integer_value_type(self):
|
||||
t = tokenize("42")[0]
|
||||
self.assertIsInstance(t.value, int)
|
||||
|
||||
def test_float_value_type(self):
|
||||
t = tokenize("3.14")[0]
|
||||
self.assertIsInstance(t.value, float)
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
def test_all_operators(self):
|
||||
result = kinds("1+2*3")
|
||||
self.assertEqual(result, ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_minus(self):
|
||||
self.assertIn('MINUS', kinds("1-2"))
|
||||
|
||||
def test_slash(self):
|
||||
self.assertIn('SLASH', kinds("1/2"))
|
||||
|
||||
def test_parens(self):
|
||||
ks = kinds("(1)")
|
||||
self.assertEqual(ks[0], 'LPAREN')
|
||||
self.assertEqual(ks[2], 'RPAREN')
|
||||
|
||||
def test_complex_expr(self):
|
||||
result = values("3.5*(1-2)")
|
||||
expected_kinds = ['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF']
|
||||
self.assertEqual([k for k, _ in result], expected_kinds)
|
||||
self.assertAlmostEqual(result[0][1], 3.5)
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
def test_whitespace_skipped(self):
|
||||
result = kinds(" 12 + 3 ")
|
||||
self.assertEqual(result, ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_whitespace_values(self):
|
||||
result = values(" 12 + 3 ")
|
||||
self.assertEqual(result[0], ('NUMBER', 12))
|
||||
self.assertEqual(result[1], ('PLUS', '+'))
|
||||
self.assertEqual(result[2], ('NUMBER', 3))
|
||||
|
||||
def test_tab_skipped(self):
|
||||
result = kinds("1\t+\t2")
|
||||
self.assertEqual(result, ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_lex_error_at_sign(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_lex_error_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('2', str(ctx.exception))
|
||||
|
||||
def test_lex_error_letter(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("1 + x")
|
||||
|
||||
def test_lex_error_dollar(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$10")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
97
calculators/builder-adversary-min/run-03/calc/test_parser.py
Normal file
97
calculators/builder-adversary-min/run-03/calc/test_parser.py
Normal file
@ -0,0 +1,97 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError, Num, BinOp, Unary
|
||||
|
||||
|
||||
def p(src):
|
||||
return parse(tokenize(src))
|
||||
|
||||
|
||||
class TestPrecedence(unittest.TestCase):
|
||||
def test_add_mul(self):
|
||||
# 1+2*3 must parse as 1+(2*3), not (1+2)*3
|
||||
self.assertEqual(p('1+2*3'), BinOp('+', Num(1), BinOp('*', Num(2), Num(3))))
|
||||
|
||||
def test_div_add(self):
|
||||
# 6/2+1 must parse as (6/2)+1
|
||||
self.assertEqual(p('6/2+1'), BinOp('+', BinOp('/', Num(6), Num(2)), Num(1)))
|
||||
|
||||
def test_mul_sub(self):
|
||||
# 4-2*3 => 4-(2*3)
|
||||
self.assertEqual(p('4-2*3'), BinOp('-', Num(4), BinOp('*', Num(2), Num(3))))
|
||||
|
||||
|
||||
class TestLeftAssoc(unittest.TestCase):
|
||||
def test_subtraction(self):
|
||||
# 8-3-2 => (8-3)-2
|
||||
self.assertEqual(p('8-3-2'), BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
|
||||
|
||||
def test_division(self):
|
||||
# 8/4/2 => (8/4)/2
|
||||
self.assertEqual(p('8/4/2'), BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
|
||||
|
||||
def test_addition(self):
|
||||
# 1+2+3 => (1+2)+3
|
||||
self.assertEqual(p('1+2+3'), BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
def test_parens_override_precedence(self):
|
||||
# (1+2)*3 => BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
self.assertEqual(
|
||||
p('(1+2)*3'),
|
||||
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(p('((5))'), Num(5))
|
||||
|
||||
def test_parens_in_add(self):
|
||||
# 2*(3+4) => BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))
|
||||
self.assertEqual(
|
||||
p('2*(3+4)'),
|
||||
BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))
|
||||
)
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
self.assertEqual(p('-5'), Unary('-', Num(5)))
|
||||
|
||||
def test_paren(self):
|
||||
# -(1+2) => Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
self.assertEqual(p('-(1+2)'), Unary('-', BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_after_mul(self):
|
||||
# 3 * -2 => BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
self.assertEqual(p('3 * -2'), BinOp('*', Num(3), Unary('-', Num(2))))
|
||||
|
||||
def test_double_unary(self):
|
||||
# --5 => Unary('-', Unary('-', Num(5)))
|
||||
self.assertEqual(p('--5'), Unary('-', Unary('-', Num(5))))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
def test_trailing_op(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('1 +')
|
||||
|
||||
def test_unclosed_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('(1')
|
||||
|
||||
def test_extra_number(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('1 2')
|
||||
|
||||
def test_close_before_open(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p(')(')
|
||||
|
||||
def test_empty_string(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@ -0,0 +1,11 @@
|
||||
# JOURNAL — phase eval
|
||||
|
||||
## Implementation notes
|
||||
|
||||
**evaluator.py**: Walks the AST recursively. Num returns its value directly. BinOp evaluates left/right then applies op. Division by zero is intercepted and re-raised as EvalError. All results pass through `_coerce()` which converts whole-valued floats to int.
|
||||
|
||||
**_coerce rule**: `if isinstance(value, float) and value == int(value): return int(value)`. This handles `4/2 = 2.0 → 2` and `-4/2 = -2.0 → -2` correctly. Pure int arithmetic stays int throughout (int + int = int in Python, so no coercion needed there).
|
||||
|
||||
**calc.py**: Catches LexError, ParseError, EvalError and prints to stderr with exit 1. No traceback exposed.
|
||||
|
||||
**test_evaluator.py**: 18 tests. D1 covers all 5 mandated expressions. D2 covers true division, EvalError raise, and confirms ZeroDivisionError doesn't escape. D3 checks isinstance for int/float. D4 uses subprocess to exercise CLI end-to-end.
|
||||
@ -0,0 +1,13 @@
|
||||
# Journal — phase `lex`
|
||||
|
||||
## 2026-06-15
|
||||
|
||||
Built `calc/lexer.py` with `Token` dataclass, `LexError`, and `tokenize()`.
|
||||
|
||||
Design notes:
|
||||
- `Token` is a dataclass with `kind: str` and `value: Union[int, float, str, None]`; EOF has `value=None`, operators carry their char as value, numbers carry the parsed numeric value.
|
||||
- Number parsing: scans while digit or `.`; uses `int()` if no dot else `float()`.
|
||||
- LexError message includes the offending character (quoted) and its 0-based position.
|
||||
- 18 tests cover all D1–D3 requirements including the plan's required expressions.
|
||||
|
||||
Committed ba1f056, then c465844 (STATUS sha update). Waiting for Adversary review.
|
||||
@ -0,0 +1,21 @@
|
||||
# JOURNAL — phase parse
|
||||
|
||||
## Build notes
|
||||
|
||||
Implemented a classic recursive-descent parser.
|
||||
|
||||
Grammar (precedence lowest → highest):
|
||||
```
|
||||
expr → term (('+' | '-') term)*
|
||||
term → unary (('*' | '/') unary)*
|
||||
unary → '-' unary | primary
|
||||
primary→ NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
The `while` loops in `_expr` and `_term` produce left-associative trees naturally: each iteration wraps the accumulated left subtree as the new `node`, so 8-3-2 folds as ((8-3)-2).
|
||||
|
||||
Unary minus recurses right (`_unary` calls itself) to allow `--5` to parse as `Unary('-', Unary('-', Num(5)))`.
|
||||
|
||||
Empty input is caught by checking for EOF before entering `_expr`. Trailing tokens (e.g. `1 2`) are caught by the EOF check after `_expr` returns. Unclosed parens are caught in `_primary` when expecting RPAREN but finding EOF.
|
||||
|
||||
36 tests: 3 precision, 3 left-assoc, 3 paren, 4 unary, 5 error, plus 18 from lexer phase.
|
||||
@ -0,0 +1,64 @@
|
||||
# REVIEW — phase eval
|
||||
|
||||
_Adversary verifies each gate cold from own clone at commit `fe7e562`._
|
||||
|
||||
## review(init): Adversary online @2026-06-15T02:35Z
|
||||
|
||||
Eval plan read. DoD gates D1–D5 noted. Waiting for Builder to push evaluator.py, calc.py, and test_evaluator.py.
|
||||
|
||||
---
|
||||
|
||||
## D1: PASS @2026-06-15T02:37Z
|
||||
|
||||
Cold run of all five mandated expressions:
|
||||
- `python calc.py "2+3*4"` → `14` ✓
|
||||
- `python calc.py "(2+3)*4"` → `20` ✓
|
||||
- `python calc.py "8-3-2"` → `3` ✓
|
||||
- `python calc.py "-2+5"` → `3` ✓
|
||||
- `python calc.py "2*-3"` → `-6` ✓
|
||||
|
||||
Adversarial: `--5` → `5` (double-unary handled correctly via recursive `_unary`) ✓; `2*(3+4)-1` → `13` ✓
|
||||
|
||||
## D2: PASS @2026-06-15T02:37Z
|
||||
|
||||
Cold run:
|
||||
- `python calc.py "7/2"` → `3.5` ✓ (true division, not floor)
|
||||
- `python calc.py "1/0"` → `error: division by zero` to stderr, exit 1 ✓
|
||||
|
||||
Adversarial: confirmed `EvalError` is raised (not bare `ZeroDivisionError`) by catching both exception types directly in Python — `EvalError` caught, no `ZeroDivisionError` escaped ✓
|
||||
|
||||
## D3: PASS @2026-06-15T02:37Z
|
||||
|
||||
Cold run:
|
||||
- `python calc.py "4/2"` → `2` (int, no trailing `.0`) ✓
|
||||
- `python calc.py "7/2"` → `3.5` (float) ✓
|
||||
|
||||
Adversarial edge cases:
|
||||
- `-4/2` → `-2` (int, not `-2.0`) ✓
|
||||
- `0/5` → `0` (int) ✓
|
||||
- `1.5+0.5` → `2` (int, float sum coerced when whole) ✓
|
||||
- `_coerce` correctly uses `value == int(value)` check ✓
|
||||
|
||||
## D4: PASS @2026-06-15T02:37Z
|
||||
|
||||
Cold run:
|
||||
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
|
||||
- `python calc.py "1 +"` → stderr `error: unexpected token 'EOF' (None)`, exit 1 ✓
|
||||
- `python calc.py "1/0"` → stderr `error: division by zero`, exit 1 ✓
|
||||
- No-args case → stderr `usage: calc.py <expression>`, exit 1 ✓
|
||||
|
||||
Adversarial: confirmed zero tracebacks on stderr for both error cases (grep -c "Traceback" = 0) ✓
|
||||
|
||||
## D5: PASS @2026-06-15T02:37Z
|
||||
|
||||
```
|
||||
Ran 54 tests in 0.209s
|
||||
OK
|
||||
```
|
||||
|
||||
54 tests (18 eval + 36 lex+parse), 0 failures. No regression in prior suite.
|
||||
`calc/test_evaluator.py` covers D1 (5 tests), D2 (3 tests), D3 (4 tests), D4 as CLI (6 tests) ✓
|
||||
|
||||
---
|
||||
|
||||
Adversary verdict: all gates D1–D5 independently verified cold at `fe7e562`. Implementation is correct.
|
||||
@ -0,0 +1,43 @@
|
||||
# REVIEW-lex — Adversary verdicts
|
||||
|
||||
_Adversary verifies each gate cold from its own clone._
|
||||
|
||||
## Verdicts
|
||||
|
||||
Builder commit verified: `ba1f056`
|
||||
|
||||
### review(D1): PASS @2026-06-15T02:16Z
|
||||
|
||||
`tokenize("42")` → `[Token('NUMBER', 42), Token('EOF', None)]` ✓
|
||||
`tokenize("3.14")` → `[Token('NUMBER', 3.14), Token('EOF', None)]` ✓
|
||||
`tokenize(".5")` → `[Token('NUMBER', 0.5), Token('EOF', None)]` ✓
|
||||
`tokenize("10.")` → `[Token('NUMBER', 10.0), Token('EOF', None)]` ✓
|
||||
Integers produce `int`, floats produce `float`. Token dataclass has `kind` and `value`.
|
||||
|
||||
### review(D2): PASS @2026-06-15T02:16Z
|
||||
|
||||
`tokenize("1+2*3")` → `NUMBER PLUS NUMBER STAR NUMBER EOF` ✓
|
||||
All six kinds (PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN) confirmed via test suite and manual probe.
|
||||
|
||||
### review(D3): PASS @2026-06-15T02:16Z
|
||||
|
||||
`tokenize(" 12 + 3 ")` → `[NUMBER, PLUS, NUMBER, EOF]` (spaces skipped) ✓
|
||||
`tokenize("1 @ 2")` → raises `calc.lexer.LexError: unexpected character '@' at position 2` ✓
|
||||
`tokenize("1 a 2")` → raises `LexError` ✓
|
||||
`tokenize("1 $ 2")` → raises `LexError` ✓
|
||||
Error message includes offending character and its position.
|
||||
|
||||
### review(D4): PASS @2026-06-15T02:16Z
|
||||
|
||||
```
|
||||
python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 18 tests in 0.000s
|
||||
|
||||
OK
|
||||
```
|
||||
Test file contains all three plan-required cases: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"` raises `LexError`. ✓
|
||||
|
||||
## Summary
|
||||
|
||||
All four gates PASS. Builder commit `ba1f056` is clean. No veto.
|
||||
@ -0,0 +1,53 @@
|
||||
# REVIEW — phase parse
|
||||
|
||||
## D1: PASS @2026-06-15T02:27:10Z
|
||||
|
||||
Cold run: `parse(tokenize('1+2*3'))` → `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`
|
||||
Structure assertion passed. `*`/`/` correctly bind tighter than `+`/`-`.
|
||||
Also verified: `6/2+1` → `BinOp('+', BinOp('/', Num(6), Num(2)), Num(1))` (div-then-add order correct).
|
||||
Multi-level: `1+2*3+4` → `BinOp('+', BinOp('+', Num(1), BinOp('*', Num(2), Num(3))), Num(4))` ✓
|
||||
|
||||
## D2: PASS @2026-06-15T02:27:10Z
|
||||
|
||||
Cold run:
|
||||
- `8-3-2` → `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` ✓
|
||||
- `8/4/2` → `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` ✓
|
||||
|
||||
Left associativity correct for both addition and division via the while-loop in `_expr`/`_term`.
|
||||
|
||||
## D3: PASS @2026-06-15T02:27:10Z
|
||||
|
||||
Cold run: `(1+2)*3` → `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` ✓
|
||||
Also verified: `((5))` → `Num(5)`, `2*(3+4)` → `BinOp('*', Num(2), BinOp('+', Num(3), Num(4)))` ✓
|
||||
|
||||
## D4: PASS @2026-06-15T02:27:10Z
|
||||
|
||||
Cold run:
|
||||
- `-5` → `Unary('-', Num(5))` ✓
|
||||
- `-(1+2)` → `Unary('-', BinOp('+', Num(1), Num(2)))` ✓
|
||||
- `3 * -2` → `BinOp('*', Num(3), Unary('-', Num(2)))` ✓
|
||||
- `--5` → `Unary('-', Unary('-', Num(5)))` ✓ (recursive _unary handles chaining)
|
||||
|
||||
## D5: PASS @2026-06-15T02:27:10Z
|
||||
|
||||
All five mandated error inputs raise `ParseError` (not any other exception):
|
||||
- `'1 +'` → ParseError: unexpected token 'EOF' ✓
|
||||
- `'(1'` → ParseError: expected ')', got 'EOF' ✓
|
||||
- `'1 2'` → ParseError: unexpected token 'NUMBER' ✓
|
||||
- `')('` → ParseError: unexpected token 'RPAREN' ✓
|
||||
- `''` → ParseError: empty input ✓
|
||||
|
||||
Adversarial: `+5` (unary plus) correctly raises ParseError (not in grammar, no crash) ✓
|
||||
|
||||
## D6: PASS @2026-06-15T02:27:10Z
|
||||
|
||||
```
|
||||
Ran 36 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
36 tests, 0 failures. Suite covers D1–D5 with structure-level assertions (not evaluation).
|
||||
|
||||
---
|
||||
|
||||
Adversary verdict: all gates D1–D6 independently verified cold. Implementation is correct.
|
||||
@ -0,0 +1,52 @@
|
||||
# STATUS — phase eval
|
||||
|
||||
## DONE
|
||||
|
||||
## Gates claimed: D1, D2, D3, D4, D5
|
||||
|
||||
### What is claimed
|
||||
|
||||
| Gate | Description |
|
||||
|------|-------------|
|
||||
| D1 | Arithmetic correctness: `+`, `-`, `*`, `/`, precedence, parens, unary minus |
|
||||
| D2 | True division; division by zero raises `EvalError` (not `ZeroDivisionError`) |
|
||||
| D3 | Whole results → `int`, non-whole → `float` |
|
||||
| D4 | CLI `calc.py`: prints result + exit 0 on success; error to stderr + non-zero on failure |
|
||||
| D5 | `calc/test_evaluator.py` (18 tests) + prior suite (36 tests) = 54 tests, 0 failures |
|
||||
|
||||
### How to verify (exact commands)
|
||||
|
||||
```bash
|
||||
cd /tmp/ao-campaign-Ofyz4E/builder-adversary-min/r3/work
|
||||
|
||||
# D5 — full suite
|
||||
python -m unittest -q
|
||||
|
||||
# D1 — arithmetic
|
||||
python calc.py "2+3*4" # expected: 14
|
||||
python calc.py "(2+3)*4" # expected: 20
|
||||
python calc.py "8-3-2" # expected: 3
|
||||
python calc.py "-2+5" # expected: 3
|
||||
python calc.py "2*-3" # expected: -6
|
||||
|
||||
# D2 — true division + EvalError
|
||||
python calc.py "7/2" # expected: 3.5
|
||||
python calc.py "1/0" # expected: error to stderr, exit non-zero
|
||||
|
||||
# D3 — result type
|
||||
python calc.py "4/2" # expected: 2 (int, no .0)
|
||||
python calc.py "7/2" # expected: 3.5 (float)
|
||||
|
||||
# D4 — CLI error handling
|
||||
python calc.py "1 +" # expected: error to stderr, exit non-zero, no traceback
|
||||
```
|
||||
|
||||
### Files added
|
||||
|
||||
- `calc/evaluator.py` — `evaluate(node) -> int | float`, `EvalError`
|
||||
- `calc.py` — top-level CLI
|
||||
- `calc/test_evaluator.py` — 18 unittest tests covering D1–D4
|
||||
|
||||
### Commit SHA
|
||||
|
||||
`fe7e562080ee15b1f13f962171cdc4719734b062`
|
||||
@ -0,0 +1,56 @@
|
||||
# Status — phase `lex`
|
||||
|
||||
## Claimed gates
|
||||
|
||||
- **D1** — integers and floats tokenize to NUMBER with correct numeric value
|
||||
- **D2** — `+ - * / ( )` tokenize to the right kinds
|
||||
- **D3** — whitespace skipped; invalid chars raise `LexError` with offending char and position
|
||||
- **D4** — `calc/test_lexer.py` passes `python -m unittest` with 0 failures
|
||||
|
||||
## How to verify (exact commands)
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
|
||||
# D4 — all tests green
|
||||
python -m unittest -q
|
||||
|
||||
# D1/D2 — token kinds and values for 3.5*(1-2)
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
|
||||
# D3 — LexError raised for invalid char
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
```
|
||||
|
||||
## Expected results
|
||||
|
||||
`python -m unittest -q`:
|
||||
```
|
||||
Ran 18 tests in 0.000s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
`tokenize('3.5*(1-2)')`:
|
||||
```
|
||||
[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]
|
||||
```
|
||||
|
||||
`tokenize('1 @ 2')`:
|
||||
```
|
||||
raises calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `calc/__init__.py` — package marker
|
||||
- `calc/lexer.py` — `Token`, `LexError`, `tokenize()`
|
||||
- `calc/test_lexer.py` — 18 unittest cases covering D1–D3
|
||||
|
||||
## Commit SHA
|
||||
|
||||
`ba1f056` — pushed to `origin/main`
|
||||
|
||||
## DONE
|
||||
|
||||
All gates D1–D4 received PASS verdicts from the Adversary (REVIEW-lex.md @2026-06-15T02:16Z). No veto. Phase `lex` is complete.
|
||||
@ -0,0 +1,91 @@
|
||||
# STATUS — phase parse
|
||||
|
||||
## DONE
|
||||
|
||||
## Claimed gates: D1, D2, D3, D4, D5, D6
|
||||
|
||||
Commit: fa50146a5acbc9e7cf65a1e60e0b0fba2d8bd2ea (main)
|
||||
Files: `calc/parser.py`, `calc/test_parser.py`
|
||||
|
||||
---
|
||||
|
||||
## What is claimed
|
||||
|
||||
- **D1 — precedence**: `*`/`/` bind tighter than `+`/`-`
|
||||
- **D2 — left associativity**: same-precedence ops are left-associative
|
||||
- **D3 — parentheses**: parens override default precedence
|
||||
- **D4 — unary minus**: leading and nested unary minus handled
|
||||
- **D5 — errors**: malformed inputs raise `ParseError` (not any other exception)
|
||||
- **D6 — tests green**: `python -m unittest -q` → 36 tests, 0 failures
|
||||
|
||||
---
|
||||
|
||||
## How to verify (exact commands)
|
||||
|
||||
```bash
|
||||
# D6 — all tests green
|
||||
python -m unittest -q
|
||||
|
||||
# D1 — mul binds tighter than add
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('1+2*3')); print(t); assert str(t)==\"BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))\""
|
||||
|
||||
# D2 — left associativity subtraction
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('8-3-2')); print(t); assert str(t)==\"BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))\""
|
||||
|
||||
# D2 — left associativity division
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('8/4/2')); print(t); assert str(t)==\"BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))\""
|
||||
|
||||
# D3 — parens override precedence
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; t=parse(tokenize('(1+2)*3')); print(t); assert str(t)==\"BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))\""
|
||||
|
||||
# D4 — unary minus
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5'))); print(parse(tokenize('-(1+2)'))); print(parse(tokenize('3 * -2')))"
|
||||
|
||||
# D5 — all five error inputs raise ParseError
|
||||
python -c "
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError
|
||||
for src in ['1 +', '(1', '1 2', ')(', '']:
|
||||
try:
|
||||
parse(tokenize(src))
|
||||
print('FAIL — no error for', repr(src))
|
||||
except ParseError:
|
||||
print('OK', repr(src))
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected output
|
||||
|
||||
**D6**: `Ran 36 tests ... OK`
|
||||
|
||||
**D1**: `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` (no assertion error)
|
||||
|
||||
**D2-sub**: `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` (no assertion error)
|
||||
|
||||
**D2-div**: `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` (no assertion error)
|
||||
|
||||
**D3**: `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` (no assertion error)
|
||||
|
||||
**D4**:
|
||||
```
|
||||
Unary('-', Num(5))
|
||||
Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
```
|
||||
|
||||
**D5**: Five lines all starting with `OK`
|
||||
|
||||
---
|
||||
|
||||
## AST shape reference
|
||||
|
||||
```
|
||||
Num(value) # numeric literal; value: int | float
|
||||
BinOp(op, left, right) # binary op; op in {'+', '-', '*', '/'}
|
||||
Unary(op, operand) # unary op; op == '-'
|
||||
```
|
||||
|
||||
Precedence (high → low): unary-minus > `*` `/` > `+` `-`
|
||||
Associativity: left for all binary operators
|
||||
Reference in New Issue
Block a user