1use crate::compiler::{desugar_lazy, is_aggregation, lazy};
17use crate::dataflow::{apply_binary, apply_unary};
18use crate::CellValue;
19use improv_core_model::functions::{self, Kind};
20use improv_core_model::{Expr, FuncId, MeasureId};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25pub enum ScalarError {
26 #[error(
29 "{0} needs dimensions; aggregate over categories in an Improv model, not a scalar formula"
30 )]
31 NeedsDimensions(String),
32 #[error("unknown function {0:?}")]
34 UnknownFunction(FuncId),
35 #[error("{want} takes other arguments than the {got} given")]
37 Arity {
38 func: FuncId,
39 want: String,
40 got: usize,
41 },
42}
43
44pub fn eval(
48 expr: &Expr,
49 env: &HashMap<MeasureId, CellValue>,
50) -> Result<Option<CellValue>, ScalarError> {
51 check(expr)?;
52 Ok(eval_checked(expr, env))
53}
54
55pub fn check(expr: &Expr) -> Result<(), ScalarError> {
58 match expr {
59 Expr::Literal(_) => Ok(()),
60 Expr::Category(_) => Err(ScalarError::NeedsDimensions("a category argument".into())),
61 Expr::Ref(_, spec) => {
62 if spec.by.is_empty() && spec.over.is_empty() && spec.except.is_empty() {
63 Ok(())
64 } else {
65 Err(ScalarError::NeedsDimensions(
66 "a dimension projection (BY/OVER/EXCEPT)".into(),
67 ))
68 }
69 }
70 Expr::UnaryOp(_, a) => check(a),
71 Expr::BinaryOp(_, a, b) => check(a).and_then(|()| check(b)),
72 Expr::Call(func, args) => {
73 if is_aggregation(*func) {
74 return Err(ScalarError::NeedsDimensions(
75 "an aggregation (SUM(x OVER …))".into(),
76 ));
77 }
78 let def = functions::by_id(*func).ok_or(ScalarError::UnknownFunction(*func))?;
79 if def.category == functions::Category::Dimensional {
80 return Err(ScalarError::NeedsDimensions(format!(
81 "{} (it reads a category)",
82 def.name
83 )));
84 }
85 if !def.accepts(args.len()) {
86 return Err(ScalarError::Arity {
87 func: *func,
88 want: def.signature(),
89 got: args.len(),
90 });
91 }
92 args.iter().try_for_each(check)
93 }
94 }
95}
96
97fn eval_checked(expr: &Expr, env: &HashMap<MeasureId, CellValue>) -> Option<CellValue> {
98 match expr {
99 Expr::Literal(v) => CellValue::from_model_value(v),
100 Expr::Category(_) => None,
101 Expr::Ref(m, _) => env.get(m).cloned(),
102 Expr::UnaryOp(op, a) => Some(apply_unary(*op, &eval_checked(a, env)?)),
103 Expr::BinaryOp(op, a, b) => {
104 let (a, b) = (eval_checked(a, env)?, eval_checked(b, env)?);
105 Some(apply_binary(*op, &a, &b))
106 }
107 Expr::Call(func, args) if func.0 == lazy::IF => {
108 match eval_checked(&args[0], env)? {
111 CellValue::Bool(true) => eval_checked(&args[1], env),
112 CellValue::Bool(false) => eval_checked(args.get(2)?, env),
113 other => Some(other),
114 }
115 }
116 Expr::Call(func, args) if func.0 == lazy::COALESCE => {
118 args.iter().find_map(|a| eval_checked(a, env))
119 }
120 Expr::Call(func, args) if functions::by_id(*func).is_some_and(|d| d.kind == Kind::Lazy) => {
121 eval_checked(&desugar_lazy(*func, args).ok()?, env)
122 }
123 Expr::Call(func, args) => {
124 let vals = args
125 .iter()
126 .map(|a| eval_checked(a, env))
127 .collect::<Option<Vec<CellValue>>>()?;
128 Some(crate::builtins::eval(*func, &vals))
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use improv_core_model::{
137 BinaryOp, CategoryId, DimensionSpec, Formula, Measure, MeasureKind, Model, Name, Value,
138 ValueType,
139 };
140 use proptest::prelude::*;
141
142 const A: MeasureId = MeasureId(1);
143 const B: MeasureId = MeasureId(2);
144
145 fn r(m: MeasureId) -> Expr {
146 Expr::Ref(m, DimensionSpec::default())
147 }
148 fn n(x: f64) -> Expr {
149 Expr::Literal(Value::Number(x))
150 }
151 fn t(s: &str) -> Expr {
152 Expr::Literal(Value::Text(s.into()))
153 }
154 fn bin(op: BinaryOp, a: Expr, b: Expr) -> Expr {
155 Expr::BinaryOp(op, Box::new(a), Box::new(b))
156 }
157 fn call(f: u32, args: Vec<Expr>) -> Expr {
158 Expr::Call(FuncId(f), args)
159 }
160 fn env(a: f64, b: f64) -> HashMap<MeasureId, CellValue> {
161 HashMap::from([(A, CellValue::num(a)), (B, CellValue::num(b))])
162 }
163 fn num(e: &Expr, a: f64, b: f64) -> f64 {
164 eval(e, &env(a, b)).unwrap().unwrap().as_num().unwrap()
165 }
166 fn boolean(e: &Expr) -> bool {
167 match eval(e, &env(2.0, 3.0)).unwrap().unwrap() {
168 CellValue::Bool(b) => b,
169 other => panic!("not a bool: {other:?}"),
170 }
171 }
172
173 #[test]
174 fn arithmetic() {
175 assert_eq!(num(&bin(BinaryOp::Add, r(A), r(B)), 2.0, 3.0), 5.0);
176 assert_eq!(num(&bin(BinaryOp::Sub, r(A), r(B)), 2.0, 3.0), -1.0);
177 assert_eq!(num(&bin(BinaryOp::Mul, r(A), r(B)), 2.0, 3.0), 6.0);
178 assert_eq!(num(&bin(BinaryOp::Div, r(A), r(B)), 3.0, 2.0), 1.5);
179 let neg = Expr::UnaryOp(improv_core_model::UnaryOp::Neg, Box::new(r(A)));
180 assert_eq!(num(&neg, 2.0, 0.0), -2.0);
181 }
182
183 fn value(e: &Expr, a: f64, b: f64) -> CellValue {
184 eval(e, &env(a, b)).unwrap().unwrap()
185 }
186
187 #[test]
188 fn division_by_zero_is_an_error() {
189 let div0 = CellValue::Err(crate::builtins::err::DIV0);
191 assert_eq!(value(&bin(BinaryOp::Div, r(A), r(B)), 1.0, 0.0), div0);
192 assert_eq!(value(&bin(BinaryOp::Div, r(A), r(B)), 0.0, 0.0), div0);
193 assert_eq!(value(&bin(BinaryOp::Div, n(-1.0), n(0.0)), 0.0, 0.0), div0);
194 }
195
196 #[test]
198 fn if_is_lazy() {
199 let div = bin(BinaryOp::Div, r(A), r(B)); let pos = bin(BinaryOp::Gt, r(B), n(0.0));
201 let iff = |args| call(30, args);
202 assert_eq!(
203 value(&iff(vec![pos.clone(), div.clone(), n(-1.0)]), 6.0, 2.0),
204 CellValue::num(3.0)
205 );
206 assert_eq!(
207 value(&iff(vec![pos.clone(), div.clone(), n(-1.0)]), 6.0, 0.0),
208 CellValue::num(-1.0)
209 );
210 assert_eq!(
211 eval(&iff(vec![pos.clone(), div.clone()]), &env(6.0, 0.0)),
212 Ok(None)
213 );
214 let bad = bin(BinaryOp::Gt, div.clone(), n(0.0));
216 assert_eq!(
217 value(&iff(vec![bad, n(1.0), n(2.0)]), 1.0, 0.0),
218 CellValue::Err(crate::builtins::err::DIV0)
219 );
220 let only_a = HashMap::from([(A, CellValue::num(1.0))]);
221 assert_eq!(
222 eval(
223 &iff(vec![bin(BinaryOp::Gt, r(A), n(0.0)), r(B), n(0.0)]),
224 &only_a
225 ),
226 Ok(None)
227 );
228 assert_eq!(
230 value(&call(34, vec![div.clone(), n(9.0)]), 1.0, 0.0),
231 CellValue::num(9.0)
232 );
233 assert_eq!(
234 value(&call(34, vec![div.clone(), n(9.0)]), 4.0, 2.0),
235 CellValue::num(2.0)
236 );
237 assert_eq!(eval(&call(36, vec![div.clone()]), &env(1.0, 0.0)), Ok(None));
238 assert_eq!(
239 value(&call(37, vec![r(A), n(5.0)]), 0.0, 0.0),
240 CellValue::num(5.0)
241 );
242 assert_eq!(
243 value(&call(33, vec![r(A), t("x"), t("y")]), 2.9, 0.0),
244 CellValue::Text("y".into())
245 );
246 assert_eq!(
247 value(&call(33, vec![r(A), t("x")]), 2.0, 0.0),
248 CellValue::Err(crate::builtins::err::VALUE)
249 );
250 assert_eq!(
251 value(
252 &call(
253 31,
254 vec![
255 bin(BinaryOp::Gt, r(A), n(5.0)),
256 t("big"),
257 bin(BinaryOp::Gt, r(A), n(1.0)),
258 t("mid")
259 ]
260 ),
261 2.0,
262 0.0
263 ),
264 CellValue::Text("mid".into())
265 );
266 assert_eq!(
267 value(
268 &call(31, vec![bin(BinaryOp::Gt, r(A), n(5.0)), t("big")]),
269 2.0,
270 0.0
271 ),
272 CellValue::Err(crate::builtins::err::NA)
273 );
274 assert_eq!(
275 value(
276 &call(
277 32,
278 vec![r(A), n(1.0), t("one"), n(2.0), t("two"), t("many")]
279 ),
280 2.0,
281 0.0
282 ),
283 CellValue::Text("two".into())
284 );
285 assert_eq!(
286 value(&call(32, vec![r(A), n(1.0), t("one"), t("many")]), 7.0, 0.0),
287 CellValue::Text("many".into())
288 );
289 assert_eq!(
290 value(&call(35, vec![call(53, vec![]), n(0.0)]), 0.0, 0.0),
291 CellValue::num(0.0)
292 );
293 }
294
295 #[test]
296 fn comparisons_and_logic() {
297 assert!(boolean(&bin(BinaryOp::Lt, r(A), r(B))));
298 assert!(boolean(&bin(BinaryOp::Le, r(A), r(A))));
299 assert!(!boolean(&bin(BinaryOp::Gt, r(A), r(B))));
300 assert!(boolean(&bin(BinaryOp::Ge, r(B), r(A))));
301 assert!(boolean(&bin(BinaryOp::Eq, r(A), n(2.0))));
302 assert!(boolean(&bin(BinaryOp::Ne, r(A), r(B))));
303 assert!(boolean(&bin(BinaryOp::And, r(A), r(B))));
304 assert!(!boolean(&bin(BinaryOp::And, r(A), n(0.0))));
305 assert!(boolean(&bin(BinaryOp::Or, n(0.0), r(B))));
306 let not = Expr::UnaryOp(improv_core_model::UnaryOp::Not, Box::new(n(0.0)));
307 assert!(boolean(¬));
308 }
309
310 #[test]
311 fn text_equality() {
312 let e = HashMap::from([(A, CellValue::Text("x".into()))]);
313 let eq = |rhs: &str| eval(&bin(BinaryOp::Eq, r(A), t(rhs)), &e).unwrap().unwrap();
314 assert_eq!(eq("x"), CellValue::Bool(true));
315 assert_eq!(eq("y"), CellValue::Bool(false));
316 }
317
318 #[test]
319 fn scalar_builtins() {
320 for (id, x, want) in [
321 (10, -2.5, 2.5),
322 (11, 2.5, 3.0),
323 (12, 2.5, 2.0),
324 (13, 2.1, 3.0),
325 (14, 9.0, 3.0),
326 (15, 4.0, -4.0),
327 ] {
328 assert_eq!(num(&call(id, vec![r(A)]), x, 0.0), want, "func {id}");
329 }
330 assert_eq!(
331 value(&call(14, vec![r(A)]), -1.0, 0.0),
332 CellValue::Err(crate::builtins::err::NUM)
333 );
334 assert_eq!(num(&call(20, vec![r(A), r(B)]), 1.0, 2.0), 1.0);
335 assert_eq!(num(&call(21, vec![r(A), r(B)]), 1.0, 2.0), 2.0);
336 }
337
338 #[test]
339 fn missing_input_propagates() {
340 let only_a = HashMap::from([(A, CellValue::num(1.0))]);
341 for e in [
342 bin(BinaryOp::Add, r(A), r(B)),
343 call(21, vec![r(A), r(B)]),
344 Expr::UnaryOp(improv_core_model::UnaryOp::Neg, Box::new(r(B))),
345 bin(BinaryOp::Or, n(1.0), r(B)), ] {
347 assert_eq!(eval(&e, &only_a), Ok(None), "{e:?}");
348 }
349 assert_eq!(
350 eval(&bin(BinaryOp::Add, r(A), n(1.0)), &only_a),
351 Ok(Some(CellValue::num(2.0)))
352 );
353 }
354
355 #[test]
356 fn rejects_what_needs_dimensions() {
357 let none = HashMap::new();
358 for f in 1..=4 {
359 assert!(matches!(
360 eval(&call(f, vec![r(A)]), &none),
361 Err(ScalarError::NeedsDimensions(_))
362 ));
363 }
364 let cats = vec![CategoryId(1)];
365 for spec in [
366 DimensionSpec {
367 by: cats.clone(),
368 ..DimensionSpec::default()
369 },
370 DimensionSpec {
371 over: cats.clone(),
372 ..DimensionSpec::default()
373 },
374 DimensionSpec {
375 except: cats.clone(),
376 ..DimensionSpec::default()
377 },
378 ] {
379 assert!(matches!(
380 eval(&Expr::Ref(A, spec), &none),
381 Err(ScalarError::NeedsDimensions(_))
382 ));
383 }
384 let nested = bin(BinaryOp::Add, r(B), call(1, vec![r(A)]));
386 assert!(eval(&nested, &none).is_err());
387 assert_eq!(
388 eval(&call(99, vec![]), &none),
389 Err(ScalarError::UnknownFunction(FuncId(99)))
390 );
391 assert!(matches!(
392 eval(&call(312, vec![r(A)]), &none),
393 Err(ScalarError::Arity { got: 1, .. })
394 ));
395 }
396
397 fn via_dataflow(expr: &Expr, a: Option<f64>, b: Option<f64>) -> Option<CellValue> {
400 let mut m = Model::new();
401 let measure = |id, name: &str, kind| Measure {
402 id,
403 name: Name(name.into()),
404 value_type: ValueType::Number,
405 categories: vec![],
406 kind,
407 description: None,
408 };
409 m.add_measure(measure(A, "A", MeasureKind::Input));
410 m.add_measure(measure(B, "B", MeasureKind::Input));
411 let target = MeasureId(3);
412 let derived = MeasureKind::Derived(Formula::new(expr.clone()));
413 m.add_measure(measure(target, "R", derived));
414 for (id, v) in [(A, a), (B, b)] {
415 if let Some(v) = v {
416 m.set_input(id, Default::default(), Value::Number(v));
417 }
418 }
419 let out = crate::dataflow::evaluate(&m, &[target]).expect("dataflow evaluate");
420 out.get(&target)
421 .and_then(|cells| cells.get(&Vec::new()).cloned())
422 }
423
424 fn num_expr() -> impl Strategy<Value = Expr> {
425 let leaf = prop_oneof![
426 Just(r(A)),
427 Just(r(B)),
428 (-3i8..=3).prop_map(|x| n(f64::from(x) / 2.0)),
429 ];
430 leaf.prop_recursive(3, 12, 2, |inner| {
431 let op = prop_oneof![
432 Just(BinaryOp::Add),
433 Just(BinaryOp::Sub),
434 Just(BinaryOp::Mul),
435 Just(BinaryOp::Div),
436 Just(BinaryOp::Pow),
437 ];
438 let one: Vec<u32> = improv_core_model::functions::all()
440 .filter(|d| {
441 d.kind == Kind::Scalar
442 && d.required == 1
443 && d.param(0) == functions::Arg::Number
444 && matches!(
445 d.ret,
446 functions::Ret::Fixed(improv_core_model::ValueType::Number)
447 )
448 })
449 .map(|d| d.id.0)
450 .collect();
451 let two: Vec<u32> = improv_core_model::functions::all()
452 .filter(|d| {
453 d.kind == Kind::Scalar
454 && d.accepts(2)
455 && d.param(0) == functions::Arg::Number
456 && d.param(1) == functions::Arg::Number
457 && matches!(
458 d.ret,
459 functions::Ret::Fixed(improv_core_model::ValueType::Number)
460 )
461 })
462 .map(|d| d.id.0)
463 .collect();
464 let cmp = (inner.clone(), inner.clone()).prop_map(|(a, b)| bin(BinaryOp::Gt, a, b));
465 prop_oneof![
466 (op, inner.clone(), inner.clone()).prop_map(|(op, a, b)| bin(op, a, b)),
467 (prop::sample::select(one), inner.clone()).prop_map(|(f, a)| call(f, vec![a])),
468 (prop::sample::select(two), inner.clone(), inner.clone())
469 .prop_map(|(f, a, b)| call(f, vec![a, b])),
470 (cmp.clone(), inner.clone(), inner.clone())
471 .prop_map(|(c, a, b)| call(30, vec![c, a, b])),
472 (cmp, inner.clone()).prop_map(|(c, a)| call(30, vec![c, a])),
473 (inner.clone(), inner).prop_map(|(a, b)| call(34, vec![a, b])),
474 ]
475 })
476 }
477
478 fn any_expr() -> impl Strategy<Value = Expr> {
479 let cmp = prop_oneof![
480 Just(BinaryOp::Lt),
481 Just(BinaryOp::Le),
482 Just(BinaryOp::Gt),
483 Just(BinaryOp::Ge),
484 Just(BinaryOp::Eq),
485 Just(BinaryOp::Ne),
486 ];
487 prop_oneof![
488 num_expr(),
489 (cmp, num_expr(), num_expr()).prop_map(|(op, a, b)| bin(op, a, b)),
490 ]
491 }
492
493 proptest! {
494 #![proptest_config(ProptestConfig::with_cases(256))]
495 #[test]
498 fn matches_dataflow(
499 expr in any_expr(),
500 a in proptest::option::weighted(0.9, -4i8..=4),
501 b in proptest::option::weighted(0.9, -4i8..=4),
502 ) {
503 let (a, b) = (a.map(f64::from), b.map(f64::from));
504 let mut env = HashMap::new();
505 for (id, v) in [(A, a), (B, b)] {
506 if let Some(v) = v {
507 env.insert(id, CellValue::num(v));
508 }
509 }
510 let scalar = eval(&expr, &env).expect("scalar eval");
511 prop_assert_eq!(scalar, via_dataflow(&expr, a, b), "{:?}", expr);
512 }
513 }
514}