"""Evaluator for the arithmetic AST produced by calc.parser. evaluate(node) -> int | float Result-type rule: if the mathematical result is whole-valued (no fractional part), it is returned as int; otherwise as float. This ensures '4/2' → 2 and '7/2' → 3.5 without a trailing .0 on whole results. """ from calc.parser import BinOp, Num, Unary class EvalError(Exception): pass def evaluate(node): """Walk an AST node and return int or float. Raises EvalError on semantic errors (e.g. division by zero). """ if isinstance(node, Num): return node.value if isinstance(node, Unary): operand = evaluate(node.operand) if node.op == '-': return _normalize(-operand) raise EvalError(f"Unknown unary op: {node.op!r}") if isinstance(node, BinOp): left = evaluate(node.left) right = evaluate(node.right) if node.op == '+': return _normalize(left + right) if node.op == '-': return _normalize(left - right) if node.op == '*': return _normalize(left * right) if node.op == '/': if right == 0: raise EvalError("Division by zero") return _normalize(left / right) raise EvalError(f"Unknown binary op: {node.op!r}") raise EvalError(f"Unknown AST node: {node!r}") def _normalize(v): """Return int if v is a whole-valued float, else return v unchanged.""" if isinstance(v, float) and v == int(v): return int(v) return v