"""Safe arithmetic expression evaluator. A recursive-descent parser that evaluates arithmetic expressions without ever calling eval/exec. Supports +, -, *, /, //, %, **, parentheses, unary minus/plus, constants (pi, e), and a whitelist of math functions. Public API: evaluate(expr: str) -> float | int Raises ValueError on malformed input or disallowed names. Designed for untrusted input: no attribute access, no calls except a fixed function whitelist, no name lookups beyond constants. """ import math import re # Token kinds _NUMBER = "NUMBER" _IDENT = "IDENT" _OP = "OP" _LPAREN = "LPAREN" _RPAREN = "RPAREN" _COMMA = "COMMA" _EOF = "EOF" # Allowed functions (name -> callable). All take floats and return floats # except min/max/round which are variadic. _FUNCTIONS = { "abs": abs, "round": round, "min": min, "max": max, "sin": math.sin, "cos": math.cos, "tan": math.tan, "sqrt": math.sqrt, "log": math.log, "log10": math.log10, "exp": math.exp, "floor": math.floor, "ceil": math.ceil, } _CONSTANTS = { "pi": math.pi, "e": math.e, } # Multi-char operators must be tried before single-char ones. _OPERATORS = ["**", "//", "+", "-", "*", "/", "%", "(", ")"] _TOKEN_RE = re.compile( r"\s*(?:(?P\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)" r"|(?P[A-Za-z_][A-Za-z0-9_]*)" r"|(?P\*\*|//|[+\-*/%(),]))" ) class _Tokenizer: def __init__(self, text): self.text = text self.pos = 0 self.tokens = [] self._scan() def _scan(self): i = 0 n = len(self.text) while i < n: m = _TOKEN_RE.match(self.text, i) if not m: # skip whitespace if self.text[i].isspace(): i += 1 continue raise ValueError(f"Unexpected character {self.text[i]!r} at position {i}") i = m.end() if m.group("num") is not None: self.tokens.append((_NUMBER, m.group("num"))) elif m.group("ident") is not None: self.tokens.append((_IDENT, m.group("ident"))) elif m.group("op") is not None: op = m.group("op") if op == "(": self.tokens.append((_LPAREN, op)) elif op == ")": self.tokens.append((_RPAREN, op)) elif op == ",": self.tokens.append((_COMMA, op)) else: self.tokens.append((_OP, op)) self.tokens.append((_EOF, None)) def __iter__(self): return iter(self.tokens) class _Parser: def __init__(self, tokens): self.tokens = tokens self.i = 0 def peek(self): return self.tokens[self.i] def advance(self): t = self.tokens[self.i] self.i += 1 return t def expect(self, kind, value=None): t = self.peek() if t[0] != kind or (value is not None and t[1] != value): raise ValueError(f"Expected {kind} {value!r} but got {t!r}") return self.advance() # Grammar: # expr := term (('+'|'-') term)* # term := power (('*'|'/'|'//'|'%') power)* # power := unary ('**' power)? (right-associative) # unary := ('+'|'-') unary | primary # primary:= NUMBER | IDENT ( '(' args ')' )? | '(' expr ')' # args := expr (',' expr)* def parse(self): result = self.expr() if self.peek()[0] != _EOF: raise ValueError(f"Unexpected trailing token {self.peek()!r}") return result def expr(self): value = self.term() while self.peek()[0] == _OP and self.peek()[1] in ("+", "-"): op = self.advance()[1] rhs = self.term() if op == "+": value = value + rhs else: value = value - rhs return value def term(self): value = self.unary() while self.peek()[0] == _OP and self.peek()[1] in ("*", "/", "//", "%"): op = self.advance()[1] rhs = self.unary() if op == "*": value = value * rhs elif op == "/": if rhs == 0: raise ValueError("Division by zero") value = value / rhs elif op == "//": if rhs == 0: raise ValueError("Division by zero") value = value // rhs else: if rhs == 0: raise ValueError("Modulo by zero") value = value % rhs return value def unary(self): if self.peek()[0] == _OP and self.peek()[1] in ("+", "-"): op = self.advance()[1] val = self.power() # unary lower precedence than ** (python semantics) return -val if op == "-" else val return self.power() def power(self): base = self.primary() if self.peek()[0] == _OP and self.peek()[1] == "**": self.advance() exp = self.unary() # right-assoc, exponent may be signed return base ** exp return base def primary(self): t = self.peek() if t[0] == _NUMBER: self.advance() s = t[1] return float(s) if ("." in s or "e" in s.lower()) else int(s) if t[0] == _IDENT: name = t[1] self.advance() if self.peek()[0] == _LPAREN: if name not in _FUNCTIONS: raise ValueError(f"Unknown function {name!r}") self.advance() # '(' args = [self.expr()] while self.peek()[0] == _COMMA: self.advance() args.append(self.expr()) self.expect(_RPAREN) try: return _FUNCTIONS[name](*args) except Exception as exc: raise ValueError(f"Error in {name}: {exc}") if name in _CONSTANTS: return _CONSTANTS[name] raise ValueError(f"Unknown identifier {name!r}") if t[0] == _LPAREN: self.advance() val = self.expr() self.expect(_RPAREN) return val raise ValueError(f"Unexpected token {t!r}") def evaluate(expr): if not isinstance(expr, str): raise ValueError("Expression must be a string") if not expr.strip(): raise ValueError("Empty expression") tokens = _Tokenizer(expr).tokens parser = _Parser(tokens) return parser.parse() if __name__ == "__main__": import sys print(evaluate(" ".join(sys.argv[1:])))