Files

234 lines
7.4 KiB
Python

"""AST-представление выражений и рекурсивный оценщик.
Замена «вычисления за время парсинга» на отдельный этап:
токенизатор -> AST (Nodes) -> eval(ast, env).
Это даёт: повторный вывод выражений (pprint), перебор/переменные в env,
отделение парсера от вычисления и чистые точки ошибок.
Иерарфия приоритета (старшие узлы ниже по дереву, но выше в вычислении):
expr := term (('+' | '-') term)*
term := factor (('*' | '/' | '//' | '%') factor)*
factor := ('+' | '-') factor | power
power := postfix ('**') factor # правосторонний
postfix := atom ('%')* # постфиксные проценты
atom := NUMBER | ID | ID '(' args ')' | '(' expr ')'
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Callable
class Node:
"""Базовый узел AST (repr = человекочитаемый текст выражения)."""
@dataclass
class NumNode(Node):
value: float
def __repr__(self) -> str:
if math.isfinite(self.value) and self.value == int(self.value):
return f"{self.value:g}"
return f"{self.value!r}"
@dataclass
class UnaryNode(Node):
operand: Node
negate: bool
def __repr__(self) -> str:
sign = "-" if self.negate else "+"
return f"{sign}{self.operand!r}"
@dataclass
class BinaryNode(Node):
op: str
left: Node
right: Node
def __repr__(self) -> str:
return f"({self.left!r} {self.op} {self.right!r})"
@dataclass
class PowNode(Node):
base: Node
exponent: Node
def __repr__(self) -> str:
return f"{self.base!r} ** {self.exponent!r}"
@dataclass
class PostfixPercentNode(Node):
operand: Node
def __repr__(self) -> str:
return f"{self.operand!r}%"
@dataclass
class CallNode(Node):
name: str
args: list[Node]
def __repr__(self) -> str:
return f"{self.name}({', '.join(map(repr, self.args))})"
@dataclass
class IdNode(Node):
name: str
def __repr__(self) -> str:
return self.name
@dataclass
class ConstNode(Node):
name: str
def __repr__(self) -> str:
return f"({self.name})"
def round2(x: float, ndigits: float | None = None) -> float:
# наш калькулятор всё считает во float; Python round() хочет int для ndigits
return float(round(x, int(ndigits)) if ndigits is not None else round(x))
_CONSTANTS: dict[str, float] = {
"pi": math.pi,
"e": math.e,
"tau": math.tau,
"inf": math.inf,
}
_FUNCTIONS: dict[str, tuple[Callable[..., float], tuple[int, ...]]] = {
"sqrt": (math.sqrt, (1,)),
"abs": (abs, (1,)),
"exp": (math.exp, (1,)),
"ln": (math.log, (1,)),
"log": (math.log, (1, 2)),
"log2": (math.log2, (1,)),
"log10": (math.log10, (1,)),
"round": (round2, (1, 2)),
"floor": (math.floor, (1,)),
"ceil": (math.ceil, (1,)),
"sin": (math.sin, (1,)),
"cos": (math.cos, (1,)),
"tan": (math.tan, (1,)),
"asin": (math.asin, (1,)),
"acos": (math.acos, (1,)),
"atan": (math.atan, (1,)),
}
def format_result(result: float) -> str:
"""Представить результат с 12 значащими цифрами (убирает артефакты float).
inf/nan отдаются без округления.
"""
if math.isnan(result) or math.isinf(result):
return f"{result:g}"
return f"{result:.12g}"
class ASTError(Exception):
"""Базовое исключение AST-оценщика."""
class SyntaxError_(ASTError):
"""Невозможно распарсить выражение."""
class DivisionByZeroError(ASTError):
"""Деление на ноль."""
class Evaluator:
"""Вычисляет узел AST при заданном env (переменные + ans)."""
def eval(self, node: Node, env: dict[str, float]) -> float:
node_type = node.__class__.__name__
if isinstance(node, NumNode):
return node.value
if isinstance(node, UnaryNode):
value = self.eval(node.operand, env)
return -value if node.negate else value
if isinstance(node, BinaryNode):
left = self.eval(node.left, env)
right = self.eval(node.right, env)
return self._binary(node.op, left, right)
if isinstance(node, PowNode):
base = self.eval(node.base, env)
exponent = self.eval(node.exponent, env)
return self._pow(base, exponent)
if isinstance(node, PostfixPercentNode):
return self.eval(node.operand, env) / 100.0
if isinstance(node, CallNode):
return self._call(node, env)
if isinstance(node, IdNode):
if node.name in env:
return env[node.name]
# не задана переменной — резолвим как константу, если знаем её
if node.name in _CONSTANTS:
return _CONSTANTS[node.name]
raise SyntaxError_(f"Неизвестный идентификатор: {node.name!r}")
if isinstance(node, ConstNode):
return _CONSTANTS[node.name]
raise ASTError(f"Неизвестный узел: {node_type}")
def _binary(self, op: str, left: float, right: float) -> float:
if op == "+":
return left + right
if op == "-":
return left - right
if op == "*":
return left * right
if op == "/":
if right == 0:
raise DivisionByZeroError("Деление на ноль")
return left / right
if op == "//":
if right == 0:
raise DivisionByZeroError("Целочисленное деление на ноль")
return left // right
if op == "%":
if right == 0:
raise DivisionByZeroError("Остаток от деления на ноль")
return left % right
raise ASTError(f"Неизвестный оператор: {op}")
@staticmethod
def _pow(base: float, exponent: float) -> float:
try:
value = base ** exponent
except (OverflowError, ValueError) as exc:
raise ASTError(f"Ошибка возведения в степень: {exc}") from exc
if isinstance(value, complex):
raise ASTError("Комплексный результат не поддерживается")
return value
def _call(self, node: CallNode, env: dict[str, float]) -> float:
if node.name not in _FUNCTIONS:
raise SyntaxError_(f"Неизвестная функция: {node.name!r}")
func, arities = _FUNCTIONS[node.name]
if len(node.args) not in arities:
expected = "/".join(str(a) for a in arities)
raise SyntaxError_(
f"'{node.name}': ожидалось аргументов {expected},"
f" получено {len(node.args)}"
)
args = [self.eval(a, env) for a in node.args]
try:
return float(func(*args))
except (ValueError, OverflowError) as exc:
raise ASTError(f"Ошибка в '{node.name}': {exc}") from exc