Files
troupe-standup/wizard-work/beam/expr_eval.py
T

166 lines
5.1 KiB
Python

"""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<num>\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)
| (?P<ident>[A-Za-z_][A-Za-z0-9_]*)
| (?P<op>\*\*|//|[+\-*/%(),])
)
""", 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]
# operand is a full power expression so that -2**2 == -(2**2)
v = self.parse_factor()
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))