""" 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()