"""Tiny arithmetic expression evaluator with proper precedence. Supports: + - * / // % **, unary +/-, parentheses, variables (dict), functions (sin, cos, sqrt, abs, max, min), and error reporting with position. No eval/exec — pure recursive descent. """ import math import re class EvalError(Exception): def __init__(self, msg, pos=None): super().__init__(msg) self.pos = pos _TOKEN_RE = re.compile(r""" \s*(?: (?P\d+(?:\.\d+)?(?:[eE][+-]?\d+)?) | (?P[A-Za-z_][A-Za-z0-9_]*) | (?P\*\*|//|[+\-*/%(),]) ) """, re.VERBOSE) def tokenize(s): toks = [] i = 0 n = len(s) while i < n: m = _TOKEN_RE.match(s, i) if not m: raise EvalError(f"unexpected char {s[i:i+5]!r}", i) i = m.end() if m.group('num') is not None: toks.append(('num', float(m.group('num')), m.start())) elif m.group('ident') is not None: toks.append(('ident', m.group('ident'), m.start())) else: toks.append(('op', m.group('op'), m.start())) toks.append(('eof', None, n)) return toks FUNCS = { 'sin': lambda a: math.sin(a), 'cos': lambda a: math.cos(a), 'sqrt': lambda a: math.sqrt(a), 'abs': lambda a: abs(a), 'max': lambda *a: max(a), 'min': lambda *a: min(a), 'round': lambda a: round(a), 'pow': lambda a, b: a ** b, } class Parser: def __init__(self, toks, env=None): self.toks = toks self.pos = 0 self.env = env or {} def peek(self): return self.toks[self.pos] def advance(self): t = self.toks[self.pos] self.pos += 1 return t def expect(self, kind, val=None): t = self.peek() if t[0] != kind or (val is not None and t[1] != val): raise EvalError(f"expected {kind} {val!r} got {t[0]} {t[1]!r}", t[2]) return self.advance() # expr := term (('+'|'-') term)* def parse_expr(self): v = self.parse_term() while self.peek()[0] == 'op' and self.peek()[1] in ('+', '-'): op = self.advance()[1] r = self.parse_term() v = v + r if op == '+' else v - r return v def parse_term(self): v = self.parse_factor() while self.peek()[0] == 'op' and self.peek()[1] in ('*', '/', '//', '%'): op = self.advance()[1] r = self.parse_factor() if op == '*': v = v * r elif op == '/': if r == 0: raise EvalError("division by zero", self.peek()[2]) v = v / r elif op == '//': if r == 0: raise EvalError("division by zero", self.peek()[2]) v = v // r elif op == '%': if r == 0: raise EvalError("modulo by zero", self.peek()[2]) v = v % r return v def parse_factor(self): # right-assoc ** base = self.parse_unary() if self.peek()[0] == 'op' and self.peek()[1] == '**': self.advance() exp = self.parse_factor() return base ** exp return base def parse_unary(self): if self.peek()[0] == 'op' and self.peek()[1] in ('+', '-'): op = self.advance()[1] v = self.parse_unary() return -v if op == '-' else v return self.parse_primary() def parse_primary(self): t = self.peek() if t[0] == 'num': self.advance() return t[1] if t[0] == 'op' and t[1] == '(': self.advance() v = self.parse_expr() self.expect('op', ')') return v if t[0] == 'ident': name = t[1] self.advance() if self.peek()[0] == 'op' and self.peek()[1] == '(': self.advance() args = [] if not (self.peek()[0] == 'op' and self.peek()[1] == ')'): args.append(self.parse_expr()) while self.peek()[0] == 'op' and self.peek()[1] == ',': self.advance() args.append(self.parse_expr()) self.expect('op', ')') if name not in FUNCS: raise EvalError(f"unknown function {name}", t[2]) try: return FUNCS[name](*args) except Exception as e: raise EvalError(f"function {name}: {e}", t[2]) # variable if name not in self.env: raise EvalError(f"unknown variable {name}", t[2]) return self.env[name] raise EvalError(f"unexpected token {t[0]} {t[1]!r}", t[2]) def evaluate(s, env=None): toks = tokenize(s) p = Parser(toks, env) v = p.parse_expr() if p.peek()[0] != 'eof': raise EvalError("trailing tokens", p.peek()[2]) return v if __name__ == '__main__': import sys env = {'x': 3, 'pi': math.pi} expr = ' '.join(sys.argv[1:]) or '2 + 3*4' print(evaluate(expr, env))