Files

57 lines
3.1 KiB
Python

"""Bounded arithmetic expressions, interpreted without eval or Python execution."""
import ast
import math
from functools import lru_cache
FUNCTIONS={k:getattr(math,k) for k in ['sin','cos','tan','asin','acos','atan','atan2','sqrt','exp','log','floor','ceil','radians','degrees']}
FUNCTIONS.update(abs=abs,min=min,max=max)
CONSTANTS={'pi':math.pi,'tau':math.tau,'e':math.e}
@lru_cache(maxsize=512)
def parse(text):
if len(text)>2048:raise ValueError('Expression exceeds 2048 characters')
tree=ast.parse(text,mode='eval')
if sum(1 for _ in ast.walk(tree))>128:raise ValueError('Expression is too complex')
permitted=(ast.Expression,ast.Constant,ast.Name,ast.Load,ast.BinOp,ast.UnaryOp,ast.Call,ast.Add,ast.Sub,ast.Mult,ast.Div,ast.Pow,ast.Mod,ast.USub,ast.UAdd)
for n in ast.walk(tree):
if not isinstance(n,permitted):raise ValueError(f'Unsupported expression syntax: {type(n).__name__}')
if isinstance(n,ast.Constant) and (isinstance(n.value,bool) or not isinstance(n.value,(int,float))):raise ValueError('Only numeric literals are allowed')
if isinstance(n,ast.Call) and (not isinstance(n.func,ast.Name) or n.func.id not in FUNCTIONS or n.keywords):raise ValueError('Only documented math functions are allowed')
return tree.body
def scalar(value, variables=None):
env={**CONSTANTS,**(variables or {})}
def walk(n):
if isinstance(n,ast.Constant):return float(n.value)
if isinstance(n,ast.Name):
if n.id not in env:raise ValueError(f'Unknown symbol: {n.id}')
return float(env[n.id])
if isinstance(n,ast.UnaryOp):return -walk(n.operand) if isinstance(n.op,ast.USub) else walk(n.operand)
if isinstance(n,ast.Call):return FUNCTIONS[n.func.id](*[walk(a) for a in n.args])
a,b=walk(n.left),walk(n.right)
if isinstance(n.op,ast.Pow):
if abs(b)>32 or abs(a)>1e9:raise ValueError('Power operands exceed limits')
return a**b
if isinstance(n.op,ast.Add):return a+b
if isinstance(n.op,ast.Sub):return a-b
if isinstance(n.op,ast.Mult):return a*b
if isinstance(n.op,ast.Div):return a/b
if isinstance(n.op,ast.Mod):return a%b
raise ValueError('Unsupported operator')
try:
if isinstance(value,bool):raise ValueError('A boolean is not a numeric expression')
result=walk(parse(value)) if isinstance(value,str) else float(value)
if isinstance(result,complex) or not math.isfinite(result) or abs(result)>1e9:raise ValueError('Expression result must be finite and within ±1e9')
return result
except (ZeroDivisionError,OverflowError,TypeError,ValueError,SyntaxError,RecursionError) as e:
raise ValueError(f'Invalid expression {str(value)[:100]!r}: {e}') from e
def vector(value,variables=None,size=3):
if not isinstance(value,(list,tuple)) or len(value)!=size:raise ValueError(f'Expected a {size}-component vector')
return [scalar(v,variables) for v in value]
def integer(value,variables=None,low=1,high=2048):
n=scalar(value,variables)
if n!=int(n) or not low<=n<=high:raise ValueError(f'Expected an integer in [{low}, {high}]')
return int(n)