1use std::collections::BTreeMap;
11
12use crate::error::{EngineError, ErrorCode};
13use crate::expr::{BinaryOp, Expr, UnaryOp};
14use crate::limits::Limits;
15use crate::number::{Number, NumericContext};
16
17pub type NumberCalls<'a> = dyn Fn(&str, &[Number]) -> Result<Number, EngineError> + 'a;
19
20pub type FloatCalls<'a> = dyn Fn(&str, &[f64]) -> Result<f64, EngineError> + 'a;
22
23struct Budget {
24 ops: u64,
25 max_ops: u64,
26 max_depth: usize,
27}
28
29impl Budget {
30 fn new(limits: &Limits) -> Budget {
31 Budget {
32 ops: 0,
33 max_ops: limits.max_operations,
34 max_depth: limits.max_ast_depth,
35 }
36 }
37
38 fn tick(&mut self, depth: usize) -> Result<(), EngineError> {
39 if depth > self.max_depth {
40 return Err(EngineError::new(
41 ErrorCode::ResourceLimit,
42 format!(
43 "expression nesting depth {depth} exceeds the limit of {}",
44 self.max_depth
45 ),
46 ));
47 }
48 self.ops += 1;
49 if self.ops > self.max_ops {
50 return Err(EngineError::new(
51 ErrorCode::ResourceLimit,
52 format!("expression operation budget of {} exceeded", self.max_ops),
53 ));
54 }
55 Ok(())
56 }
57}
58
59pub fn evaluate_number(
65 expr: &Expr,
66 bindings: &BTreeMap<String, Number>,
67 numeric: &NumericContext,
68 limits: &Limits,
69 calls: &NumberCalls<'_>,
70) -> Result<Number, EngineError> {
71 let mut budget = Budget::new(limits);
72 eval_number(expr, bindings, numeric, limits, calls, &mut budget, 0)
73}
74
75fn eval_number(
76 expr: &Expr,
77 bindings: &BTreeMap<String, Number>,
78 numeric: &NumericContext,
79 limits: &Limits,
80 calls: &NumberCalls<'_>,
81 budget: &mut Budget,
82 depth: usize,
83) -> Result<Number, EngineError> {
84 budget.tick(depth)?;
85 match expr {
86 Expr::Number(number) => Ok(number.clone()),
87 Expr::Ident(name) => bindings.get(name).cloned().ok_or_else(|| {
88 let mut available: Vec<&String> = bindings.keys().collect();
89 available.sort();
90 EngineError::new(
91 ErrorCode::NotFound,
92 format!("unknown binding {name:?}; available: {available:?}"),
93 )
94 }),
95 Expr::Unary { op, expr } => {
96 let value = eval_number(expr, bindings, numeric, limits, calls, budget, depth + 1)?;
97 match op {
98 UnaryOp::Neg => Ok(value.neg()),
99 UnaryOp::Pos => Ok(value),
100 UnaryOp::Not => Err(EngineError::malformed(
101 "logical negation is not valid in a numeric expression",
102 )),
103 }
104 }
105 Expr::Binary { op, left, right } => {
106 let a = eval_number(left, bindings, numeric, limits, calls, budget, depth + 1)?;
107 let b = eval_number(right, bindings, numeric, limits, calls, budget, depth + 1)?;
108 let result = match op {
109 BinaryOp::Add => a.add(&b, numeric, limits)?,
110 BinaryOp::Sub => a.sub(&b, numeric, limits)?,
111 BinaryOp::Mul => a.mul(&b, numeric, limits)?,
112 BinaryOp::Div => a.div(&b, numeric, limits)?,
113 BinaryOp::Rem => a.rem(&b, numeric, limits)?,
114 BinaryOp::Pow => a.pow(&b, numeric, limits)?,
115 BinaryOp::And | BinaryOp::Or => {
116 return Err(EngineError::malformed(
117 "logical operators are not valid in a numeric expression",
118 ));
119 }
120 };
121 Ok(result.value)
122 }
123 Expr::Call { name, args } => {
124 let mut values = Vec::with_capacity(args.len());
125 for arg in args {
126 if arg.name.is_some() {
127 return Err(EngineError::malformed(
128 "named arguments are not supported in this expression context",
129 ));
130 }
131 values.push(eval_number(
132 &arg.value,
133 bindings,
134 numeric,
135 limits,
136 calls,
137 budget,
138 depth + 1,
139 )?);
140 }
141 calls(name, &values)
142 }
143 Expr::Compare { .. } => Err(EngineError::malformed(
144 "comparisons are not valid in a numeric expression",
145 )),
146 Expr::Bool(_) | Expr::Text(_) | Expr::Array(_) | Expr::Record(_) => Err(
147 EngineError::malformed("expression must produce a single numeric value"),
148 ),
149 }
150}
151
152pub fn evaluate_f64(
154 expr: &Expr,
155 bindings: &BTreeMap<String, f64>,
156 limits: &Limits,
157 calls: &FloatCalls<'_>,
158) -> Result<f64, EngineError> {
159 let mut budget = Budget::new(limits);
160 eval_f64(expr, bindings, calls, &mut budget, 0)
161}
162
163fn eval_f64(
164 expr: &Expr,
165 bindings: &BTreeMap<String, f64>,
166 calls: &FloatCalls<'_>,
167 budget: &mut Budget,
168 depth: usize,
169) -> Result<f64, EngineError> {
170 budget.tick(depth)?;
171 match expr {
172 Expr::Number(number) => number
173 .to_f64()
174 .ok_or_else(|| EngineError::domain("numeric literal is not representable as float64")),
175 Expr::Ident(name) => bindings.get(name).copied().ok_or_else(|| {
176 let mut available: Vec<&String> = bindings.keys().collect();
177 available.sort();
178 EngineError::new(
179 ErrorCode::NotFound,
180 format!("unknown binding {name:?}; available: {available:?}"),
181 )
182 }),
183 Expr::Unary { op, expr } => {
184 let value = eval_f64(expr, bindings, calls, budget, depth + 1)?;
185 match op {
186 UnaryOp::Neg => Ok(-value),
187 UnaryOp::Pos => Ok(value),
188 UnaryOp::Not => Err(EngineError::malformed(
189 "logical negation is not valid in a numeric expression",
190 )),
191 }
192 }
193 Expr::Binary { op, left, right } => {
194 let a = eval_f64(left, bindings, calls, budget, depth + 1)?;
195 let b = eval_f64(right, bindings, calls, budget, depth + 1)?;
196 match op {
197 BinaryOp::Add => Ok(a + b),
198 BinaryOp::Sub => Ok(a - b),
199 BinaryOp::Mul => Ok(a * b),
200 BinaryOp::Div => {
201 if b == 0.0 {
202 Err(EngineError::division_by_zero("float division by zero"))
203 } else {
204 Ok(a / b)
205 }
206 }
207 BinaryOp::Rem => {
208 if b == 0.0 {
209 Err(EngineError::division_by_zero("float remainder by zero"))
210 } else {
211 Ok(a % b)
212 }
213 }
214 BinaryOp::Pow => {
215 let value = a.powf(b);
216 if value.is_finite() {
217 Ok(value)
218 } else {
219 Err(EngineError::domain(format!("power {a}^{b} is not finite")))
220 }
221 }
222 BinaryOp::And | BinaryOp::Or => Err(EngineError::malformed(
223 "logical operators are not valid in a numeric expression",
224 )),
225 }
226 }
227 Expr::Call { name, args } => {
228 let mut values = Vec::with_capacity(args.len());
229 for arg in args {
230 if arg.name.is_some() {
231 return Err(EngineError::malformed(
232 "named arguments are not supported in this expression context",
233 ));
234 }
235 values.push(eval_f64(&arg.value, bindings, calls, budget, depth + 1)?);
236 }
237 let value = calls(name, &values)?;
238 if value.is_finite() {
239 Ok(value)
240 } else {
241 Err(EngineError::domain(format!(
242 "function {name} produced a non-finite result"
243 )))
244 }
245 }
246 Expr::Compare { .. } => Err(EngineError::malformed(
247 "comparisons are not valid in a numeric expression",
248 )),
249 Expr::Bool(_) | Expr::Text(_) | Expr::Array(_) | Expr::Record(_) => Err(
250 EngineError::malformed("expression must produce a single numeric value"),
251 ),
252 }
253}
254
255type FloatCall = Box<dyn Fn(&[f64]) -> Result<f64, EngineError>>;
258
259pub struct SimpleCalls {
260 entries: Vec<(String, usize, FloatCall)>,
261}
262
263impl SimpleCalls {
264 pub fn new() -> SimpleCalls {
265 SimpleCalls {
266 entries: Vec::new(),
267 }
268 }
269
270 pub fn add<F>(mut self, name: &str, arity: usize, function: F) -> SimpleCalls
271 where
272 F: Fn(&[f64]) -> Result<f64, EngineError> + 'static,
273 {
274 self.entries
275 .push((name.to_string(), arity, Box::new(function)));
276 self
277 }
278
279 pub fn dispatch(&self, name: &str, args: &[f64]) -> Result<f64, EngineError> {
280 for (candidate, arity, function) in &self.entries {
281 if candidate == name {
282 if args.len() != *arity {
283 return Err(EngineError::malformed(format!(
284 "function {name} expects {arity} argument(s), found {}",
285 args.len()
286 )));
287 }
288 return function(args);
289 }
290 }
291 Err(EngineError::new(
292 ErrorCode::UnknownFunction,
293 format!("unknown function {name:?} in restricted expression"),
294 ))
295 }
296
297 pub fn as_table(&self) -> impl Fn(&str, &[f64]) -> Result<f64, EngineError> + '_ {
298 move |name, args| self.dispatch(name, args)
299 }
300}
301
302impl Default for SimpleCalls {
303 fn default() -> Self {
304 SimpleCalls::new()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::expr::parse_expression;
312
313 fn limits() -> Limits {
314 Limits::conservative()
315 }
316
317 #[test]
318 fn exact_evaluation_uses_shared_arithmetic() {
319 let expr = parse_expression("0.1 + 0.2 * 2", &limits()).unwrap();
320 let result = evaluate_number(
321 &expr,
322 &BTreeMap::new(),
323 &NumericContext::default(),
324 &limits(),
325 &|name, _| Err(EngineError::new(ErrorCode::UnknownFunction, name)),
326 )
327 .unwrap();
328 assert_eq!(result.to_string(), "0.5");
329 }
330
331 #[test]
332 fn exact_evaluation_is_rational_for_division() {
333 let expr = parse_expression("1 / 3 + 1 / 6", &limits()).unwrap();
334 let result = evaluate_number(
335 &expr,
336 &BTreeMap::new(),
337 &NumericContext::default(),
338 &limits(),
339 &|name, _| Err(EngineError::new(ErrorCode::UnknownFunction, name)),
340 )
341 .unwrap();
342 assert_eq!(result.to_string(), "1/2");
343 }
344
345 #[test]
346 fn float_evaluation_calls_provider() {
347 let calls = SimpleCalls::new().add("double", 1, |args| Ok(args[0] * 2.0));
348 let expr = parse_expression("double(x) + 1", &limits()).unwrap();
349 let mut bindings = BTreeMap::new();
350 bindings.insert("x".to_string(), 3.0);
351 let result = evaluate_f64(&expr, &bindings, &limits(), &calls.as_table()).unwrap();
352 assert_eq!(result, 7.0);
353 }
354
355 #[test]
356 fn unknown_function_is_structured() {
357 let expr = parse_expression("nope(1)", &limits()).unwrap();
358 let error = evaluate_f64(
359 &expr,
360 &BTreeMap::new(),
361 &limits(),
362 &SimpleCalls::new().as_table(),
363 )
364 .unwrap_err();
365 assert_eq!(error.code, ErrorCode::UnknownFunction);
366 }
367}