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