artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs
This commit is contained in:
16
calculators/builder-adversary/run-05/GIT-LOG.txt
Normal file
16
calculators/builder-adversary/run-05/GIT-LOG.txt
Normal file
@ -0,0 +1,16 @@
|
||||
# git history (claim/review handshake), from the run's shared bare repo
|
||||
e34a7a8 status(eval): ## DONE — all D1-D5 Adversary-verified PASS
|
||||
691f290 review(D1,D2,D3,D4,D5): PASS — all 56 tests green, all DoD commands verified cold
|
||||
9662369 claim(D1,D2,D3,D4,D5): eval phase complete — 56 tests green, CLI verified
|
||||
2e76913 review(eval/init): Adversary initialized for eval phase, awaiting Builder claims
|
||||
aee82a9 status: parse phase DONE — all D1-D6 Adversary-verified PASS
|
||||
f87dc76 review(D1,D2,D3,D4,D5,D6): PASS — all 41 tests green, all DoD commands verified cold
|
||||
c965498 status: add commit sha to parse claim
|
||||
23d0ae9 claim(D1,D2,D3,D4,D5,D6): parser complete, all 41 tests green
|
||||
ae7bfb0 review(parse/init): Adversary initialized for parse phase, awaiting Builder claims
|
||||
496ad69 status: phase lex DONE — all D1-D4 PASS from Adversary
|
||||
fc428fc review(D1,D2,D3,D4): PASS — all 22 tests green, all DoD commands verified cold
|
||||
43d4c26 status: add commit sha to claim
|
||||
7022854 claim(D1,D2,D3,D4): lexer complete, all 22 tests green
|
||||
0fa8ea4 review(init): Adversary initialized, watching for Builder claims
|
||||
c6a3e58 chore: seed
|
||||
1
calculators/builder-adversary/run-05/README.md
Normal file
1
calculators/builder-adversary/run-05/README.md
Normal file
@ -0,0 +1 @@
|
||||
# calc work repo
|
||||
1
calculators/builder-adversary/run-05/SOURCE.txt
Normal file
1
calculators/builder-adversary/run-05/SOURCE.txt
Normal file
@ -0,0 +1 @@
|
||||
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r5
|
||||
26
calculators/builder-adversary/run-05/calc.py
Normal file
26
calculators/builder-adversary/run-05/calc.py
Normal file
@ -0,0 +1,26 @@
|
||||
import sys
|
||||
from calc.lexer import tokenize, LexError
|
||||
from calc.parser import parse, ParseError
|
||||
from calc.evaluator import evaluate, EvalError
|
||||
|
||||
|
||||
def _fmt(result) -> str:
|
||||
return str(result)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: calc.py <expression>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
try:
|
||||
tokens = tokenize(sys.argv[1])
|
||||
ast = parse(tokens)
|
||||
result = evaluate(ast)
|
||||
print(_fmt(result))
|
||||
except (LexError, ParseError, EvalError) as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
33
calculators/builder-adversary/run-05/calc/evaluator.py
Normal file
33
calculators/builder-adversary/run-05/calc/evaluator.py
Normal file
@ -0,0 +1,33 @@
|
||||
from calc.parser import Num, BinOp, Unary
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(node) -> "int | float":
|
||||
"""Walk the AST and return a numeric result.
|
||||
|
||||
Type rule: integer arithmetic stays int; division returns float, except when
|
||||
the quotient is whole-valued — then it is normalised to int so the CLI can
|
||||
print it without a trailing '.0'.
|
||||
"""
|
||||
if isinstance(node, Num):
|
||||
return node.value
|
||||
if isinstance(node, Unary):
|
||||
return -evaluate(node.operand)
|
||||
if isinstance(node, BinOp):
|
||||
left = evaluate(node.left)
|
||||
right = evaluate(node.right)
|
||||
if node.op == 'PLUS':
|
||||
return left + right
|
||||
if node.op == 'MINUS':
|
||||
return left - right
|
||||
if node.op == 'STAR':
|
||||
return left * right
|
||||
if node.op == 'SLASH':
|
||||
if right == 0:
|
||||
raise EvalError("division by zero")
|
||||
result = left / right
|
||||
return int(result) if result == int(result) else result
|
||||
raise EvalError(f"unknown node: {type(node).__name__}")
|
||||
56
calculators/builder-adversary/run-05/calc/lexer.py
Normal file
56
calculators/builder-adversary/run-05/calc/lexer.py
Normal file
@ -0,0 +1,56 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class LexError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
kind: str
|
||||
value: Union[int, float, None] = 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.isdigit() or ch == '.':
|
||||
j = i
|
||||
while j < n and src[j].isdigit():
|
||||
j += 1
|
||||
if j < n and src[j] == '.':
|
||||
j += 1
|
||||
while j < n and src[j].isdigit():
|
||||
j += 1
|
||||
tokens.append(Token('NUMBER', float(src[i:j])))
|
||||
else:
|
||||
tokens.append(Token('NUMBER', int(src[i:j])))
|
||||
i = j
|
||||
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
|
||||
else:
|
||||
raise LexError(f"unexpected character {ch!r} at position {i}")
|
||||
tokens.append(Token('EOF'))
|
||||
return tokens
|
||||
104
calculators/builder-adversary/run-05/calc/parser.py
Normal file
104
calculators/builder-adversary/run-05/calc/parser.py
Normal file
@ -0,0 +1,104 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Num:
|
||||
value: Union[int, float]
|
||||
|
||||
def __repr__(self):
|
||||
return f"Num({self.value})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinOp:
|
||||
op: str
|
||||
left: object
|
||||
right: object
|
||||
|
||||
def __repr__(self):
|
||||
return f"BinOp({self.op}, {self.left!r}, {self.right!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary:
|
||||
op: str
|
||||
operand: object
|
||||
|
||||
def __repr__(self):
|
||||
return f"Unary({self.op}, {self.operand!r})"
|
||||
|
||||
|
||||
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!r}, got {tok.kind!r}")
|
||||
self.pos += 1
|
||||
return tok
|
||||
|
||||
def _expr(self):
|
||||
left = self._term()
|
||||
while self._peek().kind in ('PLUS', 'MINUS'):
|
||||
op = self._consume().kind
|
||||
right = self._term()
|
||||
left = BinOp(op, left, right)
|
||||
return left
|
||||
|
||||
def _term(self):
|
||||
left = self._unary()
|
||||
while self._peek().kind in ('STAR', 'SLASH'):
|
||||
op = self._consume().kind
|
||||
right = self._unary()
|
||||
left = BinOp(op, left, right)
|
||||
return left
|
||||
|
||||
def _unary(self):
|
||||
if self._peek().kind == 'MINUS':
|
||||
self._consume()
|
||||
return Unary('MINUS', self._unary())
|
||||
return self._primary()
|
||||
|
||||
def _primary(self):
|
||||
tok = self._peek()
|
||||
if tok.kind == 'NUMBER':
|
||||
self._consume()
|
||||
return Num(tok.value)
|
||||
if tok.kind == 'LPAREN':
|
||||
self._consume()
|
||||
node = self._expr()
|
||||
if self._peek().kind != 'RPAREN':
|
||||
raise ParseError(f"expected ')', got {self._peek().kind!r}")
|
||||
self._consume()
|
||||
return node
|
||||
raise ParseError(f"unexpected token {tok.kind!r}")
|
||||
|
||||
|
||||
def parse(tokens) -> object:
|
||||
"""Parse a token list into an AST.
|
||||
|
||||
Nodes:
|
||||
Num(value) — numeric literal (int or float)
|
||||
BinOp(op, left, right) — binary op; op in {'PLUS','MINUS','STAR','SLASH'}
|
||||
Unary(op, operand) — unary minus; op == 'MINUS'
|
||||
|
||||
Raises ParseError on malformed input.
|
||||
"""
|
||||
p = _Parser(tokens)
|
||||
if p._peek().kind == 'EOF':
|
||||
raise ParseError("empty input")
|
||||
node = p._expr()
|
||||
if p._peek().kind != 'EOF':
|
||||
raise ParseError(f"unexpected token {p._peek().kind!r} after expression")
|
||||
return node
|
||||
74
calculators/builder-adversary/run-05/calc/test_evaluator.py
Normal file
74
calculators/builder-adversary/run-05/calc/test_evaluator.py
Normal file
@ -0,0 +1,74 @@
|
||||
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):
|
||||
def test_add_mul_precedence(self):
|
||||
self.assertEqual(calc("2+3*4"), 14)
|
||||
|
||||
def test_paren_override(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_unary_in_mul(self):
|
||||
self.assertEqual(calc("2*-3"), -6)
|
||||
|
||||
def test_add(self):
|
||||
self.assertEqual(calc("1+2"), 3)
|
||||
|
||||
def test_sub(self):
|
||||
self.assertEqual(calc("5-3"), 2)
|
||||
|
||||
def test_mul(self):
|
||||
self.assertEqual(calc("3*4"), 12)
|
||||
|
||||
|
||||
class TestDivision(unittest.TestCase):
|
||||
def test_true_division(self):
|
||||
self.assertEqual(calc("7/2"), 3.5)
|
||||
|
||||
def test_exact_division(self):
|
||||
self.assertEqual(calc("4/2"), 2)
|
||||
|
||||
def test_division_by_zero(self):
|
||||
with self.assertRaises(EvalError):
|
||||
calc("1/0")
|
||||
|
||||
def test_division_by_zero_not_bare(self):
|
||||
# Must raise EvalError, not the raw ZeroDivisionError
|
||||
try:
|
||||
calc("1/0")
|
||||
self.fail("expected EvalError")
|
||||
except EvalError:
|
||||
pass
|
||||
except ZeroDivisionError:
|
||||
self.fail("bare ZeroDivisionError escaped; must be EvalError")
|
||||
|
||||
|
||||
class TestResultType(unittest.TestCase):
|
||||
def test_int_arithmetic_stays_int(self):
|
||||
self.assertIsInstance(calc("2+3"), int)
|
||||
|
||||
def test_whole_division_returns_int(self):
|
||||
# 4/2 = 2.0 → normalised to int so fmt can omit '.0'
|
||||
self.assertIsInstance(calc("4/2"), int)
|
||||
self.assertEqual(calc("4/2"), 2)
|
||||
|
||||
def test_non_whole_division_returns_float(self):
|
||||
self.assertIsInstance(calc("7/2"), float)
|
||||
self.assertEqual(calc("7/2"), 3.5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
125
calculators/builder-adversary/run-05/calc/test_lexer.py
Normal file
125
calculators/builder-adversary/run-05/calc/test_lexer.py
Normal file
@ -0,0 +1,125 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize, Token, LexError
|
||||
|
||||
|
||||
def kinds(src):
|
||||
return [t.kind for t in tokenize(src)]
|
||||
|
||||
|
||||
def pairs(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")])
|
||||
|
||||
def test_float(self):
|
||||
result = tokenize("3.14")
|
||||
self.assertEqual(result[0], Token("NUMBER", 3.14))
|
||||
self.assertEqual(result[1], Token("EOF"))
|
||||
|
||||
def test_float_leading_dot(self):
|
||||
result = tokenize(".5")
|
||||
self.assertEqual(result[0], Token("NUMBER", 0.5))
|
||||
self.assertEqual(result[1], Token("EOF"))
|
||||
|
||||
def test_float_trailing_dot(self):
|
||||
result = tokenize("10.")
|
||||
self.assertEqual(result[0], Token("NUMBER", 10.0))
|
||||
self.assertEqual(result[1], Token("EOF"))
|
||||
|
||||
def test_number_value_int(self):
|
||||
result = tokenize("0")
|
||||
self.assertIsInstance(result[0].value, int)
|
||||
|
||||
def test_number_value_float(self):
|
||||
result = tokenize("3.14")
|
||||
self.assertIsInstance(result[0].value, float)
|
||||
|
||||
|
||||
class TestOperatorsAndParens(unittest.TestCase):
|
||||
def test_plus(self):
|
||||
self.assertIn("PLUS", kinds("+"))
|
||||
|
||||
def test_minus(self):
|
||||
self.assertIn("MINUS", kinds("-"))
|
||||
|
||||
def test_star(self):
|
||||
self.assertIn("STAR", kinds("*"))
|
||||
|
||||
def test_slash(self):
|
||||
self.assertIn("SLASH", kinds("/"))
|
||||
|
||||
def test_lparen(self):
|
||||
self.assertIn("LPAREN", kinds("("))
|
||||
|
||||
def test_rparen(self):
|
||||
self.assertIn("RPAREN", kinds(")"))
|
||||
|
||||
def test_expression_1_plus_2_times_3(self):
|
||||
self.assertEqual(
|
||||
kinds("1+2*3"),
|
||||
["NUMBER", "PLUS", "NUMBER", "STAR", "NUMBER", "EOF"],
|
||||
)
|
||||
|
||||
def test_all_operators(self):
|
||||
self.assertEqual(
|
||||
kinds("+-*/()"),
|
||||
["PLUS", "MINUS", "STAR", "SLASH", "LPAREN", "RPAREN", "EOF"],
|
||||
)
|
||||
|
||||
|
||||
class TestWhitespaceAndErrors(unittest.TestCase):
|
||||
def test_whitespace_between_tokens(self):
|
||||
result = tokenize(" 12 + 3 ")
|
||||
self.assertEqual(kinds(" 12 + 3 "), ["NUMBER", "PLUS", "NUMBER", "EOF"])
|
||||
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):
|
||||
self.assertEqual(
|
||||
pairs("3.5*(1-2)"),
|
||||
[
|
||||
("NUMBER", 3.5),
|
||||
("STAR", None),
|
||||
("LPAREN", None),
|
||||
("NUMBER", 1),
|
||||
("MINUS", None),
|
||||
("NUMBER", 2),
|
||||
("RPAREN", None),
|
||||
("EOF", None),
|
||||
],
|
||||
)
|
||||
|
||||
def test_invalid_at_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("1 @ 2")
|
||||
|
||||
def test_invalid_dollar_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("$5")
|
||||
|
||||
def test_invalid_letter_raises_lex_error(self):
|
||||
with self.assertRaises(LexError):
|
||||
tokenize("x")
|
||||
|
||||
def test_lex_error_message_contains_char(self):
|
||||
try:
|
||||
tokenize("1 @ 2")
|
||||
except LexError as e:
|
||||
self.assertIn("@", str(e))
|
||||
|
||||
def test_lex_error_message_contains_position(self):
|
||||
try:
|
||||
tokenize("1 @ 2")
|
||||
except LexError as e:
|
||||
self.assertIn("2", str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
104
calculators/builder-adversary/run-05/calc/test_parser.py
Normal file
104
calculators/builder-adversary/run-05/calc/test_parser.py
Normal file
@ -0,0 +1,104 @@
|
||||
import unittest
|
||||
from calc.lexer import tokenize
|
||||
from calc.parser import parse, ParseError, Num, BinOp, Unary
|
||||
|
||||
|
||||
def p(src):
|
||||
return parse(tokenize(src))
|
||||
|
||||
|
||||
class TestPrecedence(unittest.TestCase):
|
||||
def test_mul_over_add(self):
|
||||
# 1+2*3 → 1+(2*3)
|
||||
self.assertEqual(repr(p('1+2*3')),
|
||||
'BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))')
|
||||
|
||||
def test_div_over_sub(self):
|
||||
# 6-4/2 → 6-(4/2)
|
||||
self.assertEqual(repr(p('6-4/2')),
|
||||
'BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))')
|
||||
|
||||
def test_mul_over_add_reversed(self):
|
||||
# 2*3+1 → (2*3)+1
|
||||
self.assertEqual(repr(p('2*3+1')),
|
||||
'BinOp(PLUS, BinOp(STAR, Num(2), Num(3)), Num(1))')
|
||||
|
||||
|
||||
class TestLeftAssociativity(unittest.TestCase):
|
||||
def test_subtraction_left(self):
|
||||
# 8-3-2 → (8-3)-2
|
||||
self.assertEqual(repr(p('8-3-2')),
|
||||
'BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))')
|
||||
|
||||
def test_division_left(self):
|
||||
# 8/4/2 → (8/4)/2
|
||||
self.assertEqual(repr(p('8/4/2')),
|
||||
'BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))')
|
||||
|
||||
def test_addition_left(self):
|
||||
# 1+2+3 → (1+2)+3
|
||||
self.assertEqual(repr(p('1+2+3')),
|
||||
'BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3))')
|
||||
|
||||
def test_mul_left(self):
|
||||
# 2*3*4 → (2*3)*4
|
||||
self.assertEqual(repr(p('2*3*4')),
|
||||
'BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4))')
|
||||
|
||||
|
||||
class TestParentheses(unittest.TestCase):
|
||||
def test_parens_override_precedence(self):
|
||||
# (1+2)*3 → STAR at top, PLUS inside
|
||||
self.assertEqual(repr(p('(1+2)*3')),
|
||||
'BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))')
|
||||
|
||||
def test_parens_on_right(self):
|
||||
# 3*(1+2)
|
||||
self.assertEqual(repr(p('3*(1+2)')),
|
||||
'BinOp(STAR, Num(3), BinOp(PLUS, Num(1), Num(2)))')
|
||||
|
||||
def test_nested_parens(self):
|
||||
self.assertEqual(repr(p('((2))')), 'Num(2)')
|
||||
|
||||
|
||||
class TestUnaryMinus(unittest.TestCase):
|
||||
def test_leading_unary(self):
|
||||
self.assertEqual(repr(p('-5')), 'Unary(MINUS, Num(5))')
|
||||
|
||||
def test_unary_paren(self):
|
||||
self.assertEqual(repr(p('-(1+2)')),
|
||||
'Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))')
|
||||
|
||||
def test_unary_in_mul(self):
|
||||
self.assertEqual(repr(p('3 * -2')),
|
||||
'BinOp(STAR, Num(3), Unary(MINUS, Num(2)))')
|
||||
|
||||
def test_double_unary(self):
|
||||
self.assertEqual(repr(p('--5')),
|
||||
'Unary(MINUS, Unary(MINUS, Num(5)))')
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
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_consecutive_numbers(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('1 2')
|
||||
|
||||
def test_close_open_paren(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p(')(')
|
||||
|
||||
def test_empty_string(self):
|
||||
with self.assertRaises(ParseError):
|
||||
p('')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@ -0,0 +1,16 @@
|
||||
# BACKLOG — phase: eval (shared)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Write `calc/evaluator.py` with `evaluate(node)` and `EvalError`
|
||||
- [x] Write `calc/test_evaluator.py` (unittest, 15 cases, D1–D3)
|
||||
- [x] Write `calc.py` CLI (D4)
|
||||
- [x] Verify 56 tests green (D5, no regression)
|
||||
- [x] Write STATUS-eval.md with cold-verify commands
|
||||
- [ ] Await Adversary PASS on all gates; write ## DONE
|
||||
|
||||
## Adversary findings
|
||||
|
||||
<!-- Adversary-owned — filed when breaking probes find issues -->
|
||||
|
||||
No findings yet.
|
||||
@ -0,0 +1,15 @@
|
||||
# BACKLOG — phase: lex
|
||||
|
||||
## Build backlog (Builder-owned)
|
||||
|
||||
- [x] Create machine-docs phase files
|
||||
- [x] Create calc/__init__.py
|
||||
- [x] Create calc/lexer.py with Token, LexError, tokenize()
|
||||
- [x] Create calc/test_lexer.py with unittest suite
|
||||
- [x] Run tests locally and verify all pass (22/22)
|
||||
- [x] Claim D1-D4
|
||||
- [ ] Wait for Adversary PASS on all gates
|
||||
- [ ] Write ## DONE to STATUS-lex.md
|
||||
|
||||
## Adversary findings
|
||||
(Adversary-owned — do not edit)
|
||||
@ -0,0 +1,12 @@
|
||||
# BACKLOG — phase: parse (Builder)
|
||||
|
||||
## Build backlog
|
||||
|
||||
- [x] Implement `calc/parser.py` with ParseError, Num, BinOp, Unary, parse()
|
||||
- [x] Implement `calc/test_parser.py` with 19 tests covering D1–D5
|
||||
- [x] Run full test suite (41 tests green)
|
||||
- [x] Write STATUS-parse.md with WHAT/HOW/EXPECTED/WHERE
|
||||
- [x] Claim D1–D6
|
||||
|
||||
## Adversary findings
|
||||
(Read-only — Adversary writes here)
|
||||
@ -0,0 +1,11 @@
|
||||
# DECISIONS — shared (append-only)
|
||||
|
||||
## 2026-06-15 — Token.value for non-numeric tokens
|
||||
|
||||
Decision: `Token.value` is `None` for operator/paren/EOF tokens, and `int` or `float` for NUMBER tokens.
|
||||
Rationale: Parser phases only need the numeric value; operator/paren tokens carry no useful payload beyond their kind.
|
||||
|
||||
## 2026-06-15 — Float parsing strategy
|
||||
|
||||
Decision: Use Python's built-in `float()` for converting float literals. Detect float vs int by presence of `.` in the matched string.
|
||||
Rationale: Handles edge cases like `.5` and `10.` correctly via stdlib, avoids manual parsing bugs.
|
||||
@ -0,0 +1,39 @@
|
||||
# JOURNAL — phase: eval (Builder)
|
||||
|
||||
## 2026-06-15 — implementation
|
||||
|
||||
### Design
|
||||
|
||||
AST nodes from parse phase: `Num(value)`, `BinOp(op, left, right)`, `Unary(op, operand)`.
|
||||
Evaluator is a recursive tree walk. Division uses Python's `/` (true division), then normalises
|
||||
whole-valued floats to `int` (avoids trailing `.0` in CLI output without needing special fmt logic).
|
||||
|
||||
### Runs
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 56 tests in 0.002s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
```
|
||||
$ python calc.py "2+3*4"
|
||||
14
|
||||
$ python calc.py "(2+3)*4"
|
||||
20
|
||||
$ python calc.py "7/2"
|
||||
3.5
|
||||
$ python calc.py "4/2"
|
||||
2
|
||||
$ python calc.py "1/0"
|
||||
error: division by zero (stderr, exit 1)
|
||||
$ python calc.py "1 +"
|
||||
error: unexpected token 'EOF' (stderr, exit 1)
|
||||
```
|
||||
|
||||
### Type normalisation choice
|
||||
|
||||
`int(result) if result == int(result) else result` — chose this over a `fmt` conversion in the CLI
|
||||
so `evaluate` itself is honest about the value type and tests can assert `isinstance(result, int)`.
|
||||
@ -0,0 +1,57 @@
|
||||
# JOURNAL — phase: lex
|
||||
|
||||
## 2026-06-15 — Start
|
||||
|
||||
Read phase plan. Mission: build `calc/lexer.py` with `tokenize()`, Token type, LexError, and unittest suite.
|
||||
|
||||
Plan:
|
||||
- Token: dataclass with `kind` (str) and `value` (int | float | None)
|
||||
- Kinds: NUMBER, PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN, EOF
|
||||
- LexError: custom Exception with char + position
|
||||
- tokenize(): iterate over chars, match numbers (int/float), operators, parens; skip whitespace; raise LexError on unknown char; append EOF at end
|
||||
|
||||
## 2026-06-15 — Implementation
|
||||
|
||||
Created:
|
||||
- `calc/__init__.py` (empty package marker)
|
||||
- `calc/lexer.py` — token scanner using manual char-by-char iteration
|
||||
- `calc/test_lexer.py` — 22 unittest cases
|
||||
|
||||
Float parsing design: detect float vs int by presence of `.` in scanned substring. Handle `.5` (leading dot) and `10.` (trailing dot) correctly by scanning digits before/after the dot separately.
|
||||
|
||||
## 2026-06-15 — Test run
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
----------------------------------------------------------------------
|
||||
Ran 22 tests in 0.001s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
## 2026-06-15 — Verify commands
|
||||
|
||||
```
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
|
||||
[('NUMBER', 42), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
|
||||
[('NUMBER', 3.14), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
|
||||
[('NUMBER', 0.5), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
|
||||
[('NUMBER', 10.0), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
|
||||
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]
|
||||
|
||||
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
|
||||
calc.lexer.LexError: unexpected character '@' at position 2
|
||||
```
|
||||
|
||||
All plan verify commands produce expected output. Claiming D1–D4.
|
||||
@ -0,0 +1,46 @@
|
||||
# JOURNAL — phase: parse (Builder)
|
||||
|
||||
## 2026-06-15
|
||||
|
||||
### Design decisions
|
||||
|
||||
Chose a classic recursive-descent parser with separate grammar levels for precedence:
|
||||
|
||||
- `_expr` handles `+`/`-` (low precedence, left-associative via while-loop)
|
||||
- `_term` handles `*`/`/` (high precedence, left-associative via while-loop)
|
||||
- `_unary` handles prefix `-` (right-associative via recursion)
|
||||
- `_primary` handles `NUMBER` and `(expr)`
|
||||
|
||||
Left-associativity comes naturally from the iterative loop pattern (each iteration wraps the accumulating `left` node deeper).
|
||||
|
||||
### Verification runs
|
||||
|
||||
```
|
||||
$ python -m unittest -q
|
||||
Ran 41 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
Key shape outputs verified:
|
||||
```
|
||||
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓ D1
|
||||
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓ D2
|
||||
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓ D2
|
||||
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓ D3
|
||||
-5 → Unary(MINUS, Num(5)) ✓ D4
|
||||
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓ D4
|
||||
3*-2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓ D4
|
||||
```
|
||||
|
||||
D5 errors all raise `ParseError` (not `SyntaxError`, `ValueError`, etc.):
|
||||
- `"1 +"` → ParseError: unexpected token 'EOF'
|
||||
- `"(1"` → ParseError: expected ')', got 'EOF'
|
||||
- `"1 2"` → ParseError: unexpected token 'NUMBER' after expression
|
||||
- `")("` → ParseError: unexpected token 'RPAREN'
|
||||
- `""` → ParseError: empty input
|
||||
|
||||
### Empty-string handling
|
||||
|
||||
`tokenize('')` returns `[Token('EOF')]`. The `parse()` function checks the first token;
|
||||
if it's `EOF`, raises `ParseError("empty input")` immediately, avoiding an ambiguous
|
||||
"unexpected token 'EOF'" message.
|
||||
@ -0,0 +1,52 @@
|
||||
# REVIEW — phase: eval (Adversary)
|
||||
|
||||
## Status
|
||||
Initialized @2026-06-15T01:43:40Z
|
||||
Verdicts written @2026-06-15T01:58:00Z — ALL PASS
|
||||
|
||||
## Gate verdicts
|
||||
|
||||
### eval/D1: PASS @2026-06-15T01:58:00Z
|
||||
All 5 required expressions correct from cold run:
|
||||
- `2+3*4` → `14` (precedence: * before +) ✓
|
||||
- `(2+3)*4` → `20` (parens override precedence) ✓
|
||||
- `8-3-2` → `3` (left-associativity) ✓
|
||||
- `-2+5` → `3` (unary minus leading) ✓
|
||||
- `2*-3` → `-6` (unary minus in mul) ✓
|
||||
Additional probes: `2*(3+4)-1`→13, `-(3+4)`→-7, `--5`→5 all correct.
|
||||
|
||||
### eval/D2: PASS @2026-06-15T01:58:00Z
|
||||
- `7/2` → `3.5` (true division, not floor) ✓
|
||||
- `1/0` raises `EvalError("division by zero")` — not bare `ZeroDivisionError` ✓
|
||||
- `0/0` also raises `EvalError` (edge case probed) ✓
|
||||
- Error goes to stderr, exit code 1 ✓
|
||||
- API-level check confirmed: `try/except EvalError` catches it, `ZeroDivisionError` does not ✓
|
||||
|
||||
### eval/D3: PASS @2026-06-15T01:58:00Z
|
||||
- `4/2` → `2` (int, no trailing `.0`) ✓
|
||||
- `7/2` → `3.5` (float) ✓
|
||||
- `6/3` → `2`, `9/3` → `3`, `10/5` → `2` (whole-valued normalisation consistent) ✓
|
||||
- Integer arithmetic stays `int`: `2+3` → int, `3*4` → int, `-5` → int ✓
|
||||
- Type assertions: `assertIsInstance(calc("4/2"), int)` and `assertIsInstance(calc("7/2"), float)` both pass ✓
|
||||
|
||||
### eval/D4: PASS @2026-06-15T01:58:00Z
|
||||
- `python calc.py "2+3*4"` → `14`, exit 0 ✓
|
||||
- `python calc.py "(2+3)*4"` → `20`, exit 0 ✓
|
||||
- `python calc.py "7/2"` → `3.5`, exit 0 ✓
|
||||
- `python calc.py "4/2"` → `2`, exit 0 ✓
|
||||
- `python calc.py "1/0"` → error to stderr, exit 1 ✓
|
||||
- `python calc.py "1 +"` → error to stderr, exit 1 ✓
|
||||
- `python calc.py ""` → `error: empty input`, exit 1 ✓
|
||||
- `python calc.py "abc"` → exit 1 (LexError caught) ✓
|
||||
- No tracebacks leak to stderr (grep for 'traceback' found nothing) ✓
|
||||
- Exact output format confirmed: `[14]`, `[3.5]` — no extra whitespace ✓
|
||||
|
||||
### eval/D5: PASS @2026-06-15T01:58:00Z
|
||||
- `python -m unittest -q` → `Ran 56 tests in 0.001s / OK` ✓
|
||||
- 41 lex+parse tests: still all green (no regression) ✓
|
||||
- 15 new evaluator tests: all green ✓
|
||||
- Test classes: TestArithmetic (8), TestDivision (4), TestResultType (3) ✓
|
||||
|
||||
## No findings, no VETO
|
||||
|
||||
All five gates PASS. No defects found. Builder may write ## DONE.
|
||||
@ -0,0 +1,56 @@
|
||||
# REVIEW — phase `lex` (Adversary)
|
||||
|
||||
## Status
|
||||
All 4 gates verified. Phase COMPLETE.
|
||||
|
||||
## Verdicts
|
||||
|
||||
### lex/D1: PASS @2026-06-15T01:35:56Z
|
||||
Cold-run evidence:
|
||||
```
|
||||
tokenize('42') → [('NUMBER', 42), ('EOF', None)] — int ✓
|
||||
tokenize('3.14') → [('NUMBER', 3.14), ('EOF', None)] — float ✓
|
||||
tokenize('.5') → [('NUMBER', 0.5), ('EOF', None)] — float ✓
|
||||
tokenize('10.') → [('NUMBER', 10.0), ('EOF', None)] — float ✓
|
||||
type(tokenize('42')[0].value) == int ✓
|
||||
type(tokenize('3.14')[0].value) == float ✓
|
||||
```
|
||||
Value is int for integers, float for floats. DoD fully met.
|
||||
|
||||
### lex/D2: PASS @2026-06-15T01:35:56Z
|
||||
Cold-run evidence:
|
||||
```
|
||||
tokenize('1+2*3') → ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'] ✓
|
||||
tokenize('+-*/()') → ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'] ✓
|
||||
tokenize('3.5*(1-2)') → correct kind/value pairs ✓
|
||||
```
|
||||
All 6 operator/paren kinds present and correct.
|
||||
|
||||
### lex/D3: PASS @2026-06-15T01:35:56Z
|
||||
Cold-run evidence:
|
||||
```
|
||||
tokenize(' 12 + 3 ') → NUMBER(12) PLUS NUMBER(3) EOF — spaces skipped ✓
|
||||
tokenize('1\t+\t2') → NUMBER PLUS NUMBER EOF — tabs skipped ✓
|
||||
tokenize('1 @ 2') → LexError: unexpected character '@' at position 2 ✓
|
||||
tokenize('$5') → LexError: unexpected character '$' at position 0 ✓
|
||||
tokenize('x') → LexError: unexpected character 'x' at position 0 ✓
|
||||
```
|
||||
Offending char and position both present in message. DoD fully met.
|
||||
|
||||
### lex/D4: PASS @2026-06-15T01:35:56Z
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 22 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
Zero failures, zero errors. All 22 tests cover D1–D3 including required cases:
|
||||
`" 12 + 3 "`, `"3.5*(1-2)"`, and `"1 @ 2"` raising LexError.
|
||||
|
||||
## Informational finding (non-blocking)
|
||||
|
||||
**F1 — lone `.` raises `ValueError` not `LexError`:** `tokenize('.')` crashes with
|
||||
`ValueError: could not convert string to float: '.'` instead of a `LexError`. The
|
||||
DoD does not require this case to be handled, so this is NOT a blocker for any gate.
|
||||
Noted for future phases that may extend the lexer.
|
||||
|
||||
No VETO. All DoD items independently verified.
|
||||
@ -0,0 +1,106 @@
|
||||
# REVIEW — phase: parse (Adversary)
|
||||
|
||||
## Verdict: ALL GATES PASS @2026-06-15T01:42:00Z
|
||||
|
||||
Cold-verified from commit `23d0ae9` (work-adv clone, fresh shell, no cached state).
|
||||
|
||||
---
|
||||
|
||||
### D6 (tests green): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
python -m unittest -q
|
||||
Ran 41 tests in 0.001s
|
||||
OK
|
||||
```
|
||||
|
||||
41 tests (22 lexer + 19 parser), 0 failures. Tests assert on `repr()`/tree structure, not evaluation — satisfies the plan requirement.
|
||||
|
||||
---
|
||||
|
||||
### D1 (precedence): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓
|
||||
6-4/2 → BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2))) ✓
|
||||
```
|
||||
|
||||
Re-derived independently: grammar `expr: term((+|-)term)*` / `term: unary((*|/)unary)*` guarantees `*`/`/` subtrees sit inside `+`/`-` nodes. Observed matches.
|
||||
|
||||
Extra adversarial probe:
|
||||
```
|
||||
1+2*3+4 → BinOp(PLUS, BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))), Num(4)) ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### D2 (left associativity): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓
|
||||
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓
|
||||
```
|
||||
|
||||
Extra probes:
|
||||
```
|
||||
1+2+3 → BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
|
||||
2*3*4 → BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4)) ✓
|
||||
4*3/2 → BinOp(SLASH, BinOp(STAR, Num(4), Num(3)), Num(2)) ✓
|
||||
```
|
||||
|
||||
Left-associativity correctly implemented via iterative while-loop (not recursion).
|
||||
|
||||
---
|
||||
|
||||
### D3 (parentheses): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
|
||||
((2)) → Num(2) ✓
|
||||
(-5) → Unary(MINUS, Num(5)) ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### D4 (unary minus): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
```
|
||||
-5 → Unary(MINUS, Num(5)) ✓
|
||||
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓
|
||||
3 * -2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓
|
||||
--5 → Unary(MINUS, Unary(MINUS, Num(5))) ✓ (double unary)
|
||||
2+-3 → BinOp(PLUS, Num(2), Unary(MINUS, Num(3))) ✓ (unary after binary)
|
||||
```
|
||||
|
||||
Right-recursive `_unary` correctly handles chained negation.
|
||||
|
||||
---
|
||||
|
||||
### D5 (errors raise ParseError): PASS @2026-06-15T01:42:00Z
|
||||
|
||||
All five required cases verified — each raises `ParseError`, not `ValueError`, `IndexError`, or any other exception:
|
||||
|
||||
```
|
||||
'1 +' → ParseError: unexpected token 'EOF' ✓
|
||||
'(1' → ParseError: expected ')', got 'EOF' ✓
|
||||
'1 2' → ParseError: unexpected token 'NUMBER' after expression ✓
|
||||
')(' → ParseError: unexpected token 'RPAREN' ✓
|
||||
'' → ParseError: empty input ✓
|
||||
```
|
||||
|
||||
Extra adversarial error probes (all raise ParseError):
|
||||
```
|
||||
'*5' → ParseError: unexpected token 'STAR' ✓
|
||||
'5*' → ParseError: unexpected token 'EOF' ✓
|
||||
'()' → ParseError: unexpected token 'RPAREN' ✓
|
||||
'(+5)' → ParseError: unexpected token 'PLUS' ✓
|
||||
'1++2' → ParseError: unexpected token 'PLUS' ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
All six DoD gates D1–D6 independently verified cold. No defects found. The grammar in STATUS exactly matches the implementation (`_expr`/`_term`/`_unary`/`_primary`). Node shape is stable and documented in the `parse()` docstring. No veto.
|
||||
|
||||
Builder may write `## DONE` to STATUS-parse.md.
|
||||
@ -0,0 +1,44 @@
|
||||
# STATUS — phase: eval (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
Gate: D1 D2 D3 D4 D5 — Adversary PASS @2026-06-15T01:58:00Z. Phase complete.
|
||||
|
||||
## What was built
|
||||
|
||||
- `calc/evaluator.py` — `evaluate(node) -> int | float`; `EvalError` for bad nodes / division by zero
|
||||
- `calc/test_evaluator.py` — 15 unittest cases covering D1–D3
|
||||
- `calc.py` (repo root) — CLI: `python calc.py "<expr>"`
|
||||
|
||||
## Commit SHA
|
||||
|
||||
See `git log --oneline -1` on origin/main after this push.
|
||||
|
||||
## Verify commands (cold — run from repo root)
|
||||
|
||||
```bash
|
||||
python -m unittest -q
|
||||
# Expected: Ran 56 tests in <N>s / OK
|
||||
|
||||
python calc.py "2+3*4" # Expected: 14
|
||||
python calc.py "(2+3)*4" # Expected: 20
|
||||
python calc.py "7/2" # Expected: 3.5
|
||||
python calc.py "4/2" # Expected: 2
|
||||
python calc.py "1/0" # Expected: prints error to stderr, exits 1
|
||||
python calc.py "1 +" # Expected: prints error to stderr, exits 1
|
||||
```
|
||||
|
||||
## Gate mapping
|
||||
|
||||
| Gate | DoD | Verify |
|
||||
|------|-----|--------|
|
||||
| D1 | arithmetic: `+ - * /`, precedence, parens, unary minus | `python -m unittest -q` (TestArithmetic) |
|
||||
| D2 | true division; `EvalError` on div-by-zero (not bare `ZeroDivisionError`) | `python -m unittest -q` (TestDivision) |
|
||||
| D3 | whole-valued → int (no `.0`); non-whole → float | `python -m unittest -q` (TestResultType) |
|
||||
| D4 | `python calc.py "2+3*4"` → 14 exit 0; error → stderr + exit 1 | manual CLI commands above |
|
||||
| D5 | 56 tests total (lex+parse+eval), 0 failures | `python -m unittest -q` |
|
||||
|
||||
## Result type rule (D3)
|
||||
|
||||
Division result is normalised: `result = left / right; return int(result) if result == int(result) else result`.
|
||||
The CLI's `_fmt` calls `str(result)`, so `int` prints as `"2"` and `float` prints as `"3.5"`.
|
||||
@ -0,0 +1,70 @@
|
||||
# STATUS — phase: lex (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
All gates verified PASS by Adversary @2026-06-15T01:35:56Z.
|
||||
|
||||
## Gate: D1, D2, D3, D4 — PASS
|
||||
|
||||
### WHAT is claimed
|
||||
All four gates D1–D4 are implemented and tested.
|
||||
|
||||
### HOW to verify (run from a fresh clone)
|
||||
|
||||
```bash
|
||||
cd <repo>
|
||||
python -m unittest -q
|
||||
```
|
||||
Expected: 22 tests, 0 failures, 0 errors.
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 42), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 3.14), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 0.5), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 10.0), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
|
||||
```
|
||||
Expected: `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
|
||||
```
|
||||
Expected: `[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
|
||||
```
|
||||
Expected: raises `calc.lexer.LexError: unexpected character '@' at position 2`
|
||||
|
||||
### WHERE (commit sha)
|
||||
`7022854acf94b35ebc79cb48315e91b82c7cc4ec`
|
||||
|
||||
### Files
|
||||
- `calc/__init__.py` — empty package marker
|
||||
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
|
||||
- `calc/test_lexer.py` — 22 unittest cases covering D1–D3
|
||||
|
||||
### Gate checklist
|
||||
- D1 (numbers): PASS @2026-06-15T01:35:56Z
|
||||
- D2 (operators & parens): PASS @2026-06-15T01:35:56Z
|
||||
- D3 (whitespace & errors): PASS @2026-06-15T01:35:56Z
|
||||
- D4 (tests green): PASS @2026-06-15T01:35:56Z
|
||||
|
||||
## Informational (non-blocking)
|
||||
- F1: lone `.` raises `ValueError` not `LexError` — not required by DoD, noted for future phases
|
||||
@ -0,0 +1,115 @@
|
||||
# STATUS — phase: parse (Builder)
|
||||
|
||||
## DONE
|
||||
|
||||
## Gate: D1–D6 — ALL PASS (Adversary-verified @2026-06-15T01:42:00Z)
|
||||
|
||||
### WHAT is claimed
|
||||
All six gates D1–D6 are implemented and tested.
|
||||
|
||||
- **D1 (precedence):** `*`/`/` bind tighter than `+`/`-`
|
||||
- **D2 (left associativity):** Same-precedence ops associate left
|
||||
- **D3 (parentheses):** Parens override precedence
|
||||
- **D4 (unary minus):** Leading and nested unary minus
|
||||
- **D5 (errors):** Five malformed inputs each raise `ParseError`
|
||||
- **D6 (tests green):** 19 parser tests + 22 lexer tests = 41 total, 0 failures
|
||||
|
||||
### HOW to verify (run from a fresh clone)
|
||||
|
||||
**D6 — all tests green:**
|
||||
```bash
|
||||
python -m unittest -q
|
||||
```
|
||||
Expected output: `Ran 41 tests in 0.00xs` / `OK`
|
||||
|
||||
**D1 — precedence (structural check):**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
|
||||
```
|
||||
Expected: `BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('6-4/2')))"
|
||||
```
|
||||
Expected: `BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))`
|
||||
|
||||
**D2 — left associativity:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
|
||||
```
|
||||
Expected: `BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
|
||||
```
|
||||
Expected: `BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))`
|
||||
|
||||
**D3 — parentheses:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
|
||||
```
|
||||
Expected: `BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))`
|
||||
|
||||
**D4 — unary minus:**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
|
||||
```
|
||||
Expected: `Unary(MINUS, Num(5))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
|
||||
```
|
||||
Expected: `Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))`
|
||||
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
|
||||
```
|
||||
Expected: `BinOp(STAR, Num(3), Unary(MINUS, Num(2)))`
|
||||
|
||||
**D5 — errors (each must raise `ParseError`, not a different exception):**
|
||||
```bash
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 +'))"
|
||||
# raises: ParseError: unexpected token 'EOF'
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('(1'))"
|
||||
# raises: ParseError: expected ')', got 'EOF'
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 2'))"
|
||||
# raises: ParseError: unexpected token 'NUMBER' after expression
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(')('))"
|
||||
# raises: ParseError: unexpected token 'RPAREN'
|
||||
|
||||
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(''))"
|
||||
# raises: ParseError: empty input
|
||||
```
|
||||
|
||||
### EXPECTED AST shapes (re-derivable from grammar)
|
||||
|
||||
AST node types:
|
||||
- `Num(value)` — numeric literal; value is int or float
|
||||
- `BinOp(op, left, right)` — op in `{'PLUS','MINUS','STAR','SLASH'}`
|
||||
- `Unary(op, operand)` — op == `'MINUS'`
|
||||
|
||||
Grammar used (drives precedence + associativity):
|
||||
```
|
||||
expr : term (('+' | '-') term)* ← left-assoc, low precedence
|
||||
term : unary (('*' | '/') unary)* ← left-assoc, high precedence
|
||||
unary : '-' unary | primary ← right-assoc chain, prefix only
|
||||
primary : NUMBER | '(' expr ')'
|
||||
```
|
||||
|
||||
### WHERE (commit sha)
|
||||
`23d0ae9` (claim(D1,D2,D3,D4,D5,D6): parser complete, all 41 tests green)
|
||||
|
||||
### Files
|
||||
- `calc/parser.py` — ParseError, Num, BinOp, Unary, _Parser, parse()
|
||||
- `calc/test_parser.py` — 19 unittest cases covering D1–D5
|
||||
|
||||
### Gate checklist
|
||||
- D1 (precedence): CLAIMED
|
||||
- D2 (left associativity): CLAIMED
|
||||
- D3 (parentheses): CLAIMED
|
||||
- D4 (unary minus): CLAIMED
|
||||
- D5 (errors): CLAIMED
|
||||
- D6 (tests green): CLAIMED
|
||||
Reference in New Issue
Block a user