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