artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
1
calculators/builder-adversary-stateless/run-05/.gitignore
vendored
Normal file
1
calculators/builder-adversary-stateless/run-05/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
calc/__pycache__/
|
||||
18
calculators/builder-adversary-stateless/run-05/GIT-LOG.txt
Normal file
18
calculators/builder-adversary-stateless/run-05/GIT-LOG.txt
Normal file
@ -0,0 +1,18 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
71359e7 status(eval): ## DONE — all D1-D5 Adversary-verified PASS
|
||||
d51b359 review(D1-D5): PASS — all eval gates verified; arithmetic, division, types, CLI, tests all correct
|
||||
3b507ee claim(D1-D5): implement evaluator, CLI, and tests — all eval gates
|
||||
a093fc4 review(eval): Adversary initialized — watching for gate claims
|
||||
3ede551 status(parse): ## DONE — all D1-D6 Adversary-verified PASS
|
||||
1b959eb review(D1-D6): PASS — all parse gates verified; precedence, assoc, parens, unary, errors, tests all correct
|
||||
d20b751 claim(D1-D6): implement parser — all gates claimed
|
||||
2a9645d review(parse): Adversary initialized for parse phase — watching for gate claims
|
||||
10ff032 status(lex): ## DONE — all D1-D4 Adversary-verified PASS
|
||||
25d205d review(D3): PASS — AF-1 fixed, bare-dot now raises LexError; all D1-D4 PASS, VETO lifted
|
||||
10a2f15 status(lex): record fix commit sha f40a364
|
||||
f40a364 claim(D3): fix AF-1 bare-dot ValueError leak, add test
|
||||
c46f537 review(D3): FAIL — bare '.' leaks ValueError not LexError (AF-1); D1 D2 D4 PASS; VETO until fixed
|
||||
f72e487 claim(D1,D2,D3,D4): all lex gates claimed, tests green
|
||||
fb03159 feat(lex): implement lexer D1-D3 and test suite D4
|
||||
cabf40a review(init): Adversary initialized, watching for gate claims
|
||||
20b6566 chore: seed
|
||||
1
calculators/builder-adversary-stateless/run-05/README.md
Normal file
1
calculators/builder-adversary-stateless/run-05/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-ufRkmF/builder-adversary-stateless/r5
|
||||
27
calculators/builder-adversary-stateless/run-05/calc.py
Normal file
27
calculators/builder-adversary-stateless/run-05/calc.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""CLI entry point: 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 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(result)
|
||||
except (LexError, ParseError, EvalError) as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,42 @@
|
||||
"""Evaluator for arithmetic AST nodes produced by calc.parser."""
|
||||
|
||||
from calc.parser import Num, BinOp, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(node) -> int | float:
|
||||
"""Walk the AST and return the numeric result.
|
||||
|
||||
Whole-valued floats are returned as int (e.g. 4/2 → 2, not 2.0).
|
||||
Division by zero raises EvalError.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, Unary):
|
||||
operand = evaluate(node.operand)
|
||||
if node.op == "-":
|
||||
return -operand
|
||||
raise EvalError(f"unknown unary operator: {node.op!r}")
|
||||
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}")
|
||||
# Normalize: whole-valued float → int
|
||||
if isinstance(result, float) and result == int(result):
|
||||
return int(result)
|
||||
return result
|
||||
raise EvalError(f"unknown node type: {type(node).__name__}")
|
||||
60
calculators/builder-adversary-stateless/run-05/calc/lexer.py
Normal file
60
calculators/builder-adversary-stateless/run-05/calc/lexer.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Lexer for arithmetic calculator."""
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Token:
|
||||
__slots__ = ("kind", "value")
|
||||
|
||||
def __init__(self, kind: str, value):
|
||||
self.kind = kind
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"Token({self.kind}, {self.value!r})"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Token) and self.kind == other.kind and self.value == other.value
|
||||
|
||||
|
||||
_SINGLE = {
|
||||
"+": "PLUS",
|
||||
"-": "MINUS",
|
||||
"*": "STAR",
|
||||
"/": "SLASH",
|
||||
"(": "LPAREN",
|
||||
")": "RPAREN",
|
||||
}
|
||||
|
||||
|
||||
def tokenize(src: str) -> list:
|
||||
tokens = []
|
||||
i = 0
|
||||
n = len(src)
|
||||
while i < n:
|
||||
ch = src[i]
|
||||
if ch in " \t":
|
||||
i += 1
|
||||
elif ch in _SINGLE:
|
||||
tokens.append(Token(_SINGLE[ch], ch))
|
||||
i += 1
|
||||
elif ch.isdigit() or ch == ".":
|
||||
j = i
|
||||
has_dot = False
|
||||
while j < n and (src[j].isdigit() or (src[j] == "." and not has_dot)):
|
||||
if src[j] == ".":
|
||||
has_dot = True
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
try:
|
||||
value = float(raw) if has_dot else int(raw)
|
||||
except ValueError:
|
||||
raise LexError(f"invalid number 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
|
||||
144
calculators/builder-adversary-stateless/run-05/calc/parser.py
Normal file
144
calculators/builder-adversary-stateless/run-05/calc/parser.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""Recursive-descent parser for arithmetic expressions.
|
||||
|
||||
AST nodes:
|
||||
Num(value) -- numeric literal (int or float)
|
||||
BinOp(op, left, right) -- binary op; op in {'+', '-', '*', '/'}
|
||||
Unary(op, operand) -- unary op; op == '-'
|
||||
|
||||
Grammar:
|
||||
expr → term (('+' | '-') term)*
|
||||
term → unary (('*' | '/') unary)*
|
||||
unary → '-' unary | primary
|
||||
primary → NUMBER | '(' expr ')'
|
||||
"""
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Num:
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Num) and self.value == other.value
|
||||
|
||||
|
||||
class BinOp:
|
||||
__slots__ = ("op", "left", "right")
|
||||
|
||||
def __init__(self, op, left, right):
|
||||
self.op = op
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __repr__(self):
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, BinOp)
|
||||
and self.op == other.op
|
||||
and self.left == other.left
|
||||
and self.right == other.right
|
||||
)
|
||||
|
||||
|
||||
class Unary:
|
||||
__slots__ = ("op", "operand")
|
||||
|
||||
def __init__(self, op, operand):
|
||||
self.op = op
|
||||
self.operand = operand
|
||||
|
||||
def __repr__(self):
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, Unary)
|
||||
and self.op == other.op
|
||||
and self.operand == other.operand
|
||||
)
|
||||
|
||||
|
||||
class _Parser:
|
||||
def __init__(self, tokens):
|
||||
self._tokens = tokens
|
||||
self._pos = 0
|
||||
|
||||
def _peek(self):
|
||||
return self._tokens[self._pos]
|
||||
|
||||
def _consume(self, kind=None):
|
||||
tok = self._tokens[self._pos]
|
||||
if kind 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):
|
||||
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 = self._term()
|
||||
while self._peek().kind in ("PLUS", "MINUS"):
|
||||
op = self._consume().value
|
||||
right = self._term()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _term(self):
|
||||
node = self._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):
|
||||
if self._peek().kind == "MINUS":
|
||||
op = self._consume().value
|
||||
operand = self._unary()
|
||||
return Unary(op, operand)
|
||||
return self._primary()
|
||||
|
||||
def _primary(self):
|
||||
tok = self._peek()
|
||||
if tok.kind == "NUMBER":
|
||||
self._consume()
|
||||
return Num(tok.value)
|
||||
if tok.kind == "LPAREN":
|
||||
self._consume("LPAREN")
|
||||
node = self._expr()
|
||||
if self._peek().kind != "RPAREN":
|
||||
raise ParseError("unclosed parenthesis")
|
||||
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) -> object:
|
||||
"""Parse a token list (from tokenize()) into an AST node.
|
||||
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
@ -0,0 +1,92 @@
|
||||
"""Tests for calc.evaluator — covers D1 (arithmetic), D2 (division/EvalError), D3 (result type)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def calc(s):
|
||||
return evaluate(parse(tokenize(s)))
|
||||
|
||||
|
||||
class TestArithmetic(unittest.TestCase):
|
||||
"""D1 — arithmetic: +, -, *, /, precedence, parens, unary minus."""
|
||||
|
||||
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_subtract(self):
|
||||
self.assertEqual(calc("8-3-2"), 3)
|
||||
|
||||
def test_unary_minus_add(self):
|
||||
self.assertEqual(calc("-2+5"), 3)
|
||||
|
||||
def test_unary_minus_mul(self):
|
||||
self.assertEqual(calc("2*-3"), -6)
|
||||
|
||||
def test_simple_add(self):
|
||||
self.assertEqual(calc("1+2"), 3)
|
||||
|
||||
def test_simple_sub(self):
|
||||
self.assertEqual(calc("5-3"), 2)
|
||||
|
||||
def test_simple_mul(self):
|
||||
self.assertEqual(calc("3*4"), 12)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(calc("((2+3))"), 5)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
"""D2 — true division and EvalError on divide-by-zero."""
|
||||
|
||||
def test_true_division(self):
|
||||
self.assertEqual(calc("7/2"), 3.5)
|
||||
|
||||
def test_divide_by_zero_raises(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("1/0")
|
||||
|
||||
def test_divide_by_zero_expression(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("5/(2-2)")
|
||||
|
||||
def test_no_bare_zero_division_error(self):
|
||||
"""ZeroDivisionError must NOT escape the API."""
|
||||
try:
|
||||
calc("1/0")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("ZeroDivisionError escaped — must be EvalError")
|
||||
|
||||
|
||||
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_division_is_float(self):
|
||||
result = calc("7/2")
|
||||
self.assertEqual(result, 3.5)
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_integer_add_is_int(self):
|
||||
result = calc("2+3")
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_integer_mul_is_int(self):
|
||||
result = calc("3*4")
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,126 @@
|
||||
"""Tests for calc/lexer.py covering D1-D3."""
|
||||
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):
|
||||
result = tokenize("42")
|
||||
self.assertEqual(result, [Token("NUMBER", 42), Token("EOF", None)])
|
||||
self.assertIsInstance(result[0].value, int)
|
||||
|
||||
def test_float_standard(self):
|
||||
result = tokenize("3.14")
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertEqual(result[0].kind, "NUMBER")
|
||||
self.assertAlmostEqual(result[0].value, 3.14)
|
||||
self.assertIsInstance(result[0].value, float)
|
||||
|
||||
def test_float_leading_dot(self):
|
||||
result = tokenize(".5")
|
||||
self.assertEqual(result[0].kind, "NUMBER")
|
||||
self.assertAlmostEqual(result[0].value, 0.5)
|
||||
|
||||
def test_float_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)
|
||||
|
||||
def test_zero(self):
|
||||
result = tokenize("0")
|
||||
self.assertEqual(result[0], Token("NUMBER", 0))
|
||||
|
||||
|
||||
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_eof_last(self):
|
||||
result = tokenize("1+2")
|
||||
self.assertEqual(result[-1].kind, "EOF")
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
def test_spaces_skipped(self):
|
||||
self.assertEqual(
|
||||
kinds(" 12 + 3 "),
|
||||
["NUMBER", "PLUS", "NUMBER", "EOF"],
|
||||
)
|
||||
result = tokenize(" 12 + 3 ")
|
||||
self.assertEqual(result[0].value, 12)
|
||||
self.assertEqual(result[2].value, 3)
|
||||
|
||||
def test_tabs_skipped(self):
|
||||
self.assertEqual(kinds("1\t+\t2"), ["NUMBER", "PLUS", "NUMBER", "EOF"])
|
||||
|
||||
def test_complex_expression(self):
|
||||
result = kv("3.5*(1-2)")
|
||||
self.assertEqual(
|
||||
result,
|
||||
[
|
||||
("NUMBER", 3.5),
|
||||
("STAR", "*"),
|
||||
("LPAREN", "("),
|
||||
("NUMBER", 1),
|
||||
("MINUS", "-"),
|
||||
("NUMBER", 2),
|
||||
("RPAREN", ")"),
|
||||
("EOF", None),
|
||||
],
|
||||
)
|
||||
|
||||
def test_at_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("1 @ 2")
|
||||
|
||||
def test_dollar_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$")
|
||||
|
||||
def test_letter_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("x")
|
||||
|
||||
def test_lex_error_message_has_char_and_pos(self):
|
||||
try:
|
||||
tokenize("1 @ 2")
|
||||
except LexError as e:
|
||||
self.assertIn("@", str(e))
|
||||
|
||||
def test_bare_dot_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize(".")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,154 @@
|
||||
"""Tests for calc/parser.py — covers D1–D5."""
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import BinOp, Num, ParseError, Unary, parse
|
||||
|
||||
|
||||
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+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
self.assertEqual(p("2*3+4"), BinOp("+", BinOp("*", Num(2), Num(3)), Num(4)))
|
||||
|
||||
def test_add_div(self):
|
||||
# 1+8/4 → BinOp('+', Num(1), BinOp('/', Num(8), Num(4)))
|
||||
self.assertEqual(p("1+8/4"), BinOp("+", Num(1), BinOp("/", Num(8), Num(4))))
|
||||
|
||||
def test_sub_mul(self):
|
||||
# 10-2*3 → BinOp('-', Num(10), BinOp('*', Num(2), Num(3)))
|
||||
self.assertEqual(
|
||||
p("10-2*3"), BinOp("-", Num(10), BinOp("*", Num(2), Num(3)))
|
||||
)
|
||||
|
||||
def test_single_number(self):
|
||||
self.assertEqual(p("42"), Num(42))
|
||||
|
||||
def test_single_float(self):
|
||||
self.assertEqual(p("3.14"), Num(3.14))
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
"""D2 — same-precedence operators associate left."""
|
||||
|
||||
def test_sub_sub(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_div(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_add(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_mul(self):
|
||||
# 2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
self.assertEqual(
|
||||
p("2*3*4"), BinOp("*", BinOp("*", Num(2), Num(3)), Num(4))
|
||||
)
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
"""D3 — parentheses override precedence."""
|
||||
|
||||
def test_parens_override_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_nested_parens(self):
|
||||
# ((1+2)) → BinOp('+', Num(1), Num(2))
|
||||
self.assertEqual(p("((1+2))"), BinOp("+", Num(1), Num(2)))
|
||||
|
||||
def test_parens_inside_add(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_parens_alone(self):
|
||||
self.assertEqual(p("(5)"), Num(5))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
"""D4 — unary minus."""
|
||||
|
||||
def test_leading_unary(self):
|
||||
# -5 → Unary('-', Num(5))
|
||||
self.assertEqual(p("-5"), Unary("-", Num(5)))
|
||||
|
||||
def test_unary_of_paren(self):
|
||||
# -(1+2) → Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
self.assertEqual(
|
||||
p("-(1+2)"), Unary("-", BinOp("+", Num(1), Num(2)))
|
||||
)
|
||||
|
||||
def test_mul_unary(self):
|
||||
# 3 * -2 → BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
self.assertEqual(
|
||||
p("3 * -2"), BinOp("*", Num(3), Unary("-", Num(2)))
|
||||
)
|
||||
|
||||
def test_double_unary(self):
|
||||
# --5 → Unary('-', Unary('-', Num(5)))
|
||||
self.assertEqual(p("--5"), Unary("-", Unary("-", Num(5))))
|
||||
|
||||
def test_unary_in_add(self):
|
||||
# 1 + -2 → BinOp('+', Num(1), Unary('-', Num(2)))
|
||||
self.assertEqual(
|
||||
p("1 + -2"), BinOp("+", Num(1), Unary("-", Num(2)))
|
||||
)
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
"""D5 — malformed input raises ParseError."""
|
||||
|
||||
def _raises(self, src):
|
||||
with self.assertRaises(ParseError):
|
||||
p(src)
|
||||
|
||||
def test_trailing_op(self):
|
||||
self._raises("1 +")
|
||||
|
||||
def test_unclosed_paren(self):
|
||||
self._raises("(1")
|
||||
|
||||
def test_two_numbers(self):
|
||||
self._raises("1 2")
|
||||
|
||||
def test_close_before_open(self):
|
||||
self._raises(")(")
|
||||
|
||||
def test_empty(self):
|
||||
self._raises("")
|
||||
|
||||
def test_just_op(self):
|
||||
self._raises("*")
|
||||
|
||||
def test_mismatched_parens(self):
|
||||
self._raises("(1+2")
|
||||
|
||||
def test_extra_close(self):
|
||||
self._raises("(1+2))")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,9 @@
|
||||
# BACKLOG-eval.md
|
||||
|
||||
## Build backlog
|
||||
|
||||
All items complete. Gates D1–D5 claimed.
|
||||
|
||||
## Adversary findings
|
||||
|
||||
(none yet)
|
||||
@ -0,0 +1,14 @@
|
||||
# BACKLOG-lex.md
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] D1 — Numbers: integers and floats tokenize correctly
|
||||
- [x] D2 — Operators & parens: all six symbols tokenize correctly
|
||||
- [x] D3 — Whitespace skipped; invalid chars raise LexError
|
||||
- [x] D4 — 20 unit tests passing under python -m unittest
|
||||
|
||||
All items complete. Awaiting Adversary PASS verdicts.
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(Adversary writes here)_
|
||||
@ -0,0 +1,16 @@
|
||||
# BACKLOG-parse.md — Phase `parse`
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Write calc/parser.py (ParseError, Num, BinOp, Unary, parse())
|
||||
- [x] Write calc/test_parser.py (unittest, D1-D5 coverage)
|
||||
- [x] Claim D1 (precedence)
|
||||
- [x] Claim D2 (left associativity)
|
||||
- [x] Claim D3 (parentheses)
|
||||
- [x] Claim D4 (unary minus)
|
||||
- [x] Claim D5 (errors)
|
||||
- [x] Claim D6 (tests green)
|
||||
- [ ] Await Adversary verdict on D1–D6
|
||||
|
||||
## Adversary findings
|
||||
*(None yet — awaiting gate claims)*
|
||||
@ -0,0 +1,5 @@
|
||||
# DECISIONS.md — Shared (append-only)
|
||||
|
||||
## 2026-06-15T04:30:28Z — Adversary initialized
|
||||
|
||||
Adversary loop started for phase `lex`. No decisions yet.
|
||||
@ -0,0 +1,35 @@
|
||||
# JOURNAL-eval.md
|
||||
|
||||
## Session 1
|
||||
|
||||
### Implementation
|
||||
|
||||
Created three files:
|
||||
- `calc/evaluator.py` — `evaluate(node)` walks Num/BinOp/Unary nodes. Division uses Python `/` (true division). After division, normalizes whole-valued floats to int via `int(result)`.
|
||||
- `calc/test_evaluator.py` — 16 tests covering D1 arithmetic (all 5 DoD cases + extras), D2 division/EvalError, D3 type normalization.
|
||||
- `calc.py` — CLI: parses argv[1], calls tokenize→parse→evaluate, prints result; catches LexError/ParseError/EvalError, prints to stderr, exits 1.
|
||||
|
||||
### Verification run
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 65 tests in 0.001s
|
||||
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" 2>&1; echo "exit: $?"
|
||||
error: division by zero
|
||||
exit: 1
|
||||
$ python calc.py "1 +" 2>&1; echo "exit: $?"
|
||||
error: unexpected end of input
|
||||
exit: 1
|
||||
```
|
||||
|
||||
All outputs match DoD specs. 65 tests (lex + parse + eval) pass, zero failures.
|
||||
@ -0,0 +1,41 @@
|
||||
# JOURNAL-lex.md — Adversary Journal
|
||||
|
||||
## 2026-06-15T04:30:28Z — Initialized
|
||||
|
||||
Adversary started. Phase plan read. No Builder work present yet.
|
||||
Watching for gate claims in machine-docs/STATUS-lex.md.
|
||||
|
||||
---
|
||||
|
||||
# JOURNAL-lex.md — Builder Entries
|
||||
|
||||
## 2026-06-15 — Implementation complete
|
||||
|
||||
Built `calc/lexer.py` and `calc/test_lexer.py` in a single session.
|
||||
|
||||
**Design choices:**
|
||||
- `Token` uses `__slots__` for memory efficiency; `__eq__` for test assertions.
|
||||
- Number parsing: scan ahead consuming digits and at most one `.`. A leading dot (`.5`) and trailing dot (`10.`) both work because the while loop checks `src[j] == "."` only when `not has_dot`.
|
||||
- Integer values stored as `int`, floats as `float` — allows parser to distinguish.
|
||||
- `LexError` extends `Exception` directly; message includes the character repr and its position.
|
||||
|
||||
**Test run:**
|
||||
```
|
||||
$ python -m unittest -q
|
||||
....................
|
||||
----------------------------------------------------------------------
|
||||
Ran 20 tests in 0.000s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
**Verification commands run locally:**
|
||||
```
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
Traceback:
|
||||
...
|
||||
calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
@ -0,0 +1,32 @@
|
||||
# JOURNAL-parse.md
|
||||
|
||||
## 2026-06-15 — Build + verify
|
||||
|
||||
Lex phase DONE. Token kinds: NUMBER, PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN, EOF.
|
||||
|
||||
Grammar chosen (iterative at each level → left associativity by construction):
|
||||
```
|
||||
expr → term (('+' | '-') term)*
|
||||
term → unary (('*' | '/') unary)*
|
||||
unary → '-' unary | primary
|
||||
primary → NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
### Manual verification output
|
||||
|
||||
```
|
||||
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: unclosed parenthesis
|
||||
D5 '1 2' -> ParseError: unexpected token 'NUMBER' (2) after expression
|
||||
D5 ')(' -> ParseError: unexpected token 'RPAREN' (')')
|
||||
D5 '' -> ParseError: empty input
|
||||
```
|
||||
|
||||
Test run: `Ran 48 tests in 0.001s — OK` (21 lex + 27 parser)
|
||||
@ -0,0 +1,60 @@
|
||||
# REVIEW-eval.md — Adversary verdicts
|
||||
|
||||
## eval/D1: PASS @2026-06-15T04:45:33Z
|
||||
|
||||
**Evidence (cold run, commit 3b507ee):**
|
||||
```
|
||||
2+3*4 → 14 ✓
|
||||
(2+3)*4 → 20 ✓
|
||||
8-3-2 → 3 ✓
|
||||
-2+5 → 3 ✓
|
||||
2*-3 → -6 ✓
|
||||
```
|
||||
Extra probes: `--5`→5, `-(3+4)`→-7, `1+2+3+4+5`→15 ✓
|
||||
|
||||
---
|
||||
|
||||
## eval/D2: PASS @2026-06-15T04:45:33Z
|
||||
|
||||
**Evidence:**
|
||||
- `7/2` → `3.5` (true division, not integer) ✓
|
||||
- `1/0` → `EvalError: division by zero` (not bare ZeroDivisionError) ✓
|
||||
- `5/(2-2)` → `EvalError` ✓
|
||||
|
||||
---
|
||||
|
||||
## eval/D3: PASS @2026-06-15T04:45:33Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
4/2 → int 2 ✓
|
||||
7/2 → float 3.5 ✓
|
||||
3*2 → int 6 ✓
|
||||
-4/2 → int -2 ✓
|
||||
1/3 → float 0.333... ✓
|
||||
2.5*2 → int 5 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## eval/D4: PASS @2026-06-15T04:45:33Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
python calc.py "2+3*4" → stdout: 14, exit 0 ✓
|
||||
python calc.py "1/0" → stderr: "error: division by zero", exit 1, empty stdout ✓
|
||||
python calc.py "1 +" → stderr: "error: unexpected end of input", exit 1 ✓
|
||||
python calc.py → exit 1 (usage error) ✓
|
||||
python calc.py "1+1" "extra" → exit 1 (too many args) ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## eval/D5: PASS @2026-06-15T04:45:33Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
Ran 65 tests in 0.001s OK (21 lex + 27 parser + 17 evaluator) ✓
|
||||
```
|
||||
- `test_evaluator.py` covers D1 (arithmetic), D2 (division/EvalError), D3 (result type) ✓
|
||||
- No regressions in lex or parse suites ✓
|
||||
@ -0,0 +1,69 @@
|
||||
# REVIEW-lex.md — Adversary Verdicts
|
||||
|
||||
## Gates
|
||||
|
||||
- D1: PASS @2026-06-15T04:34:35Z
|
||||
- D2: PASS @2026-06-15T04:34:35Z
|
||||
- D3: PASS @2026-06-15T04:40:18Z (re-verified after AF-1 fix)
|
||||
- D4: PASS @2026-06-15T04:40:18Z (21 tests, 0 failures)
|
||||
|
||||
All DoD items verified. VETO lifted.
|
||||
|
||||
---
|
||||
|
||||
## D1 — Numbers: PASS @2026-06-15T04:34:35Z
|
||||
|
||||
```
|
||||
NUMBER 42 int ✓
|
||||
NUMBER 3.14 float ✓
|
||||
NUMBER 0.5 ✓ (.5 leading dot)
|
||||
NUMBER 10.0 float ✓ (10. trailing dot)
|
||||
[('NUMBER', 42), ('EOF', None)] ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D2 — Operators & Parens: PASS @2026-06-15T04:34:35Z
|
||||
|
||||
```
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓ (1+2*3)
|
||||
[('PLUS', '+'), ('MINUS', '-'), ('STAR', '*'), ('SLASH', '/'), ('LPAREN', '('), ('RPAREN', ')'), ('EOF', None)] ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D3 — Whitespace & Errors: PASS @2026-06-15T04:40:18Z
|
||||
|
||||
**First verdict (FAIL @2026-06-15T04:34:35Z):** bare `.` raised `ValueError` not `LexError` (AF-1).
|
||||
|
||||
**Re-verified after fix commit f40a364:**
|
||||
|
||||
```
|
||||
Ran 21 tests in 0.000s — OK ✓
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'EOF'] (whitespace skipped) ✓
|
||||
tokenize('.') → LexError: invalid number literal '.' at position 0 ✓
|
||||
tokenize('1 @ 2') → LexError: unexpected character '@' at position 2 ✓
|
||||
```
|
||||
|
||||
Valid floats unaffected by fix: `.5` → `0.5`, `10.` → `10.0`, `3.5*(1-2)` correct.
|
||||
|
||||
---
|
||||
|
||||
## D4 — Tests Green: PASS @2026-06-15T04:40:18Z
|
||||
|
||||
```
|
||||
Ran 21 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
|
||||
21 tests (was 20; `test_bare_dot_raises_lex_error` added). All three required DoD examples covered and passing.
|
||||
|
||||
---
|
||||
|
||||
## Adversary findings
|
||||
|
||||
### AF-1 (CLOSED @2026-06-15T04:40:18Z): Bare `.` leaked `ValueError` instead of `LexError`
|
||||
|
||||
**Fix:** `lexer.py` now wraps `float(raw)` / `int(raw)` in try/except, re-raises as `LexError`. Test added.
|
||||
Re-tested: `tokenize('.')` now raises `LexError: invalid number literal '.' at position 0`. ✓
|
||||
_Closed by Adversary after re-test._
|
||||
@ -0,0 +1,67 @@
|
||||
# REVIEW-parse.md — Adversary verdicts for phase `parse`
|
||||
|
||||
Phase plan SSOT: /home/loops/project-orchestrator/projects/agent-orchestrator-benchmark/plans/calc/parse.md
|
||||
|
||||
Gates: D1 (precedence), D2 (left associativity), D3 (parentheses), D4 (unary minus), D5 (errors), D6 (tests green)
|
||||
|
||||
## Verdicts
|
||||
|
||||
- D1: PASS @2026-06-15T04:41:47Z
|
||||
- D2: PASS @2026-06-15T04:41:47Z
|
||||
- D3: PASS @2026-06-15T04:41:47Z
|
||||
- D4: PASS @2026-06-15T04:41:47Z
|
||||
- D5: PASS @2026-06-15T04:41:47Z
|
||||
- D6: PASS @2026-06-15T04:41:47Z
|
||||
|
||||
All DoD gates PASS. No VETO. Phase `parse` complete — Builder may write ## DONE to STATUS-parse.md.
|
||||
|
||||
---
|
||||
|
||||
## Evidence
|
||||
|
||||
**D1 — Precedence**
|
||||
```
|
||||
1+2*3 → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓ (* binds tighter)
|
||||
2*3+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)) ✓
|
||||
1+2*3-4/2 → BinOp('-', BinOp('+', Num(1), BinOp('*', Num(2), Num(3))), BinOp('/', Num(4), Num(2))) ✓
|
||||
```
|
||||
|
||||
**D2 — Left Associativity**
|
||||
```
|
||||
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)) ✓
|
||||
2/3*4 → BinOp('*', BinOp('/', Num(2), Num(3)), Num(4)) ✓
|
||||
```
|
||||
|
||||
**D3 — Parentheses**
|
||||
```
|
||||
(1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
((((1)))) → Num(1) ✓
|
||||
```
|
||||
|
||||
**D4 — Unary Minus**
|
||||
```
|
||||
-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))) ✓
|
||||
-2*3 → BinOp('*', Unary('-', Num(2)), Num(3)) ✓
|
||||
2+-3 → BinOp('+', Num(2), Unary('-', Num(3))) ✓
|
||||
-2*-3 → BinOp('*', Unary('-', Num(2)), Unary('-', Num(3))) ✓
|
||||
```
|
||||
|
||||
**D5 — Errors (all raise ParseError, no wrong exception types)**
|
||||
```
|
||||
'1 +' → ParseError: unexpected end of input ✓
|
||||
'(1' → ParseError: unclosed parenthesis ✓
|
||||
'1 2' → ParseError: unexpected token 'NUMBER'… ✓
|
||||
')(' → ParseError: unexpected token 'RPAREN' ✓
|
||||
'' → ParseError: empty input ✓
|
||||
Extra probes: '+1', '1++2', '1*', '((1+2)', '1+2)', '*', '()', '1 + + 2' — all ParseError ✓
|
||||
```
|
||||
|
||||
**D6 — Tests Green**
|
||||
```
|
||||
Ran 48 tests in 0.001s OK (21 lex + 27 parser) ✓
|
||||
```
|
||||
@ -0,0 +1,46 @@
|
||||
# STATUS-eval.md
|
||||
|
||||
## DONE
|
||||
|
||||
All eval gates D1–D5 Adversary-verified PASS. Phase complete.
|
||||
|
||||
---
|
||||
|
||||
## Gate: D1-D5 CLAIMED → ALL PASS
|
||||
|
||||
All gates are implemented and self-verified. Claiming D1–D5 together.
|
||||
|
||||
### WHAT is claimed
|
||||
- D1 — arithmetic: evaluate() correct for +, -, *, /, precedence, parens, unary minus
|
||||
- D2 — division: true division, EvalError on divide-by-zero (no bare ZeroDivisionError)
|
||||
- D3 — result type: whole-valued → int, non-whole → float
|
||||
- D4 — CLI: `python calc.py <expr>` prints result and exits 0; errors to stderr, non-zero exit
|
||||
- D5 — tests green + no regression
|
||||
|
||||
### HOW to verify (cold, from fresh clone)
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
python -m unittest -q # must be 0 failures (65 tests)
|
||||
python calc.py "2+3*4" # expect: 14
|
||||
python calc.py "(2+3)*4" # expect: 20
|
||||
python calc.py "7/2" # expect: 3.5
|
||||
python calc.py "4/2" # expect: 2
|
||||
python calc.py "1/0" # expect: non-zero exit, error on stderr
|
||||
python calc.py "1 +" # expect: non-zero exit, error on stderr
|
||||
```
|
||||
|
||||
### EXPECTED outcomes
|
||||
- unittest: `Ran 65 tests in ~0.001s` — `OK`
|
||||
- `2+3*4` → `14`
|
||||
- `(2+3)*4` → `20`
|
||||
- `7/2` → `3.5`
|
||||
- `4/2` → `2`
|
||||
- `1/0` → exit 1, stderr: `error: division by zero`
|
||||
- `1 +` → exit 1, stderr: `error: unexpected end of input`
|
||||
|
||||
### WHERE inputs live
|
||||
- `calc/evaluator.py` — evaluator + EvalError
|
||||
- `calc/test_evaluator.py` — unittest suite covering D1–D3
|
||||
- `calc.py` — CLI entry point
|
||||
- Commit: (see git log — latest commit on main)
|
||||
@ -0,0 +1,98 @@
|
||||
# STATUS-lex.md
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase `lex` complete.
|
||||
|
||||
- D1: PASS @2026-06-15T04:34:35Z
|
||||
- D2: PASS @2026-06-15T04:34:35Z
|
||||
- D3: PASS @2026-06-15T04:40:18Z (AF-1 fixed: bare `.` now raises LexError)
|
||||
- D4: PASS @2026-06-15T04:40:18Z (21 tests, 0 failures)
|
||||
|
||||
Fix commit: f40a364
|
||||
|
||||
---
|
||||
|
||||
## Gate D1 — Numbers CLAIMED
|
||||
|
||||
**WHAT:** `tokenize()` converts integers and floats to `NUMBER` tokens with numeric values.
|
||||
|
||||
**HOW to verify:**
|
||||
```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, type(r[0].value).__name__)"
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `NUMBER 42 int`
|
||||
- `NUMBER 3.14 float`
|
||||
- `NUMBER 0.5`
|
||||
- `NUMBER 10.0 float`
|
||||
- `[('NUMBER', 42), ('EOF', None)]`
|
||||
|
||||
**WHERE:** `calc/lexer.py`, commit fb03159
|
||||
|
||||
---
|
||||
|
||||
## Gate D2 — Operators & Parens CLAIMED
|
||||
|
||||
**WHAT:** `+ - * / ( )` tokenize to correct kinds; `"1+2*3"` → NUMBER PLUS NUMBER STAR NUMBER EOF.
|
||||
|
||||
**HOW to verify:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('+-*/()')] )"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']`
|
||||
- `[('PLUS', '+'), ('MINUS', '-'), ('STAR', '*'), ('SLASH', '/'), ('LPAREN', '('), ('RPAREN', ')'), ('EOF', None)]`
|
||||
|
||||
**WHERE:** `calc/lexer.py`, commit fb03159
|
||||
|
||||
---
|
||||
|
||||
## Gate D3 — Whitespace & Errors RE-CLAIMED (AF-1 fixed)
|
||||
|
||||
**WHAT:** Spaces/tabs skipped; ALL invalid inputs raise `LexError` (including bare `.`).
|
||||
|
||||
**Fix applied:** Wrapped `float(raw)` / `int(raw)` conversion in try/except; raises `LexError` on bad numeric literals. Added `test_bare_dot_raises_lex_error` to test suite.
|
||||
|
||||
**HOW to verify:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
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')"
|
||||
python -c "from calc.lexer import tokenize; tokenize('.')"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `Ran 21 tests in 0.000s\nOK`
|
||||
- `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
|
||||
- Raises `calc.lexer.LexError: unexpected character '@' at position 2`
|
||||
- Raises `calc.lexer.LexError: invalid number literal '.' at position 0`
|
||||
|
||||
**WHERE:** `calc/lexer.py`, `calc/test_lexer.py` (see fix commit)
|
||||
|
||||
---
|
||||
|
||||
## Gate D4 — Tests Green CLAIMED
|
||||
|
||||
**WHAT:** `calc/test_lexer.py` passes under `python -m unittest` with 0 failures.
|
||||
|
||||
**HOW to verify:**
|
||||
```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:**
|
||||
- `Ran 20 tests in 0.000s\nOK`
|
||||
- `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
|
||||
- Raises `LexError`
|
||||
|
||||
**WHERE:** `calc/test_lexer.py`, commit fb03159
|
||||
@ -0,0 +1,126 @@
|
||||
# STATUS-parse.md
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase `parse` complete.
|
||||
|
||||
- D1: PASS @2026-06-15T04:41:47Z
|
||||
- D2: PASS @2026-06-15T04:41:47Z
|
||||
- D3: PASS @2026-06-15T04:41:47Z
|
||||
- D4: PASS @2026-06-15T04:41:47Z
|
||||
- D5: PASS @2026-06-15T04:41:47Z
|
||||
- D6: PASS @2026-06-15T04:41:47Z
|
||||
|
||||
---
|
||||
|
||||
## Gate D1–D6 CLAIMED (archived)
|
||||
|
||||
---
|
||||
|
||||
## AST Shape
|
||||
|
||||
Nodes defined in `calc/parser.py`:
|
||||
|
||||
```python
|
||||
Num(value) # numeric leaf; value is int or float
|
||||
BinOp(op, left, right) # binary: op in {'+','-','*','/'}; left/right are nodes
|
||||
Unary(op, operand) # unary: op == '-'; operand is a node
|
||||
```
|
||||
|
||||
All nodes implement `__repr__` and `__eq__`.
|
||||
|
||||
---
|
||||
|
||||
## D1 — Precedence
|
||||
|
||||
**WHAT:** `*` and `/` bind tighter than `+` and `-`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('1+2*3'))))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('2*3+4'))))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`
|
||||
- `BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))`
|
||||
|
||||
---
|
||||
|
||||
## D2 — Left Associativity
|
||||
|
||||
**WHAT:** Same-precedence operators associate left.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('8-3-2'))))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('8/4/2'))))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))`
|
||||
- `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))`
|
||||
|
||||
---
|
||||
|
||||
## D3 — Parentheses
|
||||
|
||||
**WHAT:** Parens override precedence.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('(1+2)*3'))))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
|
||||
|
||||
---
|
||||
|
||||
## D4 — Unary Minus
|
||||
|
||||
**WHAT:** Leading and nested unary minus parses correctly.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('-5'))))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('-(1+2)'))))"
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(repr(parse(tokenize('3 * -2'))))"
|
||||
```
|
||||
|
||||
**EXPECTED:**
|
||||
- `Unary('-', Num(5))`
|
||||
- `Unary('-', BinOp('+', Num(1), Num(2)))`
|
||||
- `BinOp('*', Num(3), Unary('-', Num(2)))`
|
||||
|
||||
---
|
||||
|
||||
## D5 — Errors
|
||||
|
||||
**WHAT:** Malformed input raises `ParseError`.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
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('NO ERROR for', repr(s))
|
||||
except ParseError as e: print('OK ParseError:', repr(s), '->', e)
|
||||
"
|
||||
```
|
||||
|
||||
**EXPECTED:** All five raise `ParseError` (not any other exception type).
|
||||
|
||||
---
|
||||
|
||||
## D6 — Tests Green
|
||||
|
||||
**WHAT:** `python -m unittest -q` passes with 0 failures; covers D1–D5.
|
||||
|
||||
**HOW:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
|
||||
**EXPECTED:** `Ran 48 tests in ...s\n\nOK` (21 lex + 27 parser)
|
||||
|
||||
**WHERE:** `calc/parser.py`, `calc/test_parser.py`
|
||||
Reference in New Issue
Block a user