"""Evaluator for the arithmetic AST produced by calc.parser. Result-type rule: if a computation yields a float that is whole-valued (e.g. 4/2 == 2.0), it is coerced to int before returning. Non-whole floats (e.g. 7/2 == 3.5) are returned as float. """ from .parser import BinOp, Num, Unary class EvalError(Exception): """Raised on a runtime evaluation error (e.g. division by zero).""" def _coerce(value): if isinstance(value, float) and value == int(value): return int(value) return value def evaluate(node): """Walk an AST node and return int | float. Raises: EvalError: on division by zero. """ if isinstance(node, Num): return _coerce(node.value) if isinstance(node, Unary): return _coerce(-evaluate(node.operand)) if isinstance(node, BinOp): left = evaluate(node.left) right = evaluate(node.right) op = node.op if op == '+': return _coerce(left + right) if op == '-': return _coerce(left - right) if op == '*': return _coerce(left * right) if op == '/': if right == 0: raise EvalError("division by zero") return _coerce(left / right) raise EvalError(f"unknown node type: {type(node).__name__}")