142 lines
3.7 KiB
Python
142 lines
3.7 KiB
Python
"""Recursive-descent parser for arithmetic expressions.
|
||
|
||
Grammar (precedence low → high):
|
||
expr = term ( ('+' | '-') term )*
|
||
term = unary ( ('*' | '/') unary )*
|
||
unary = '-' unary | primary
|
||
primary = NUMBER | '(' expr ')'
|
||
|
||
AST nodes (stable shape for the evaluator):
|
||
Num(value) – numeric literal; value is int or float
|
||
BinOp(op, left, right) – op is one of '+', '-', '*', '/'
|
||
Unary(op, operand) – op is '-'
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
from typing import List
|
||
from .lexer import Token
|
||
|
||
|
||
class ParseError(Exception):
|
||
pass
|
||
|
||
|
||
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
|
||
)
|
||
|
||
|
||
class _Parser:
|
||
def __init__(self, tokens: List[Token]):
|
||
self._tokens = tokens
|
||
self._pos = 0
|
||
|
||
def _peek(self) -> Token:
|
||
return self._tokens[self._pos]
|
||
|
||
def _consume(self) -> Token:
|
||
tok = self._tokens[self._pos]
|
||
self._pos += 1
|
||
return tok
|
||
|
||
def _expect(self, kind: str) -> Token:
|
||
tok = self._peek()
|
||
if tok.kind != kind:
|
||
raise ParseError(f"expected {kind}, got {tok.kind!r} ({tok.value!r})")
|
||
return self._consume()
|
||
|
||
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 {tok.kind!r} ({tok.value!r})")
|
||
return node
|
||
|
||
def _expr(self):
|
||
node = self._term()
|
||
while self._peek().kind in ("PLUS", "MINUS"):
|
||
op = self._consume().value
|
||
right = self._term()
|
||
node = BinOp(op, node, right)
|
||
return node
|
||
|
||
def _term(self):
|
||
node = self._unary()
|
||
while self._peek().kind in ("STAR", "SLASH"):
|
||
op = self._consume().value
|
||
right = self._unary()
|
||
node = BinOp(op, node, right)
|
||
return node
|
||
|
||
def _unary(self):
|
||
if self._peek().kind == "MINUS":
|
||
op = self._consume().value
|
||
operand = self._unary()
|
||
return Unary(op, operand)
|
||
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()
|
||
self._expect("RPAREN")
|
||
return node
|
||
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
|
||
|
||
|
||
def parse(tokens: List[Token]):
|
||
"""Parse a token list produced by calc.lexer.tokenize and return an AST root node."""
|
||
return _Parser(tokens).parse()
|