234 lines
7.4 KiB
Python
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
|