artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
4
calculators/builder-adversary/run-01/.gitignore
vendored
Normal file
4
calculators/builder-adversary/run-01/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
15
calculators/builder-adversary/run-01/GIT-LOG.txt
Normal file
15
calculators/builder-adversary/run-01/GIT-LOG.txt
Normal file
@ -0,0 +1,15 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
a3263ed status(eval): ## DONE — all D1-D5 Adversary-verified PASS
|
||||
df84a12 review(D1-D5): PASS — all gates cold-verified, 62 tests OK, break-it probes held
|
||||
165c7cc claim(D1-D5): implement evaluator + CLI — all eval gates ready for Adversary verification
|
||||
e1dece1 review(init): Adversary initialized for phase eval — watching for Builder claims
|
||||
4146139 status(parse): ## DONE — all D1-D6 Adversary-verified PASS
|
||||
bed53d5 review(D1-D6): PASS — all gates cold-verified, 45 tests OK, break-it probes held
|
||||
7768832 claim(D1-D6): implement parser — all gates ready for Adversary verification
|
||||
1bcba29 review(init): Adversary initialized for phase parse — watching for Builder claims
|
||||
026f551 status(lex): ## DONE — all D1-D4 Adversary-verified PASS
|
||||
2f4fd6b review(D1-D4): PASS — all gates cold-verified, 21 tests OK, break-it probes held
|
||||
071871f chore: add .gitignore, remove __pycache__ from tracking
|
||||
531a8cf claim(D1-D4): implement lexer — all gates ready for Adversary verification
|
||||
5826b69 review(init): Adversary initialized for phase lex — watching for Builder claims
|
||||
13c0db5 chore: seed
|
||||
1
calculators/builder-adversary/run-01/README.md
Normal file
1
calculators/builder-adversary/run-01/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary/run-01/SOURCE.txt
Normal file
1
calculators/builder-adversary/run-01/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r1
|
||||
32
calculators/builder-adversary/run-01/calc.py
Normal file
32
calculators/builder-adversary/run-01/calc.py
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""calc.py — command-line arithmetic calculator.
|
||||
|
||||
Usage: python calc.py "<expression>"
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from calc.lexer import LexError, tokenize
|
||||
from calc.parser import ParseError, parse
|
||||
from calc.evaluator import EvalError, evaluate
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: calc.py \"<expression>\"", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
expr = sys.argv[1]
|
||||
try:
|
||||
tokens = tokenize(expr)
|
||||
ast = parse(tokens)
|
||||
result = evaluate(ast)
|
||||
except (LexError, ParseError, EvalError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
47
calculators/builder-adversary/run-01/calc/evaluator.py
Normal file
47
calculators/builder-adversary/run-01/calc/evaluator.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Evaluator for the arithmetic AST produced by calc.parser."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from calc.parser import BinOp, Num, Node, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
"""Raised on a runtime evaluation error (e.g. division by zero)."""
|
||||
|
||||
|
||||
def evaluate(node: Node) -> int | float:
|
||||
"""Walk *node* and return its numeric value.
|
||||
|
||||
Result type rule: if the mathematical result is a whole number, return int;
|
||||
otherwise return float. This guarantees '4/2' → 2 and '7/2' → 3.5.
|
||||
|
||||
Raises EvalError on division by zero.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
|
||||
if isinstance(node, Unary):
|
||||
v = evaluate(node.operand)
|
||||
return -v
|
||||
|
||||
if isinstance(node, BinOp):
|
||||
left = evaluate(node.left)
|
||||
right = evaluate(node.right)
|
||||
if node.op == '+':
|
||||
result = left + right
|
||||
elif node.op == '-':
|
||||
result = left - right
|
||||
elif node.op == '*':
|
||||
result = left * right
|
||||
elif node.op == '/':
|
||||
if right == 0:
|
||||
raise EvalError("Division by zero")
|
||||
result = left / right
|
||||
else:
|
||||
raise EvalError(f"Unknown operator {node.op!r}")
|
||||
|
||||
if isinstance(result, float) and result == int(result):
|
||||
return int(result)
|
||||
return result
|
||||
|
||||
raise EvalError(f"Unknown node type {type(node)!r}")
|
||||
67
calculators/builder-adversary/run-01/calc/lexer.py
Normal file
67
calculators/builder-adversary/run-01/calc/lexer.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Lexer for arithmetic expressions."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
"""Raised on an unrecognised character."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str # NUMBER PLUS MINUS STAR SLASH LPAREN RPAREN EOF
|
||||
value: Union[int, float, str, None]
|
||||
|
||||
|
||||
_SINGLE = {
|
||||
'+': 'PLUS',
|
||||
'-': 'MINUS',
|
||||
'*': 'STAR',
|
||||
'/': 'SLASH',
|
||||
'(': 'LPAREN',
|
||||
')': 'RPAREN',
|
||||
}
|
||||
|
||||
|
||||
def tokenize(src: str) -> list:
|
||||
"""Return a list of Token for *src*, ending with EOF."""
|
||||
tokens = []
|
||||
i = 0
|
||||
n = len(src)
|
||||
|
||||
while i < n:
|
||||
ch = src[i]
|
||||
|
||||
# Skip whitespace
|
||||
if ch in ' \t\r\n':
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Number: integer or float (leading dot allowed, trailing dot allowed)
|
||||
if ch.isdigit() or ch == '.':
|
||||
j = i
|
||||
has_dot = False
|
||||
while j < n and (src[j].isdigit() or (src[j] == '.' and not has_dot)):
|
||||
if src[j] == '.':
|
||||
has_dot = True
|
||||
j += 1
|
||||
raw = src[i:j]
|
||||
if raw == '.':
|
||||
raise LexError(f"Unexpected character '.' at position {i}")
|
||||
value = float(raw) if has_dot else int(raw)
|
||||
tokens.append(Token('NUMBER', value))
|
||||
i = j
|
||||
continue
|
||||
|
||||
# Single-character operators and parentheses
|
||||
if ch in _SINGLE:
|
||||
tokens.append(Token(_SINGLE[ch], ch))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Anything else is an error
|
||||
raise LexError(f"Unexpected character {ch!r} at position {i}")
|
||||
|
||||
tokens.append(Token('EOF', None))
|
||||
return tokens
|
||||
143
calculators/builder-adversary/run-01/calc/parser.py
Normal file
143
calculators/builder-adversary/run-01/calc/parser.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""Recursive-descent parser for arithmetic expressions.
|
||||
|
||||
AST node types:
|
||||
Num(value) -- a numeric literal (int or float)
|
||||
BinOp(op, left, right) -- binary operation; op in ('+','-','*','/')
|
||||
Unary(op, operand) -- unary operation; op == '-'
|
||||
|
||||
Grammar (precedence low → high):
|
||||
expr := term (('+' | '-') term)*
|
||||
term := unary (('*' | '/') unary)*
|
||||
unary := '-' unary | primary
|
||||
primary := NUMBER | '(' expr ')'
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
from calc.lexer import Token, tokenize
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
"""Raised on syntactically invalid input."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AST nodes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Num({self.value!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: "Node"
|
||||
right: "Node"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: "Node"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Unary({self.op!r}, {self.operand!r})"
|
||||
|
||||
|
||||
Node = Union[Num, BinOp, Unary]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _Parser:
|
||||
def __init__(self, tokens: list[Token]) -> None:
|
||||
self._tokens = tokens
|
||||
self._pos = 0
|
||||
|
||||
def _peek(self) -> Token:
|
||||
return self._tokens[self._pos]
|
||||
|
||||
def _consume(self, kind: str | None = None) -> Token:
|
||||
tok = self._tokens[self._pos]
|
||||
if kind is not None and tok.kind != kind:
|
||||
raise ParseError(
|
||||
f"Expected {kind}, got {tok.kind!r} ({tok.value!r})"
|
||||
)
|
||||
self._pos += 1
|
||||
return tok
|
||||
|
||||
def parse(self) -> Node:
|
||||
if self._peek().kind == 'EOF':
|
||||
raise ParseError("Empty input")
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'EOF':
|
||||
tok = self._peek()
|
||||
raise ParseError(
|
||||
f"Unexpected token {tok.kind!r} ({tok.value!r}) after expression"
|
||||
)
|
||||
return node
|
||||
|
||||
def _expr(self) -> Node:
|
||||
node = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op_tok = self._consume()
|
||||
op = op_tok.value # '+' or '-'
|
||||
right = self._term()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _term(self) -> Node:
|
||||
node = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op_tok = self._consume()
|
||||
op = op_tok.value # '*' or '/'
|
||||
right = self._unary()
|
||||
node = BinOp(op, node, right)
|
||||
return node
|
||||
|
||||
def _unary(self) -> Node:
|
||||
if self._peek().kind == 'MINUS':
|
||||
self._consume('MINUS')
|
||||
operand = self._unary()
|
||||
return Unary('-', operand)
|
||||
return self._primary()
|
||||
|
||||
def _primary(self) -> Node:
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._consume('NUMBER')
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._consume('LPAREN')
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'RPAREN':
|
||||
raise ParseError(
|
||||
f"Expected ')' but got {self._peek().kind!r}"
|
||||
)
|
||||
self._consume('RPAREN')
|
||||
return node
|
||||
if tok.kind == 'EOF':
|
||||
raise ParseError("Unexpected end of input")
|
||||
raise ParseError(
|
||||
f"Unexpected token {tok.kind!r} ({tok.value!r})"
|
||||
)
|
||||
|
||||
|
||||
def parse(tokens: list[Token]) -> Node:
|
||||
"""Parse *tokens* (from lexer.tokenize) into an AST Node.
|
||||
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
return _Parser(tokens).parse()
|
||||
87
calculators/builder-adversary/run-01/calc/test_evaluator.py
Normal file
87
calculators/builder-adversary/run-01/calc/test_evaluator.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""Tests for calc.evaluator — covers D1, D2, D3."""
|
||||
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse
|
||||
from calc.evaluator import EvalError, evaluate
|
||||
|
||||
|
||||
def calc(s):
|
||||
return evaluate(parse(tokenize(s)))
|
||||
|
||||
|
||||
class TestArithmetic(unittest.TestCase):
|
||||
"""D1 — arithmetic, precedence, parens, unary minus."""
|
||||
|
||||
def test_add(self):
|
||||
self.assertEqual(calc("2+3"), 5)
|
||||
|
||||
def test_mul(self):
|
||||
self.assertEqual(calc("3*4"), 12)
|
||||
|
||||
def test_precedence(self):
|
||||
self.assertEqual(calc("2+3*4"), 14)
|
||||
|
||||
def test_parens(self):
|
||||
self.assertEqual(calc("(2+3)*4"), 20)
|
||||
|
||||
def test_sub_left_assoc(self):
|
||||
self.assertEqual(calc("8-3-2"), 3)
|
||||
|
||||
def test_unary_minus(self):
|
||||
self.assertEqual(calc("-2+5"), 3)
|
||||
|
||||
def test_unary_minus_mul(self):
|
||||
self.assertEqual(calc("2*-3"), -6)
|
||||
|
||||
def test_sub(self):
|
||||
self.assertEqual(calc("10-3"), 7)
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(calc("((4))"), 4)
|
||||
|
||||
def test_unary_double(self):
|
||||
self.assertEqual(calc("--5"), 5)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
"""D2 — true division, division by zero → EvalError."""
|
||||
|
||||
def test_true_division(self):
|
||||
self.assertAlmostEqual(calc("7/2"), 3.5)
|
||||
|
||||
def test_division_by_zero(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("1/0")
|
||||
|
||||
def test_division_by_zero_expr(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("5/(3-3)")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
"""D3 — whole-valued results are int, non-whole are float."""
|
||||
|
||||
def test_whole_division_is_int(self):
|
||||
result = calc("4/2")
|
||||
self.assertEqual(result, 2)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_non_whole_is_float(self):
|
||||
result = calc("7/2")
|
||||
self.assertEqual(result, 3.5)
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_integer_arithmetic_stays_int(self):
|
||||
result = calc("2+3")
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_whole_negative_is_int(self):
|
||||
result = calc("6/2")
|
||||
self.assertEqual(result, 3)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
134
calculators/builder-adversary/run-01/calc/test_lexer.py
Normal file
134
calculators/builder-adversary/run-01/calc/test_lexer.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""Unit tests for calc.lexer — covers D1, D2, D3."""
|
||||
|
||||
import unittest
|
||||
from calc.lexer import tokenize, Token, LexError
|
||||
|
||||
|
||||
def kinds(src):
|
||||
return [t.kind for t in tokenize(src)]
|
||||
|
||||
|
||||
def values(src):
|
||||
return [(t.kind, t.value) for t in tokenize(src)]
|
||||
|
||||
|
||||
class TestNumbers(unittest.TestCase):
|
||||
"""D1 — integers and floats."""
|
||||
|
||||
def test_integer(self):
|
||||
toks = tokenize("42")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertEqual(toks[0].value, 42)
|
||||
self.assertIsInstance(toks[0].value, int)
|
||||
self.assertEqual(toks[1].kind, 'EOF')
|
||||
|
||||
def test_float(self):
|
||||
toks = tokenize("3.14")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 3.14)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_leading_dot(self):
|
||||
toks = tokenize(".5")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 0.5)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_float_trailing_dot(self):
|
||||
toks = tokenize("10.")
|
||||
self.assertEqual(toks[0].kind, 'NUMBER')
|
||||
self.assertAlmostEqual(toks[0].value, 10.0)
|
||||
self.assertIsInstance(toks[0].value, float)
|
||||
|
||||
def test_integer_eof(self):
|
||||
toks = tokenize("42")
|
||||
self.assertEqual(kinds("42"), ['NUMBER', 'EOF'])
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
"""D2 — operators and parentheses."""
|
||||
|
||||
def test_plus(self):
|
||||
self.assertIn('PLUS', kinds("+"))
|
||||
|
||||
def test_minus(self):
|
||||
self.assertIn('MINUS', kinds("-"))
|
||||
|
||||
def test_star(self):
|
||||
self.assertIn('STAR', kinds("*"))
|
||||
|
||||
def test_slash(self):
|
||||
self.assertIn('SLASH', kinds("/"))
|
||||
|
||||
def test_lparen(self):
|
||||
self.assertIn('LPAREN', kinds("("))
|
||||
|
||||
def test_rparen(self):
|
||||
self.assertIn('RPAREN', kinds(")"))
|
||||
|
||||
def test_expr_kinds(self):
|
||||
self.assertEqual(
|
||||
kinds("1+2*3"),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
def test_complex_expr(self):
|
||||
self.assertEqual(
|
||||
kinds("3.5*(1-2)"),
|
||||
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'],
|
||||
)
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
"""D3 — whitespace is skipped; invalid chars raise LexError."""
|
||||
|
||||
def test_whitespace_skipped(self):
|
||||
self.assertEqual(
|
||||
kinds(" 12 + 3 "),
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'EOF'],
|
||||
)
|
||||
|
||||
def test_tab_whitespace(self):
|
||||
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
|
||||
|
||||
def test_at_raises_lexerror(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
self.assertIn('@', str(ctx.exception))
|
||||
|
||||
def test_dollar_raises_lexerror(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("$")
|
||||
self.assertIn('$', str(ctx.exception))
|
||||
|
||||
def test_letter_raises_lexerror(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("x")
|
||||
self.assertIn('x', str(ctx.exception))
|
||||
|
||||
def test_lexerror_includes_position(self):
|
||||
with self.assertRaises(LexError) as ctx:
|
||||
tokenize("1 @ 2")
|
||||
# position 2 (0-indexed) where '@' appears
|
||||
self.assertIn('2', str(ctx.exception))
|
||||
|
||||
def test_required_whitespace_expr(self):
|
||||
# " 12 + 3 " from the DoD
|
||||
toks = tokenize(" 12 + 3 ")
|
||||
self.assertEqual(toks[0].value, 12)
|
||||
self.assertEqual(toks[2].value, 3)
|
||||
|
||||
def test_required_complex_expr(self):
|
||||
# "3.5*(1-2)" from the DoD
|
||||
toks = tokenize("3.5*(1-2)")
|
||||
self.assertAlmostEqual(toks[0].value, 3.5)
|
||||
self.assertEqual(toks[1].kind, 'STAR')
|
||||
self.assertEqual(toks[2].kind, 'LPAREN')
|
||||
self.assertEqual(toks[3].value, 1)
|
||||
self.assertEqual(toks[4].kind, 'MINUS')
|
||||
self.assertEqual(toks[5].value, 2)
|
||||
self.assertEqual(toks[6].kind, 'RPAREN')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
149
calculators/builder-adversary/run-01/calc/test_parser.py
Normal file
149
calculators/builder-adversary/run-01/calc/test_parser.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""Tests for calc/parser.py — D1 through D5."""
|
||||
|
||||
import unittest
|
||||
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError, Num, BinOp, Unary
|
||||
|
||||
|
||||
def p(src: str):
|
||||
"""Shorthand: tokenize + parse."""
|
||||
return parse(tokenize(src))
|
||||
|
||||
|
||||
class TestPrecedence(unittest.TestCase):
|
||||
"""D1 — * and / bind tighter than + and -."""
|
||||
|
||||
def test_add_then_mul(self):
|
||||
# 1+2*3 → BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
|
||||
tree = p("1+2*3")
|
||||
self.assertEqual(tree, BinOp('+', Num(1), BinOp('*', Num(2), Num(3))))
|
||||
|
||||
def test_mul_then_add(self):
|
||||
# 2*3+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
tree = p("2*3+4")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
def test_sub_then_div(self):
|
||||
# 10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2)))
|
||||
tree = p("10-6/2")
|
||||
self.assertEqual(tree, BinOp('-', Num(10), BinOp('/', Num(6), Num(2))))
|
||||
|
||||
def test_div_then_sub(self):
|
||||
# 6/2-1 → BinOp('-', BinOp('/', Num(6), Num(2)), Num(1))
|
||||
tree = p("6/2-1")
|
||||
self.assertEqual(tree, BinOp('-', BinOp('/', Num(6), Num(2)), Num(1)))
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
"""D2 — same-precedence operators associate left."""
|
||||
|
||||
def test_sub_left(self):
|
||||
# 8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))
|
||||
tree = p("8-3-2")
|
||||
self.assertEqual(tree, BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
|
||||
|
||||
def test_div_left(self):
|
||||
# 8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
|
||||
tree = p("8/4/2")
|
||||
self.assertEqual(tree, BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
|
||||
|
||||
def test_add_left(self):
|
||||
# 1+2+3 → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("1+2+3")
|
||||
self.assertEqual(tree, BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_mul_left(self):
|
||||
# 2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4))
|
||||
tree = p("2*3*4")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
"""D3 — parentheses override precedence."""
|
||||
|
||||
def test_paren_overrides_mul(self):
|
||||
# (1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
|
||||
tree = p("(1+2)*3")
|
||||
self.assertEqual(tree, BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)))
|
||||
|
||||
def test_paren_on_right(self):
|
||||
# 3*(1+2) → BinOp('*', Num(3), BinOp('+', Num(1), Num(2)))
|
||||
tree = p("3*(1+2)")
|
||||
self.assertEqual(tree, BinOp('*', Num(3), BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_nested_parens(self):
|
||||
# ((4)) → Num(4)
|
||||
tree = p("((4))")
|
||||
self.assertEqual(tree, Num(4))
|
||||
|
||||
def test_paren_changes_assoc(self):
|
||||
# 8-(3-2) → BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))
|
||||
tree = p("8-(3-2)")
|
||||
self.assertEqual(tree, BinOp('-', Num(8), BinOp('-', Num(3), Num(2))))
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
"""D4 — leading and nested unary minus."""
|
||||
|
||||
def test_simple_negative(self):
|
||||
# -5 → Unary('-', Num(5))
|
||||
tree = p("-5")
|
||||
self.assertEqual(tree, Unary('-', Num(5)))
|
||||
|
||||
def test_unary_paren(self):
|
||||
# -(1+2) → Unary('-', BinOp('+', Num(1), Num(2)))
|
||||
tree = p("-(1+2)")
|
||||
self.assertEqual(tree, Unary('-', BinOp('+', Num(1), Num(2))))
|
||||
|
||||
def test_mul_unary_rhs(self):
|
||||
# 3 * -2 → BinOp('*', Num(3), Unary('-', Num(2)))
|
||||
tree = p("3 * -2")
|
||||
self.assertEqual(tree, BinOp('*', Num(3), Unary('-', Num(2))))
|
||||
|
||||
def test_double_unary(self):
|
||||
# --5 → Unary('-', Unary('-', Num(5)))
|
||||
tree = p("--5")
|
||||
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
|
||||
|
||||
def test_unary_in_expr(self):
|
||||
# 1 + -2 → BinOp('+', Num(1), Unary('-', Num(2)))
|
||||
tree = p("1 + -2")
|
||||
self.assertEqual(tree, BinOp('+', Num(1), Unary('-', Num(2))))
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
"""D5 — malformed input raises ParseError."""
|
||||
|
||||
def test_trailing_operator(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("1 +")
|
||||
|
||||
def test_unclosed_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("(1")
|
||||
|
||||
def test_two_numbers(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("1 2")
|
||||
|
||||
def test_close_open_paren(self):
|
||||
# ")(" has no valid parse
|
||||
with self.assertRaises(ParseError):
|
||||
p(")(")
|
||||
|
||||
def test_empty_string(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("")
|
||||
|
||||
def test_close_paren_only(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p(")")
|
||||
|
||||
def test_only_operator(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p("*")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -0,0 +1,7 @@
|
||||
# BACKLOG — eval phase
|
||||
|
||||
## Build backlog
|
||||
_(Builder's section — read-only for Adversary)_
|
||||
|
||||
## Adversary findings
|
||||
_(none yet)_
|
||||
@ -0,0 +1,13 @@
|
||||
# BACKLOG-lex
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/__init__.py
|
||||
- [x] Create calc/lexer.py with Token, LexError, tokenize()
|
||||
- [x] Create calc/test_lexer.py covering D1–D4 (21 tests)
|
||||
- [x] Run tests: 21 passed, 0 failed
|
||||
- [ ] Claim D1 + D2 + D3 + D4 (all gates, single claim)
|
||||
- [ ] Await Adversary verification
|
||||
|
||||
## Adversary findings
|
||||
(none yet)
|
||||
@ -0,0 +1,20 @@
|
||||
# BACKLOG — Phase parse
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Create calc/parser.py (ParseError, Num, BinOp, Unary, recursive-descent parse())
|
||||
- [x] Create calc/test_parser.py (24 unittest tests, D1–D5 coverage)
|
||||
- [x] Verified 45 tests pass (21 lexer + 24 parser), 0 failures
|
||||
- [x] CLAIM D1–D6 (all gates claimed together)
|
||||
|
||||
## Adversary findings
|
||||
|
||||
(none yet)
|
||||
|
||||
## Break-it probes planned
|
||||
|
||||
- Precedence weak test: ensure `1+2*3` really builds `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` not `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
|
||||
- Associativity weak test: ensure `8-3-2` builds `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` not `BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))`
|
||||
- Nested unary: `--5`, `-(-(1+2))` should work
|
||||
- ParseError specificity: check it's ParseError (not generic Exception) for all 5 error cases in D5
|
||||
- Empty input edge case
|
||||
@ -0,0 +1,12 @@
|
||||
# DECISIONS — shared append-only log
|
||||
|
||||
## 2026-06-15T00:12Z — Adversary initialized
|
||||
Adversary started for phase lex. Watching for Builder claims via git.
|
||||
|
||||
## 2026-06-15 — Token representation (Builder)
|
||||
Using a dataclass for Token with `kind: str` and `value`. The `kind` is a plain string constant
|
||||
(e.g. "NUMBER"), not an enum, keeping pure stdlib with minimal boilerplate.
|
||||
|
||||
## 2026-06-15 — NUMBER value type (Builder)
|
||||
`value` for NUMBER tokens is `int` if no decimal point, else `float`. Matches Python's natural
|
||||
numeric types; convenient for the evaluator phase.
|
||||
@ -0,0 +1,43 @@
|
||||
# JOURNAL — eval phase
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Built evaluator, CLI, and tests in one go.
|
||||
|
||||
### evaluator.py
|
||||
|
||||
`evaluate(node)` walks the AST recursively:
|
||||
- `Num` → return `node.value` directly (already int or float from lexer)
|
||||
- `Unary('-', operand)` → negate result
|
||||
- `BinOp(op, left, right)` → evaluate both sides, apply op
|
||||
|
||||
Division: uses Python `left / right` (always returns float). If `right == 0`, raises `EvalError("Division by zero")`.
|
||||
|
||||
D3 result type rule: after computing a float result in BinOp, if `result == int(result)` we cast to int. This ensures `4/2 → 2` (int) and `7/2 → 3.5` (float). Integer arithmetic returns int naturally.
|
||||
|
||||
### calc.py CLI
|
||||
|
||||
`main()` accepts exactly one argv argument (the expression string).
|
||||
Catches `LexError`, `ParseError`, `EvalError` → prints `Error: <msg>` to stderr, exits 1.
|
||||
On success prints `result` (which is already int or float with correct type per D3 rule).
|
||||
|
||||
### Test run output
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 62 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
(45 prior tests from lex+parse phases, 17 new evaluator tests)
|
||||
|
||||
### CLI checks
|
||||
|
||||
```
|
||||
$ python calc.py "2+3*4" → 14
|
||||
$ python calc.py "(2+3)*4" → 20
|
||||
$ python calc.py "7/2" → 3.5
|
||||
$ python calc.py "4/2" → 2
|
||||
$ python calc.py "1/0" → Error: Division by zero (exit 1)
|
||||
$ python calc.py "1 +" → Error: Unexpected end of input (exit 1)
|
||||
```
|
||||
@ -0,0 +1,48 @@
|
||||
# JOURNAL-lex — Adversary
|
||||
|
||||
## 2026-06-15T00:12Z — Cold start
|
||||
Read phase plan lex.md. Builder has not pushed any code yet (only seed commit 13c0db5).
|
||||
Initialized REVIEW, BACKLOG, JOURNAL files. Waiting for Builder to push work.
|
||||
|
||||
DoD gates to verify:
|
||||
- D1: numbers (integers and floats)
|
||||
- D2: operators & parens
|
||||
- D3: whitespace & errors (LexError)
|
||||
- D4: tests green (python -m unittest -q)
|
||||
|
||||
## 2026-06-15T00:20Z — Verification complete
|
||||
Builder pushed claim(D1-D4). Pulled, ran cold verification, ran break-it probes.
|
||||
All four gates PASS. No vetoes. Wrote verdicts to REVIEW-lex.md.
|
||||
Adversary probes: empty string, lone dot, whitespace-only, double-dot, 1.2.3, position accuracy — all held.
|
||||
|
||||
---
|
||||
|
||||
# JOURNAL-lex — Builder
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Read phase plan. Built calc/lexer.py and calc/test_lexer.py from scratch.
|
||||
|
||||
### Implementation choices
|
||||
- Token is a dataclass with `kind: str` and `value: int | float | str | None`
|
||||
- NUMBER value is int (no dot) or float (dot present)
|
||||
- LexError message includes repr of the offending char and its 0-based position
|
||||
- Leading dot (`.5`) and trailing dot (`10.`) are valid floats (scanned by loop: stops when second dot seen)
|
||||
- Single lone dot is an error (raw == '.')
|
||||
|
||||
### Test run output
|
||||
```
|
||||
python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 21 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
|
||||
### Verification commands output
|
||||
```
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
calc.lexer.LexError: Unexpected character '@' at position 2
|
||||
```
|
||||
@ -0,0 +1,44 @@
|
||||
# JOURNAL — Phase parse (Adversary)
|
||||
|
||||
## 2026-06-15T00:18Z — Adversary initialized
|
||||
- Phase parse kicked off. Lex phase confirmed ## DONE.
|
||||
- Initialized REVIEW-parse.md, STATUS-parse.md, BACKLOG-parse.md, JOURNAL-parse.md.
|
||||
- Watching for Builder to claim gates D1–D6.
|
||||
- Planned break-it probes logged in BACKLOG.
|
||||
|
||||
## 2026-06-15T00:22Z — Cold verification complete, all gates PASS
|
||||
- Watchdog pinged: Builder claimed D1–D6 in commit 7768832.
|
||||
- Read parser.py and test_parser.py cold (no prior state).
|
||||
- Ran `python -m unittest -q` → 45 tests, 0 failures.
|
||||
- Ran all AST shape checks from STATUS-parse.md — every output matched expected.
|
||||
- Ran full break-it probe suite: right-assoc trap, triple unary, deep nesting, float, extra error cases.
|
||||
- All held. No defects found. PASS recorded in REVIEW-parse.md for D1–D6.
|
||||
|
||||
---
|
||||
|
||||
# JOURNAL — Phase parse (Builder)
|
||||
|
||||
## 2026-06-15T00:19Z — Implementation complete
|
||||
|
||||
Design: recursive-descent with grammar:
|
||||
```
|
||||
expr := term (('+' | '-') term)*
|
||||
term := unary (('*' | '/') unary)*
|
||||
unary := '-' unary | primary
|
||||
primary := NUMBER | '(' expr ')'
|
||||
```
|
||||
Left-associativity emerges from iterative loops (not recursion) in expr/term.
|
||||
|
||||
Ran `python -m unittest -q` → `Ran 45 tests in 0.001s OK`
|
||||
|
||||
Key outputs verified:
|
||||
- `1+2*3` → `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` ✓
|
||||
- `8-3-2` → `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` ✓
|
||||
- `8/4/2` → `BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))` ✓
|
||||
- `(1+2)*3` → `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))` ✓
|
||||
- `-5` → `Unary('-', Num(5))` ✓
|
||||
- `-(1+2)` → `Unary('-', BinOp('+', Num(1), Num(2)))` ✓
|
||||
- `3 * -2` → `BinOp('*', Num(3), Unary('-', Num(2)))` ✓
|
||||
- All D5 error cases → `ParseError` ✓
|
||||
|
||||
Claiming all gates D1–D6.
|
||||
@ -0,0 +1,96 @@
|
||||
# REVIEW — eval phase (Adversary)
|
||||
|
||||
_Adversary cold-verification log. Each verdict is recorded here._
|
||||
|
||||
## Status summary
|
||||
- D1 arithmetic: PASS
|
||||
- D2 division + EvalError: PASS
|
||||
- D3 result type: PASS
|
||||
- D4 CLI: PASS
|
||||
- D5 tests green + end-to-end: PASS
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1 — arithmetic: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
Cold run, commit 165c7cc.
|
||||
|
||||
Commands run and results:
|
||||
```
|
||||
python calc.py "2+3*4" → 14 ✓
|
||||
python calc.py "(2+3)*4" → 20 ✓
|
||||
python calc.py "8-3-2" → 3 ✓
|
||||
python calc.py "-2+5" → 3 ✓
|
||||
python calc.py "2*-3" → -6 ✓
|
||||
python calc.py "--5" → 5 ✓ (double unary)
|
||||
python calc.py "10-3-2" → 5 ✓ (left-associativity)
|
||||
python calc.py "-(2+3)" → -5 ✓ (unary on paren)
|
||||
```
|
||||
|
||||
### D2 — division + EvalError: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
python calc.py "7/2" → 3.5 ✓ (true division)
|
||||
python calc.py "1/0" → "Error: Division by zero" on stderr, exit 1 ✓
|
||||
python calc.py "5/(3-3)" → "Error: Division by zero" on stderr, exit 1 ✓
|
||||
```
|
||||
|
||||
Verified `EvalError` is raised (not bare `ZeroDivisionError`) via:
|
||||
- `calc/evaluator.py:37-38` explicitly checks `right == 0` and raises `EvalError`
|
||||
- `calc.py:24` catches `EvalError` — ZeroDivisionError would escape if not caught; confirmed not raised
|
||||
|
||||
`EvalError` is a proper subclass of `Exception`: confirmed True.
|
||||
|
||||
### D3 — result type: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
evaluate(parse(tokenize("4/2"))) → int 2 ✓
|
||||
evaluate(parse(tokenize("7/2"))) → float 3.5 ✓
|
||||
evaluate(parse(tokenize("2+3"))) → int 5 ✓ (integer arithmetic stays int)
|
||||
evaluate(parse(tokenize("-6/2"))) → int -3 ✓ (negative whole result is int)
|
||||
evaluate(parse(tokenize("1000*1000"))) → int 1000000 ✓
|
||||
```
|
||||
|
||||
Rule documented in `calc/evaluator.py` docstring. `print(4/2)` outputs `2` (not `2.0`). ✓
|
||||
|
||||
### D4 — CLI: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
python calc.py "2+3*4" → prints 14, exit 0 ✓
|
||||
python calc.py "1 +" → "Error: Unexpected end of input" on stderr, exit 1, no traceback ✓
|
||||
python calc.py "1/0" → "Error: Division by zero" on stderr, exit 1, no traceback ✓
|
||||
python calc.py "" → "Error: Empty input" on stderr, exit 1 ✓
|
||||
python calc.py → usage message on stderr, exit 1 ✓
|
||||
python calc.py "((1+2)" → error on stderr, exit 1, no traceback ✓
|
||||
```
|
||||
|
||||
No tracebacks in any error path. ✓
|
||||
|
||||
### D5 — tests green + end-to-end: PASS @2026-06-15T00:27:02Z
|
||||
|
||||
```
|
||||
python -m unittest -q → Ran 62 tests in 0.001s OK
|
||||
```
|
||||
|
||||
62 tests = lex + parse + evaluator suites. 0 failures, 0 errors. No regression. ✓
|
||||
|
||||
## Adversary findings
|
||||
|
||||
_(none — all gates pass, no defects found)_
|
||||
|
||||
## Break-it probes run
|
||||
|
||||
- Traceback check on all error paths: no traceback in any case ✓
|
||||
- No-args invocation: graceful usage error ✓
|
||||
- Empty string input: graceful error ✓
|
||||
- Double unary minus `--5` → 5 ✓
|
||||
- Left-associativity `10-3-2` → 5 ✓
|
||||
- Unary in division `-8/2` → -4 ✓
|
||||
- Negative whole result type `-6/2` → int -3 ✓
|
||||
- Large numbers `1000*1000` → int 1000000 ✓
|
||||
- Division by zero via expression `5/(3-3)` → EvalError ✓
|
||||
- Unclosed paren `((1+2)` → parse error, no crash ✓
|
||||
|
||||
---
|
||||
Initialized: 2026-06-15T00:24:45Z
|
||||
Verdicts filed: 2026-06-15T00:27:02Z
|
||||
@ -0,0 +1,67 @@
|
||||
# REVIEW-lex — Adversary verdicts
|
||||
|
||||
Phase: lex
|
||||
Adversary cold-started: 2026-06-15
|
||||
|
||||
## Status
|
||||
All gates verified PASS. No vetoes.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### D1: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence (cold run from work-adv):**
|
||||
```
|
||||
tokenize("42") → NUMBER(42 int), EOF ✓
|
||||
tokenize("3.14") → NUMBER(3.14 float) ✓
|
||||
tokenize(".5") → NUMBER(0.5 float) ✓
|
||||
tokenize("10.") → NUMBER(10.0 float) ✓
|
||||
```
|
||||
Plan spec: "tokenize('42') → [NUMBER(42), EOF]" — confirmed exactly.
|
||||
|
||||
### D2: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
kinds("1+2*3") → ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓
|
||||
all operators +-*/() → PLUS MINUS STAR SLASH LPAREN RPAREN ✓
|
||||
tokens("3.5*(1-2)") → [('NUMBER', 3.5), ('STAR','*'), ('LPAREN','('), ('NUMBER', 1),
|
||||
('MINUS','-'), ('NUMBER', 2), ('RPAREN',')'), ('EOF', None)] ✓
|
||||
```
|
||||
|
||||
### D3: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
tokenize(" 12 + 3 ") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓ (whitespace skipped)
|
||||
tokenize("1 @ 2") → LexError: Unexpected character '@' at position 2 ✓
|
||||
tokenize("$") → LexError: ... '$' ... ✓
|
||||
tokenize("x") → LexError: ... 'x' at position 0 ✓
|
||||
tokenize(".") → LexError: ... '.' at position 0 ✓
|
||||
LexError message includes offending char AND position ✓
|
||||
```
|
||||
|
||||
### D4: PASS @2026-06-15T00:20Z
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 21 tests in 0.000s
|
||||
OK
|
||||
```
|
||||
Exit code: 0. 21 tests, 0 failures.
|
||||
Required cases covered: " 12 + 3 " ✓, "3.5*(1-2)" ✓, "1 @ 2" → LexError ✓.
|
||||
|
||||
## Adversarial probes (break-it attempts — all held)
|
||||
|
||||
- Empty string → `[EOF]` (correct)
|
||||
- Whitespace-only → `[EOF]` (correct)
|
||||
- Lone dot → `LexError: Unexpected character '.' at position 0` (correct)
|
||||
- `..` → LexError on lone dot (correct)
|
||||
- `1.2.3` → `NUMBER(1.2) NUMBER(0.3) EOF` (valid lexer behavior; parser rejects)
|
||||
- `12 @ 5` → LexError at position 3 (position accuracy confirmed)
|
||||
- All operators in sequence `+-*/()` → correct kinds
|
||||
- Newline treated as whitespace (conservative, robust)
|
||||
|
||||
No defects found. Implementation is correct per the phase plan.
|
||||
@ -0,0 +1,91 @@
|
||||
# REVIEW — Phase parse (Adversary)
|
||||
|
||||
## Status
|
||||
All gates PASS. Ready to approve ## DONE.
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
| Gate | Verdict | Timestamp | Evidence |
|
||||
|------|---------|-----------|----------|
|
||||
| D1 — precedence | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D2 — left associativity | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D3 — parentheses | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D4 — unary minus | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D5 — errors | PASS | 2026-06-15T00:22:39Z | See below |
|
||||
| D6 — tests green | PASS | 2026-06-15T00:22:39Z | Ran 45 tests in 0.001s OK |
|
||||
|
||||
## Cold-verification evidence
|
||||
|
||||
### D6 — tests green
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 45 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
21 lexer + 24 parser tests, 0 failures.
|
||||
|
||||
### D1 — precedence (cold AST shape check)
|
||||
```
|
||||
1+2*3 → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓ (* binds tighter)
|
||||
2*3+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)) ✓
|
||||
10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2))) ✓
|
||||
```
|
||||
Independently derived: `1+2*3` must have `+` at root with `*` in right child — confirmed.
|
||||
|
||||
### D2 — left associativity (cold AST shape check)
|
||||
```
|
||||
8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓ (left-assoc)
|
||||
8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
|
||||
1+2+3 → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)) ✓
|
||||
1-2-3 → BinOp('-', BinOp('-', Num(1), Num(2)), Num(3)) ✓ (break-it: not right-assoc)
|
||||
```
|
||||
Iterative while-loop in `_expr`/`_term` enforces left-assoc by construction.
|
||||
|
||||
### D3 — parentheses (cold AST shape check)
|
||||
```
|
||||
(1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
|
||||
3*(1+2) → BinOp('*', Num(3), BinOp('+', Num(1), Num(2))) ✓
|
||||
((4)) → Num(4) ✓
|
||||
8-(3-2) → BinOp('-', Num(8), BinOp('-', Num(3), Num(2))) ✓
|
||||
((((1+2)))) → BinOp('+', Num(1), Num(2)) ✓ (deep nesting)
|
||||
```
|
||||
|
||||
### D4 — unary minus (cold AST shape check)
|
||||
```
|
||||
-5 → Unary('-', Num(5)) ✓
|
||||
-(1+2) → Unary('-', BinOp('+', Num(1), Num(2))) ✓
|
||||
3 * -2 → BinOp('*', Num(3), Unary('-', Num(2))) ✓
|
||||
--5 → Unary('-', Unary('-', Num(5))) ✓
|
||||
1 + -2 → BinOp('+', Num(1), Unary('-', Num(2))) ✓
|
||||
---5 → Unary('-', Unary('-', Unary('-', Num(5)))) ✓ (break-it: triple unary)
|
||||
6 / -2 → BinOp('/', Num(6), Unary('-', Num(2))) ✓ (break-it: unary in denom)
|
||||
```
|
||||
`_unary` is right-recursive: `'-' _unary | _primary` — correct for unary.
|
||||
|
||||
### D5 — errors (cold: all five plan cases + extras)
|
||||
```
|
||||
'1 +' → OK ParseError
|
||||
'(1' → OK ParseError
|
||||
'1 2' → OK ParseError
|
||||
')(' → OK ParseError
|
||||
'' → OK ParseError
|
||||
'*' → OK ParseError (break-it)
|
||||
')' → OK ParseError (break-it)
|
||||
'1+2)' → OK ParseError (break-it)
|
||||
'((1+2)' → OK ParseError (break-it)
|
||||
```
|
||||
All raise `ParseError` specifically, not a generic exception.
|
||||
|
||||
## Break-it probes run
|
||||
- Right-assoc trap (`1-2-3`, `8-3-2`, `8/4/2`): held — correctly left-assoc
|
||||
- Deep nesting `((((1+2))))`: held
|
||||
- Triple unary `---5`: held
|
||||
- Unary in denominator `6/-2`: held
|
||||
- Float literal `3.14 → Num(3.14)`: parsed fine
|
||||
- Trailing `)` after valid expr `1+2)`: ParseError ✓
|
||||
- Solo operator `*`: ParseError ✓
|
||||
- Double-unclosed paren `((1+2)`: ParseError ✓
|
||||
|
||||
## Adversary findings
|
||||
None. No defects found. All probes held.
|
||||
@ -0,0 +1,23 @@
|
||||
# STATUS — Phase eval
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase eval is complete. This is the final phase — the calculator is end-to-end: string → tokens → AST → number.
|
||||
|
||||
| Gate | Status | Verified |
|
||||
|------|--------|----------|
|
||||
| D1 — arithmetic | PASS | 2026-06-15T00:27:02Z |
|
||||
| D2 — division + EvalError | PASS | 2026-06-15T00:27:02Z |
|
||||
| D3 — result type | PASS | 2026-06-15T00:27:02Z |
|
||||
| D4 — CLI | PASS | 2026-06-15T00:27:02Z |
|
||||
| D5 — tests green + end-to-end | PASS | 2026-06-15T00:27:02Z |
|
||||
|
||||
Adversary ran 62 tests (exit 0), cold-verified all D1–D5 gates, and ran full break-it probe suite
|
||||
(double unary, left-assoc, negative whole result type, division by zero via expression, unclosed
|
||||
paren, empty string, no-args) — all held. No defects found.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `calc/evaluator.py` — `EvalError`, `evaluate()`
|
||||
- `calc/test_evaluator.py` — 17 unittest tests (D1–D3 coverage, type assertions)
|
||||
- `calc.py` — top-level CLI (D4 + end-to-end check)
|
||||
@ -0,0 +1,20 @@
|
||||
# STATUS — Phase lex
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase lex is complete.
|
||||
|
||||
| Gate | Status | Verified |
|
||||
|------|--------|----------|
|
||||
| D1 — numbers | PASS | 2026-06-15T00:20Z |
|
||||
| D2 — operators & parens | PASS | 2026-06-15T00:20Z |
|
||||
| D3 — whitespace & errors | PASS | 2026-06-15T00:20Z |
|
||||
| D4 — tests green | PASS | 2026-06-15T00:20Z |
|
||||
|
||||
Adversary ran 21 tests (exit 0), cold-verified all token outputs, and ran break-it probes — all held.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
|
||||
- `calc/test_lexer.py` — 21 unittest tests
|
||||
- `calc/__init__.py` — package marker
|
||||
@ -0,0 +1,22 @@
|
||||
# STATUS — Phase parse
|
||||
|
||||
## DONE
|
||||
|
||||
All DoD gates Adversary-verified PASS. Phase parse is complete.
|
||||
|
||||
| Gate | Status | Verified |
|
||||
|------|--------|----------|
|
||||
| D1 — precedence | PASS | 2026-06-15T00:22:39Z |
|
||||
| D2 — left associativity | PASS | 2026-06-15T00:22:39Z |
|
||||
| D3 — parentheses | PASS | 2026-06-15T00:22:39Z |
|
||||
| D4 — unary minus | PASS | 2026-06-15T00:22:39Z |
|
||||
| D5 — errors | PASS | 2026-06-15T00:22:39Z |
|
||||
| D6 — tests green | PASS | 2026-06-15T00:22:39Z |
|
||||
|
||||
Adversary ran 45 tests (exit 0), cold-verified all AST shapes, and ran full break-it probe suite
|
||||
(right-assoc trap, triple unary, deep nesting, float, extra error cases) — all held. No defects found.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `calc/parser.py` — ParseError, Num, BinOp, Unary, parse()
|
||||
- `calc/test_parser.py` — 24 unittest tests (D1–D5 coverage, tree-structure assertions)
|
||||
Reference in New Issue
Block a user