beam: safe arithmetic expression evaluator with tests
This commit is contained in:
1 parent
397bcb3c99
commit
e1182ce4f8
2 files changed
+198
No files matched your search
@@ -0,0 +1,164 @@
|
||||
"""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]
|
||||
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))
|
||||
@@ -0,0 +1,34 @@
|
||||
import math, sys
|
||||
sys.path.insert(0, '/root/.hermes/wizard-outputs/beam')
|
||||
from expr_eval import evaluate, EvalError
|
||||
|
||||
cases = [
|
||||
("2+3*4", 14.0),
|
||||
("(2+3)*4", 20.0),
|
||||
("2**3**2", 512.0), # right assoc
|
||||
("10 // 3", 3.0),
|
||||
("10 % 3", 1.0),
|
||||
("-5 + 2", -3.0),
|
||||
("2*-3", -6.0),
|
||||
("sqrt(16)+abs(-2)", 6.0),
|
||||
("max(1,5,2)", 5.0),
|
||||
("sin(0)", 0.0),
|
||||
("2**0.5", math.sqrt(2)),
|
||||
("1e3 + 0.5", 1000.5),
|
||||
]
|
||||
for expr, exp in cases:
|
||||
got = evaluate(expr)
|
||||
assert got == exp, (expr, got, exp)
|
||||
print("OK", expr, got)
|
||||
|
||||
# variables
|
||||
assert evaluate("x*2 + pi", {'x': 4, 'pi': math.pi}) == 8 + math.pi
|
||||
# errors
|
||||
for bad in ["2 +", "foo", "1/0", "sin(1,2)", "2 **", "abc("]:
|
||||
try:
|
||||
evaluate(bad)
|
||||
print("FAIL no error:", bad)
|
||||
sys.exit(1)
|
||||
except EvalError as e:
|
||||
print("ERR ok:", bad, "->", e)
|
||||
print("ALL PASS")
|
||||
Reference in new issue
Block a user