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!r})" @dataclass class BinOp: op: str left: object right: object def __repr__(self): return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})" @dataclass class Unary: op: str operand: object def __repr__(self): return f"Unary({self.op!r}, {self.operand!r})" def parse(tokens: list): """Parse a token list into an AST. Grammar: expr → term (('+' | '-') term)* term → unary (('*' | '/') unary)* unary → '-' unary | primary primary → NUMBER | '(' expr ')' Returns the root Node. Raises ParseError on malformed input. """ pos = 0 def peek(): return tokens[pos] def consume(kind=None): nonlocal pos tok = tokens[pos] if kind and tok.kind != kind: raise ParseError(f"expected {kind}, got {tok.kind!r}") pos += 1 return tok def expr(): left = term() while peek().kind in ('PLUS', 'MINUS'): op = consume().kind right = term() left = BinOp(op, left, right) return left def term(): left = unary() while peek().kind in ('STAR', 'SLASH'): op = consume().kind right = unary() left = BinOp(op, left, right) return left def unary(): if peek().kind == 'MINUS': op = consume().kind operand = unary() return Unary(op, operand) return primary() def primary(): tok = peek() if tok.kind == 'NUMBER': consume() return Num(tok.value) if tok.kind == 'LPAREN': consume() node = expr() if peek().kind != 'RPAREN': raise ParseError(f"expected ')', got {peek().kind!r}") consume() return node raise ParseError(f"unexpected token {tok.kind!r}") node = expr() if peek().kind != 'EOF': raise ParseError(f"unexpected token {peek().kind!r} after expression") return node