Files
agent-orchestrator-benchmark/calculators/builder-adversary-deferred/run-04/calc/parser.py

150 lines
4.1 KiB
Python

"""
Recursive-descent parser for the calc expression grammar.
Grammar:
expr = term ( ('+' | '-') term )*
term = unary ( ('*' | '/') unary )*
unary = '-' unary | primary
primary = NUMBER | '(' expr ')'
AST node shapes (stable contract for the evaluator):
Num(value) — numeric literal; .value is int or float
BinOp(op, left, right) — binary operation; .op is '+', '-', '*', or '/'
Unary(op, operand) — unary prefix; .op is '-'
"""
class ParseError(Exception):
pass
# ---------------------------------------------------------------------------
# AST nodes
# ---------------------------------------------------------------------------
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: str, 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: str, 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)
# ---------------------------------------------------------------------------
# Parser
# ---------------------------------------------------------------------------
class _Parser:
def __init__(self, tokens):
self._tokens = tokens
self._pos = 0
def _peek(self):
return self._tokens[self._pos]
def _advance(self):
tok = self._tokens[self._pos]
self._pos += 1
return tok
def _expect(self, kind):
tok = self._peek()
if tok.kind != kind:
raise ParseError(
f"Expected {kind}, got {tok.kind!r} ({tok.value!r})"
)
return self._advance()
# expr = term ( ('+' | '-') term )*
def _expr(self):
node = self._term()
while self._peek().kind in ('PLUS', 'MINUS'):
op = self._advance().value
node = BinOp(op, node, self._term())
return node
# term = unary ( ('*' | '/') unary )*
def _term(self):
node = self._unary()
while self._peek().kind in ('STAR', 'SLASH'):
op = self._advance().value
node = BinOp(op, node, self._unary())
return node
# unary = '-' unary | primary
def _unary(self):
if self._peek().kind == 'MINUS':
self._advance()
return Unary('-', self._unary())
return self._primary()
# primary = NUMBER | '(' expr ')'
def _primary(self):
tok = self._peek()
if tok.kind == 'NUMBER':
self._advance()
return Num(tok.value)
if tok.kind == 'LPAREN':
self._advance()
node = self._expr()
self._expect('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(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 after expression: {tok.kind!r} ({tok.value!r})"
)
return node
def parse(tokens) -> object:
"""Parse a token list produced by `calc.lexer.tokenize` into an AST."""
return _Parser(tokens).parse()