1use crate::builtins::{self, err};
18use crate::compiler::{compile_formula, CompileContext};
19use crate::plan::{PlanNode, PlanNodeKind};
20use crate::reduce::{self, Rows};
21use crate::{encode_coord, CellValue, CoordKey};
22use differential_dataflow::input::InputSession;
23use differential_dataflow::operators::{Join as _, Reduce, Threshold};
24use differential_dataflow::AsCollection;
25use differential_dataflow::Collection;
26use improv_core_model::{
27 BinaryOp, CategoryId, Formula, FuncId, ItemId, MeasureId, MeasureKind, Model, UnaryOp,
28};
29#[cfg(test)]
30use std::collections::BTreeMap;
31use std::collections::HashMap;
32use std::sync::{Arc, Mutex};
33use timely::dataflow::operators::ToStream;
34use timely::dataflow::Scope;
35
36#[derive(Debug, thiserror::Error)]
37pub enum EngineError {
38 #[error("compile error: {0}")]
39 Compile(#[from] crate::compiler::CompileError),
40 #[error("unsupported plan node in v1 dataflow builder: {0}")]
41 Unsupported(String),
42}
43
44pub(crate) type Coll<G> = Collection<G, (CoordKey, CellValue), isize>;
46
47pub(crate) fn build_coll<G: Scope>(
52 node: &PlanNode,
53 scope: &mut G,
54 inputs: &HashMap<MeasureId, Coll<G>>,
55 derived: &HashMap<MeasureId, Coll<G>>,
56) -> Result<Coll<G>, EngineError>
57where
58 G::Timestamp: differential_dataflow::lattice::Lattice + timely::progress::Timestamp,
59{
60 match &node.kind {
61 PlanNodeKind::InputMeasure(m) => inputs
62 .get(m)
63 .or_else(|| derived.get(m))
64 .cloned()
65 .ok_or_else(|| EngineError::Unsupported(format!("no collection for measure {m:?}"))),
66
67 PlanNodeKind::MapUnary(op, child) => {
68 let c = build_coll(child, scope, inputs, derived)?;
69 let op = *op;
70 Ok(c.map(move |(k, v)| (k, apply_unary(op, &v))))
71 }
72
73 PlanNodeKind::MapBinary(op, left, right) => {
74 let op = *op;
78 match (literal_value(left), literal_value(right)) {
79 (Some(lit), None) => {
80 let r = build_coll(right, scope, inputs, derived)?;
81 Ok(r.map(move |(k, b)| (k, apply_binary(op, &lit, &b))))
82 }
83 (None, Some(lit)) => {
84 let l = build_coll(left, scope, inputs, derived)?;
85 Ok(l.map(move |(k, a)| (k, apply_binary(op, &a, &lit))))
86 }
87 (Some(a), Some(b)) => {
88 let v = apply_binary(op, &a, &b);
90 let empty: CoordKey = Vec::new();
91 let t = <G::Timestamp as timely::progress::Timestamp>::minimum();
92 Ok(vec![((empty, v), t, 1isize)]
93 .to_stream(scope)
94 .as_collection())
95 }
96 (None, None) => {
97 let l = build_coll(left, scope, inputs, derived)?;
98 let r = build_coll(right, scope, inputs, derived)?;
99 Ok(align(&l, &r, shared_cats(left, right))
100 .map(move |(k, (a, b))| (k, apply_binary(op, &a, &b))))
101 }
102 }
103 }
104
105 PlanNodeKind::Join {
106 left,
107 right,
108 join_keys,
109 } => {
110 let l = build_coll(left, scope, inputs, derived)?;
114 let r = build_coll(right, scope, inputs, derived)?;
115 Ok(align(&l, &r, join_keys.clone()).map(|(k, (lv, _rv))| (k, lv)))
116 }
117
118 PlanNodeKind::Aggregate {
119 inputs: ins,
120 params,
121 pred,
122 group_by,
123 func,
124 order,
125 window,
126 per_item,
127 into,
128 } => build_aggregate(
129 scope,
130 inputs,
131 derived,
132 AggSpec {
133 ins,
134 params,
135 pred: pred.as_deref(),
136 group_by,
137 func: *func,
138 order,
139 window: *window,
140 per_item: *per_item,
141 into,
142 },
143 ),
144
145 PlanNodeKind::Items(cat, vals) => {
146 let t = <G::Timestamp as timely::progress::Timestamp>::minimum();
147 let rows: Vec<_> = vals
148 .iter()
149 .filter_map(|(item, v)| {
150 let cv = CellValue::from_model_value(v)?;
151 Some(((vec![(cat.0, item.0)], cv), t.clone(), 1isize))
152 })
153 .collect();
154 Ok(rows.to_stream(scope).as_collection())
155 }
156
157 PlanNodeKind::Coalesce { args, grid } => {
158 let colls = args
159 .iter()
160 .map(|a| build_coll(a, scope, inputs, derived))
161 .collect::<Result<Vec<_>, _>>()?;
162 let spans = |a: &PlanNode| a.ty.dim == node.ty.dim;
163 let domain = match grid {
166 Some(g) => {
167 let t = <G::Timestamp as timely::progress::Timestamp>::minimum();
168 grid_keys(g)
169 .into_iter()
170 .map(|k| (k, t.clone(), 1isize))
171 .collect::<Vec<_>>()
172 .to_stream(scope)
173 .as_collection()
174 }
175 None => {
176 let mut keys = None::<Collection<G, CoordKey, isize>>;
177 for (_, c) in args.iter().zip(&colls).filter(|(a, _)| spans(a)) {
178 let k = c.map(|(k, _)| k);
179 keys = Some(keys.map_or(k.clone(), |acc| acc.concat(&k)));
180 }
181 keys.expect("the widest argument spans the result")
182 .distinct()
183 }
184 };
185 let domain = domain.map(|k| (k, CellValue::Bool(true)));
186 let mut tagged = None::<Collection<G, (CoordKey, (u32, CellValue)), isize>>;
189 for (i, (a, c)) in args.iter().zip(&colls).enumerate() {
190 let i = i as u32;
191 let t = if spans(a) {
192 c.map(move |(k, v)| (k, (i, v)))
193 } else {
194 align(&domain, c, a.ty.dim.categories.clone())
195 .map(move |(k, (_, v))| (k, (i, v)))
196 };
197 tagged = Some(tagged.map_or(t.clone(), |acc| acc.concat(&t)));
198 }
199 let tagged = tagged.expect("COALESCE has arguments");
200 Ok(tagged.reduce(|_k, input, output| {
201 if let Some(((_, v), _)) = input.iter().find(|(_, m)| *m > 0) {
203 output.push((v.clone(), 1isize));
204 }
205 }))
206 }
207
208 PlanNodeKind::Fixpoint { .. } => Err(EngineError::Unsupported(
209 "FIXPOINT is solved by the engine, as a measure's whole formula".into(),
210 )),
211
212 PlanNodeKind::Remap { input, maps } => {
213 let x = build_coll(input, scope, inputs, derived)?;
214 let (from, to) = remap_table(maps);
215 Ok(x.flat_map(move |(k, v)| Some((remap_key(&k, &from, &to)?, v))))
216 }
217
218 PlanNodeKind::Recurrence {
219 body,
220 refs,
221 slots,
222 cat,
223 order,
224 } => {
225 let ids: Vec<MeasureId> = refs.iter().map(|(id, _)| *id).collect();
227 let (_, p0) = &refs[0];
228 let mut rows = build_coll(p0, scope, inputs, derived)?.map(|(k, v)| (k, vec![v]));
229 let mut dim = p0.ty.dim.clone();
230 for (_, p) in &refs[1..] {
231 let c = build_coll(p, scope, inputs, derived)?;
232 let keys: Vec<CategoryId> = dim
233 .categories
234 .iter()
235 .copied()
236 .filter(|c| p.ty.dim.categories.contains(c))
237 .collect();
238 rows = align_with(&rows, &c, keys).map(|(k, (mut vs, v))| {
239 vs.push(v);
240 (k, vs)
241 });
242 dim = dim.union(&p.ty.dim);
243 }
244 let pos: HashMap<u32, u32> = order
245 .iter()
246 .enumerate()
247 .map(|(i, it)| (it.0, i as u32))
248 .collect();
249 let c = cat.0;
250 let keyed = rows.map(move |(k, vs)| {
251 let item = k.iter().find(|(cc, _)| *cc == c).map_or(0, |(_, i)| *i);
252 let p = pos.get(&item).copied().unwrap_or(u32::MAX / 2 + item);
253 let group: CoordKey = k.into_iter().filter(|(cc, _)| *cc != c).collect();
254 (group, ((p, item), vs))
255 });
256 let (body, slots) = (body.clone(), slots.clone());
257 let scan = keyed.reduce(
258 move |_g, input, output: &mut Vec<((u32, CellValue), isize)>| {
259 let mut done: HashMap<i64, CellValue> = HashMap::new();
260 for (((p, item), vs), mult) in input {
261 if *mult <= 0 {
262 continue;
263 }
264 let mut env: HashMap<MeasureId, CellValue> =
265 ids.iter().copied().zip(vs.iter().cloned()).collect();
266 for (slot, back, default) in &slots {
267 let earlier =
268 done.get(&(i64::from(*p) - back)).cloned().or_else(|| {
269 default
270 .as_ref()
271 .and_then(|d| crate::scalar::eval(d, &env).ok().flatten())
272 });
273 if let Some(v) = earlier {
274 env.insert(*slot, v);
275 }
276 }
277 if let Ok(Some(v)) = crate::scalar::eval(&body, &env) {
278 done.insert(i64::from(*p), v.clone());
279 output.push(((*item, v), 1isize));
280 }
281 }
282 },
283 );
284 Ok(scan.map(move |(g, (item, v))| {
285 let mut k = g;
286 k.push((c, item));
287 k.sort_unstable();
288 (k, v)
289 }))
290 }
291
292 PlanNodeKind::Shift {
293 input,
294 cat,
295 offset,
296 order,
297 default,
298 } => {
299 let x = build_coll(input, scope, inputs, derived)?;
300 let pos: HashMap<u32, usize> =
301 order.iter().enumerate().map(|(i, it)| (it.0, i)).collect();
302 let (cat, offset, order) = (cat.0, *offset, order.clone());
303 let moved = x.flat_map(move |(k, v)| {
306 let mut k2 = k.clone();
307 let slot = k2.iter_mut().find(|(c, _)| *c == cat)?;
308 let p = *pos.get(&slot.1)? as i64 - offset;
309 let it = order.get(usize::try_from(p).ok()?)?;
310 slot.1 = it.0;
311 Some((k2, v))
312 });
313 let Some(d) = default else {
314 return Ok(moved);
315 };
316 let dflt = build_coll(d, scope, inputs, derived)?;
318 let own = x.map(|(k, _)| k);
319 let landed = moved.map(|(k, _)| k);
320 let holes = own
321 .concat(&landed.negate())
322 .threshold(|_, m| if *m > 0 { 1 } else { 0 });
323 let holes = holes.map(|k| (k, CellValue::Bool(true)));
324 let filled =
325 align(&holes, &dflt, d.ty.dim.categories.clone()).map(|(k, (_, v))| (k, v));
326 Ok(moved.concat(&filled))
327 }
328
329 PlanNodeKind::Literal(v) => {
330 let cv = CellValue::from_model_value(v)
334 .ok_or_else(|| EngineError::Unsupported("literal has no cell value".into()))?;
335 let empty: CoordKey = Vec::new();
336 let t = <G::Timestamp as timely::progress::Timestamp>::minimum();
337 Ok(vec![((empty, cv), t, 1isize)]
338 .to_stream(scope)
339 .as_collection())
340 }
341 PlanNodeKind::FuncCall { func, args } => {
342 let func = *func;
343 let template: Vec<Option<CellValue>> = args.iter().map(literal_value).collect();
346 let colls: Vec<(usize, &PlanNode)> = args
347 .iter()
348 .enumerate()
349 .filter(|(i, _)| template[*i].is_none())
350 .collect();
351 let fill = move |vals: &[CellValue]| call_with(func, &template, vals);
352 let Some(((_, first), rest)) = colls.split_first() else {
353 return Ok(constant(scope, fill(&[])));
355 };
356 let mut acc = build_coll(first, scope, inputs, derived)?.map(|(k, v)| (k, vec![v]));
357 let mut dim = first.ty.dim.clone();
358 for (_, a) in rest {
359 let c = build_coll(a, scope, inputs, derived)?;
360 let keys: Vec<CategoryId> = dim
361 .categories
362 .iter()
363 .copied()
364 .filter(|c| a.ty.dim.categories.contains(c))
365 .collect();
366 acc = align_with(&acc, &c, keys).map(|(k, (mut vs, v))| {
367 vs.push(v);
368 (k, vs)
369 });
370 dim = dim.union(&a.ty.dim);
371 }
372 Ok(acc.map(move |(k, vs)| (k, fill(&vs))))
373 }
374
375 PlanNodeKind::If {
376 test,
377 then,
378 otherwise,
379 spanning,
380 } => {
381 let t = build_coll(test, scope, inputs, derived)?;
382 let a = build_coll(then, scope, inputs, derived)?;
383 let b = otherwise
384 .as_ref()
385 .map(|o| build_coll(o, scope, inputs, derived))
386 .transpose()?;
387 let dom = match spanning {
390 None => t,
391 Some(i) => {
392 let (node, coll) = if *i == 0 {
393 (then.as_ref(), &a)
394 } else {
395 let node = otherwise.as_deref().expect("spanning else exists");
396 (node, b.as_ref().expect("spanning else exists"))
397 };
398 align(&t, coll, shared_cats(test, node)).map(|(k, (tv, _))| (k, tv))
399 }
400 };
401 let pick = |want: bool, dom: &Coll<G>, branch: &Coll<G>, node: &PlanNode| {
402 let keys = node.ty.dim.categories.clone();
403 let sel = dom.filter(move |(_, v)| *v == CellValue::Bool(want));
404 align(&sel, branch, keys).map(|(k, (_, v))| (k, v))
407 };
408 let mut out = pick(true, &dom, &a, then);
409 if let (Some(b), Some(node)) = (&b, otherwise) {
410 out = out.concat(&pick(false, &dom, b, node));
411 }
412 Ok(out.concat(&dom.filter(|(_, v)| matches!(v, CellValue::Err(_)))))
413 }
414 }
415}
416
417pub(crate) type AggRow<V = CellValue> = ((Vec<u32>, u32), Vec<V>);
420
421pub(crate) struct AggSpec<'p> {
423 pub(crate) ins: &'p [PlanNode],
424 pub(crate) params: &'p [PlanNode],
425 pub(crate) pred: Option<&'p PlanNode>,
426 pub(crate) group_by: &'p [CategoryId],
427 pub(crate) func: FuncId,
428 pub(crate) order: &'p [(CategoryId, Vec<ItemId>)],
429 pub(crate) window: Option<crate::typed::Window>,
430 pub(crate) per_item: bool,
431 pub(crate) into: &'p [(CategoryId, Vec<ItemId>)],
432}
433
434pub(crate) type AggOrder = Vec<(u32, HashMap<u32, u32>)>;
436
437pub(crate) fn agg_order(order: &[(CategoryId, Vec<ItemId>)]) -> AggOrder {
439 order
440 .iter()
441 .map(|(c, items)| {
442 let pos = items
443 .iter()
444 .enumerate()
445 .map(|(i, it)| (it.0, i as u32))
446 .collect();
447 (c.0, pos)
448 })
449 .collect()
450}
451
452pub(crate) fn agg_key<V>(
456 k: &CoordKey,
457 vs: Vec<V>,
458 order: &AggOrder,
459 gb: &[u32],
460) -> (CoordKey, AggRow<V>) {
461 let pos: Vec<u32> = order
462 .iter()
463 .map(|(c, m)| {
464 let item = k.iter().find(|(cc, _)| cc == c).map_or(0, |(_, i)| *i);
465 m.get(&item).copied().unwrap_or(u32::MAX / 2 + item)
467 })
468 .collect();
469 let item = order
470 .first()
471 .and_then(|(c, _)| k.iter().find(|(cc, _)| cc == c))
472 .map_or(0, |(_, i)| *i);
473 (project(k, gb), ((pos, item), vs))
474}
475
476pub(crate) fn agg_rows(
480 input: &[(&AggRow, isize)],
481 n_in: usize,
482 consts: &[Option<CellValue>],
483 capacity: usize,
484) -> (Rows, Vec<CellValue>, Vec<u32>) {
485 let mut rs = Rows {
486 pos: Vec::new(),
487 vals: Vec::new(),
488 capacity,
489 };
490 let mut params: Vec<CellValue> = Vec::new();
491 let mut items = Vec::new();
492 for ((pk, vs), mult) in input {
493 for _ in 0..(*mult).max(0) {
494 rs.pos.push(pk.0.clone());
495 rs.vals.push(vs[..n_in].to_vec());
496 items.push(pk.1);
497 if params.is_empty() {
498 let mut joined = vs[n_in..].iter();
500 params = consts
501 .iter()
502 .map(|c| match c {
503 Some(v) => v.clone(),
504 None => joined.next().cloned().unwrap_or(CellValue::Err(err::NA)),
505 })
506 .collect();
507 }
508 }
509 }
510 if params.is_empty() {
511 params = consts
512 .iter()
513 .map(|c| c.clone().unwrap_or(CellValue::Err(err::NA)))
514 .collect();
515 }
516 (rs, params, items)
517}
518
519pub(crate) fn where_row<V: Clone>(
523 vs: Vec<V>,
524 t: CellValue,
525 err: impl Fn(CellValue) -> V,
526) -> Option<Vec<V>> {
527 match t {
528 CellValue::Bool(true) => Some(vs),
529 CellValue::Err(_) => Some(vec![err(t); vs.len()]),
530 _ => None,
531 }
532}
533
534pub(crate) fn grid_keys(grid: &[(CategoryId, Vec<ItemId>)]) -> Vec<CoordKey> {
536 grid.iter().fold(vec![Vec::new()], |acc, (c, items)| {
537 acc.iter()
538 .flat_map(|pre: &CoordKey| {
539 items.iter().map(move |i| {
540 let mut k = pre.clone();
541 k.push((c.0, i.0));
542 k
543 })
544 })
545 .collect()
546 })
547}
548
549pub(crate) type RemapTable = (Vec<u32>, HashMap<(u32, u32), (u32, u32)>);
551
552pub(crate) fn remap_table(maps: &crate::plan::Remaps) -> RemapTable {
554 let to = maps
555 .iter()
556 .flat_map(|(from, to, items)| items.iter().map(|(i, j)| ((from.0, i.0), (to.0, j.0))))
557 .collect();
558 (maps.iter().map(|(c, _, _)| c.0).collect(), to)
559}
560
561pub(crate) fn remap_key(
563 k: &CoordKey,
564 from: &[u32],
565 to: &HashMap<(u32, u32), (u32, u32)>,
566) -> Option<CoordKey> {
567 let mut k2 = Vec::with_capacity(k.len());
568 for &(c, i) in k {
569 if from.contains(&c) {
570 k2.push(*to.get(&(c, i))?);
571 } else {
572 k2.push((c, i));
573 }
574 }
575 k2.sort_unstable();
576 Some(k2)
577}
578
579pub(crate) fn call_with(
581 func: FuncId,
582 template: &[Option<CellValue>],
583 vals: &[CellValue],
584) -> CellValue {
585 let mut it = vals.iter();
586 let full: Vec<CellValue> = template
587 .iter()
588 .map(|t| match t {
589 Some(v) => v.clone(),
590 None => it.next().expect("one value per collection arg").clone(),
591 })
592 .collect();
593 builtins::eval(func, &full)
594}
595
596fn build_aggregate<G: Scope>(
600 scope: &mut G,
601 inputs: &HashMap<MeasureId, Coll<G>>,
602 derived: &HashMap<MeasureId, Coll<G>>,
603 a: AggSpec<'_>,
604) -> Result<Coll<G>, EngineError>
605where
606 G::Timestamp: differential_dataflow::lattice::Lattice + timely::progress::Timestamp,
607{
608 let (first, rest) = a
609 .ins
610 .split_first()
611 .ok_or_else(|| EngineError::Unsupported("aggregation with no input".into()))?;
612 let full: Vec<CategoryId> = first
613 .ty
614 .dim
615 .categories
616 .iter()
617 .chain(a.order.iter().map(|(c, _)| c))
618 .copied()
619 .collect();
620 let mut rows = build_coll(first, scope, inputs, derived)?.map(|(k, v)| (k, vec![v]));
621 let mut full = full;
624 for n in rest {
625 let c = build_coll(n, scope, inputs, derived)?;
626 let mine: Vec<CategoryId> =
627 n.ty.dim
628 .categories
629 .iter()
630 .chain(a.order.iter().map(|(c, _)| c))
631 .copied()
632 .collect();
633 let shared = mine.iter().copied().filter(|c| full.contains(c)).collect();
634 full.extend(
635 mine.into_iter()
636 .filter(|c| !full.contains(c))
637 .collect::<Vec<_>>(),
638 );
639 rows = align_with(&rows, &c, shared).map(|(k, (mut vs, v))| {
640 vs.push(v);
641 (k, vs)
642 });
643 }
644 let n_in = a.ins.len();
645 if let Some(p) = a.pred {
646 let pc = build_coll(p, scope, inputs, derived)?;
647 rows = align_with(&rows, &pc, p.ty.dim.categories.clone())
648 .flat_map(|(k, (vs, t))| where_row(vs, t, |t| t).map(|vs| (k, vs)));
649 }
650 let mut consts: Vec<Option<CellValue>> = Vec::new();
656 let mut row_dim: Vec<CategoryId> = full.clone();
657 for p in a.params {
658 match literal_value(p) {
659 Some(v) => consts.push(Some(v)),
660 None => {
661 consts.push(None);
662 let pc = build_coll(p, scope, inputs, derived)?;
663 let shared: Vec<CategoryId> =
664 p.ty.dim
665 .categories
666 .iter()
667 .copied()
668 .filter(|c| row_dim.contains(c))
669 .collect();
670 for c in &p.ty.dim.categories {
671 if !row_dim.contains(c) {
672 row_dim.push(*c);
673 }
674 }
675 rows = align_with(&rows, &pc, shared).map(|(k, (mut vs, v))| {
676 vs.push(v);
677 (k, vs)
678 });
679 }
680 }
681 }
682 let order = agg_order(a.order);
683 let capacity: usize = a.order.iter().map(|(_, items)| items.len()).product();
684 let gb: Vec<u32> = a.group_by.iter().map(|c| c.0).collect();
685 let keyed = rows.map(move |(k, vs)| agg_key(&k, vs, &order, &gb));
687 let func = a.func;
688 let to_rows = move |input: &[(&AggRow, isize)]| agg_rows(input, n_in, &consts, capacity);
689 let cat0 = a.order.first().map(|(c, _)| c.0);
690 if !a.into.is_empty() {
691 let cells = grid_keys(a.into);
694 let lists = keyed.reduce(
695 move |_k, input, output: &mut Vec<((CoordKey, CellValue), isize)>| {
696 let (rs, params, _) = to_rows(input);
697 for (cell, v) in cells.iter().zip(reduce::array(func, &rs, ¶ms)) {
698 output.push(((cell.clone(), v), 1isize));
699 }
700 },
701 );
702 return Ok(lists.map(move |(gk, (cell, v))| {
703 let mut key = gk;
704 key.extend(cell);
705 key.sort_unstable();
706 (key, v)
707 }));
708 }
709 if a.window.is_none() && !a.per_item {
710 return Ok(keyed.reduce(move |_k, input, output| {
711 let (rs, params, _) = to_rows(input);
712 if !rs.is_empty() {
713 output.push((reduce::reduce(func, &rs, ¶ms), 1isize));
714 }
715 }));
716 }
717 let window = a.window;
719 let per = keyed.reduce(
720 move |_k, input, output: &mut Vec<((u32, CellValue), isize)>| {
721 let (rs, params, items) = to_rows(input);
722 let outs = match window {
723 None => reduce::per_item(func, &rs, ¶ms),
724 Some(w) => windows(func, &rs, ¶ms, w),
725 };
726 for (item, v) in items.into_iter().zip(outs) {
727 output.push(((item, v), 1isize));
728 }
729 },
730 );
731 let cat0 = cat0.unwrap_or(0);
732 Ok(per.map(move |(gk, (item, v))| {
733 let mut k = gk;
734 k.push((cat0, item));
735 k.sort_unstable();
736 (k, v)
737 }))
738}
739
740pub(crate) fn windows(
743 func: FuncId,
744 rs: &Rows,
745 params: &[CellValue],
746 w: crate::typed::Window,
747) -> Vec<CellValue> {
748 use crate::typed::Bound;
749 let positions: Vec<i64> = rs.pos.iter().map(|p| i64::from(p[0])).collect();
750 let first = positions.iter().copied().min().unwrap_or(0);
752 let last = positions.iter().copied().max().unwrap_or(0);
753 (0..rs.len())
754 .map(|i| {
755 let me = positions[i];
756 let at = |b: Bound| match b {
757 Bound::First => first,
758 Bound::Last => last,
759 Bound::Offset(o) => me + o,
760 };
761 let (lo, hi) = (at(w.0), at(w.1));
762 let pick: Vec<usize> = (0..rs.len())
763 .filter(|j| (lo..=hi).contains(&positions[*j]))
764 .collect();
765 if pick.is_empty() {
766 return CellValue::Err(err::NA);
767 }
768 let sub = Rows {
769 pos: pick.iter().map(|j| rs.pos[*j].clone()).collect(),
770 vals: pick.iter().map(|j| rs.vals[*j].clone()).collect(),
771 capacity: rs.capacity,
772 };
773 reduce::reduce(func, &sub, params)
774 })
775 .collect()
776}
777
778fn constant<G: Scope>(scope: &mut G, v: CellValue) -> Coll<G>
780where
781 G::Timestamp: differential_dataflow::lattice::Lattice + timely::progress::Timestamp,
782{
783 let empty: CoordKey = Vec::new();
784 let t = <G::Timestamp as timely::progress::Timestamp>::minimum();
785 vec![((empty, v), t, 1isize)]
786 .to_stream(scope)
787 .as_collection()
788}
789
790#[allow(clippy::type_complexity)]
795fn align<G: Scope>(
796 l: &Coll<G>,
797 r: &Coll<G>,
798 keys: Vec<CategoryId>,
799) -> Collection<G, (CoordKey, (CellValue, CellValue)), isize>
800where
801 G::Timestamp: differential_dataflow::lattice::Lattice,
802{
803 align_with(l, r, keys)
804}
805
806#[allow(clippy::type_complexity)]
808fn align_with<G: Scope, V>(
809 l: &Collection<G, (CoordKey, V), isize>,
810 r: &Coll<G>,
811 keys: Vec<CategoryId>,
812) -> Collection<G, (CoordKey, (V, CellValue)), isize>
813where
814 G::Timestamp: differential_dataflow::lattice::Lattice,
815 V: differential_dataflow::ExchangeData + std::hash::Hash,
816{
817 let keys: Vec<u32> = keys.iter().map(|c| c.0).collect();
818 let keys2 = keys.clone();
819 let l = l.map(move |(k, v)| (project(&k, &keys), (k, v)));
820 let r = r.map(move |(k, v)| (project(&k, &keys2), (k, v)));
821 l.join(&r)
822 .map(|(_jk, ((lk, lv), (rk, rv)))| (union_keys(&lk, &rk), (lv, rv)))
823}
824
825pub(crate) fn shared_cats(a: &PlanNode, b: &PlanNode) -> Vec<CategoryId> {
827 a.ty.dim
828 .categories
829 .iter()
830 .copied()
831 .filter(|c| b.ty.dim.categories.contains(c))
832 .collect()
833}
834
835pub(crate) fn literal_value(node: &PlanNode) -> Option<CellValue> {
838 match &node.kind {
839 PlanNodeKind::Literal(v) => CellValue::from_model_value(v),
840 _ => None,
841 }
842}
843
844pub(crate) fn apply_binary(op: BinaryOp, a: &CellValue, b: &CellValue) -> CellValue {
845 if let CellValue::Err(_) = a {
847 return a.clone();
848 }
849 if let CellValue::Err(_) = b {
850 return b.clone();
851 }
852 match op {
853 BinaryOp::Eq => return CellValue::Bool(a == b),
854 BinaryOp::Ne => return CellValue::Bool(a != b),
855 BinaryOp::And => return CellValue::Bool(truthy(a) && truthy(b)),
856 BinaryOp::Or => return CellValue::Bool(truthy(a) || truthy(b)),
857 BinaryOp::Concat => {
858 return match (builtins::text_of(a), builtins::text_of(b)) {
859 (Ok(x), Ok(y)) => CellValue::Text(x + &y),
860 (Err(c), _) | (_, Err(c)) => CellValue::Err(c),
861 }
862 }
863 _ => {}
864 }
865 let ord = match (a, b) {
868 (CellValue::Text(x), CellValue::Text(y)) => Some(x.cmp(y)),
869 (CellValue::Date(x), CellValue::Date(y)) => Some(x.cmp(y)),
870 (CellValue::Bool(x), CellValue::Bool(y)) => Some(x.cmp(y)),
871 _ => None,
872 };
873 const DAY_MS: f64 = 86_400_000.0;
875 match (op, a, b) {
876 (BinaryOp::Add | BinaryOp::Sub, CellValue::Date(d), CellValue::Num(_)) => {
877 let days = b.as_num().unwrap_or(f64::NAN);
878 let days = if op == BinaryOp::Sub { -days } else { days };
879 return builtins::date_plus_days(*d, days);
880 }
881 (BinaryOp::Add, CellValue::Num(_), CellValue::Date(d)) => {
882 return builtins::date_plus_days(*d, a.as_num().unwrap_or(f64::NAN));
883 }
884 (BinaryOp::Sub, CellValue::Date(x), CellValue::Date(y)) => {
885 return CellValue::num((x - y) as f64 / DAY_MS);
886 }
887 _ => {}
888 }
889 let (Some(x), Some(y)) = (a.as_num(), b.as_num()) else {
890 return match (op, ord) {
891 (BinaryOp::Lt, Some(o)) => CellValue::Bool(o.is_lt()),
892 (BinaryOp::Le, Some(o)) => CellValue::Bool(o.is_le()),
893 (BinaryOp::Gt, Some(o)) => CellValue::Bool(o.is_gt()),
894 (BinaryOp::Ge, Some(o)) => CellValue::Bool(o.is_ge()),
895 _ => CellValue::Err(err::VALUE),
896 };
897 };
898 match op {
899 BinaryOp::Add => builtins::num(x + y),
900 BinaryOp::Sub => builtins::num(x - y),
901 BinaryOp::Mul => builtins::num(x * y),
902 BinaryOp::Div if y == 0.0 => CellValue::Err(err::DIV0),
903 BinaryOp::Div => builtins::num(x / y),
904 BinaryOp::Pow => builtins::power(x, y).unwrap_or_else(CellValue::Err),
905 BinaryOp::Lt => CellValue::Bool(x < y),
906 BinaryOp::Le => CellValue::Bool(x <= y),
907 BinaryOp::Gt => CellValue::Bool(x > y),
908 BinaryOp::Ge => CellValue::Bool(x >= y),
909 BinaryOp::Eq | BinaryOp::Ne | BinaryOp::And | BinaryOp::Or | BinaryOp::Concat => {
910 unreachable!("handled above")
911 }
912 }
913}
914
915pub(crate) fn apply_unary(op: UnaryOp, v: &CellValue) -> CellValue {
916 match (op, v) {
917 (_, CellValue::Err(_)) => v.clone(),
918 (UnaryOp::Neg, _) => match v.as_num() {
919 Some(x) => builtins::num(-x),
920 None => CellValue::Err(err::VALUE),
921 },
922 (UnaryOp::Not, _) => CellValue::Bool(!truthy(v)),
923 }
924}
925
926fn truthy(v: &CellValue) -> bool {
929 match v {
930 CellValue::Bool(b) => *b,
931 CellValue::Num(bits) => f64::from_bits(*bits) != 0.0,
932 CellValue::Text(t) => !t.is_empty(),
933 CellValue::Date(ms) => *ms != 0,
934 CellValue::Err(_) => false,
935 }
936}
937
938pub(crate) fn project(k: &CoordKey, keep: &[u32]) -> CoordKey {
941 improv_verified::keys::project(k, keep)
942}
943
944pub(crate) fn union_keys(a: &CoordKey, b: &CoordKey) -> CoordKey {
948 improv_verified::keys::union_keys(a, b)
949}
950
951pub(crate) fn derived_build_order<'m>(
955 model: &'m Model,
956 targets: &[MeasureId],
957) -> Result<Vec<(MeasureId, &'m Formula)>, EngineError> {
958 let derived: Vec<(MeasureId, &'m Formula)> = model
963 .measures
964 .iter()
965 .filter_map(|(id, m)| match &m.kind {
966 MeasureKind::Derived(f) => Some((*id, f)),
967 MeasureKind::Input => None,
968 })
969 .collect();
970 let index: HashMap<MeasureId, usize> = derived
971 .iter()
972 .enumerate()
973 .map(|(i, (id, _))| (*id, i))
974 .collect();
975 let graph: Vec<Vec<usize>> = derived
976 .iter()
977 .map(|(id, f)| {
978 f.referenced_measures()
979 .iter()
980 .filter(|d| *d != id)
981 .filter_map(|d| index.get(d).copied())
982 .collect()
983 })
984 .collect();
985 let roots: Vec<usize> = targets
986 .iter()
987 .filter_map(|t| index.get(t).copied())
988 .collect();
989 match improv_verified::graph::build_order(&graph, &roots) {
992 Ok(order) => Ok(order.into_iter().map(|i| derived[i]).collect()),
993 Err(i) => Err(EngineError::Unsupported(format!(
994 "cyclic measure dependency at {:?}",
995 derived[i].0
996 ))),
997 }
998}
999
1000pub(crate) fn compile_plans(
1002 model: &Model,
1003 targets: &[MeasureId],
1004) -> Result<Vec<(MeasureId, PlanNode)>, EngineError> {
1005 let ctx = CompileContext::for_model(model);
1006 derived_build_order(model, targets)?
1007 .into_iter()
1008 .map(|(m, f)| Ok((m, compile_formula(&ctx, m, f)?)))
1009 .collect()
1010}
1011
1012pub(crate) fn build_derived<G: Scope>(
1016 scope: &mut G,
1017 plans: &[(MeasureId, PlanNode)],
1018 inputs: &HashMap<MeasureId, Coll<G>>,
1019) -> Vec<(MeasureId, Coll<G>)>
1020where
1021 G::Timestamp: differential_dataflow::lattice::Lattice + timely::progress::Timestamp,
1022{
1023 let mut derived = HashMap::new();
1024 let mut built = Vec::new();
1025 for (mid, plan) in plans {
1026 let Ok(coll) = build_coll(plan, scope, inputs, &derived) else {
1027 continue;
1028 };
1029 let reduced = coll.reduce(|_k, inp, out| {
1030 for (val, mult) in inp {
1031 if *mult > 0 {
1032 out.push(((*val).clone(), 1isize));
1033 }
1034 }
1035 });
1036 derived.insert(*mid, reduced.clone());
1037 built.push((*mid, reduced));
1038 }
1039 built
1040}
1041
1042pub fn evaluate(
1050 model: &Model,
1051 targets: &[MeasureId],
1052) -> Result<HashMap<MeasureId, HashMap<CoordKey, CellValue>>, EngineError> {
1053 let plans = compile_plans(model, targets)?;
1056 if plans
1059 .iter()
1060 .any(|(_, p)| matches!(p.kind, PlanNodeKind::Fixpoint { .. }))
1061 {
1062 let (_engine, snap) = crate::session::Engine::new(model, targets)?;
1063 return Ok(snap.iter().map(|(m, v)| (*m, (**v).clone())).collect());
1064 }
1065
1066 let mut input_cells: HashMap<MeasureId, Vec<(CoordKey, CellValue)>> = HashMap::new();
1070 for ((mid, coord), val) in &model.inputs {
1071 if let Some(cv) = CellValue::from_model_value(val) {
1072 input_cells
1073 .entry(*mid)
1074 .or_default()
1075 .push((encode_coord(coord), cv));
1076 }
1077 }
1078
1079 let results: Arc<Mutex<HashMap<MeasureId, HashMap<CoordKey, CellValue>>>> =
1080 Arc::new(Mutex::new(HashMap::new()));
1081 let results2 = results.clone();
1082
1083 let input_ids: Vec<MeasureId> = model
1086 .measures
1087 .values()
1088 .filter(|m| matches!(m.kind, MeasureKind::Input))
1089 .map(|m| m.id)
1090 .collect();
1091 let plans_arc = plans.clone();
1092
1093 timely::execute::execute_directly(move |worker| {
1094 let mut sessions: HashMap<MeasureId, InputSession<u64, (CoordKey, CellValue), isize>> =
1095 HashMap::new();
1096 let res = results2.clone();
1097
1098 worker.dataflow::<u64, _, _>(|scope| {
1099 let mut input_colls: HashMap<MeasureId, Coll<_>> = HashMap::new();
1100 for id in &input_ids {
1101 let mut session = InputSession::new();
1102 let coll = session.to_collection(scope);
1103 sessions.insert(*id, session);
1104 input_colls.insert(*id, coll);
1105 }
1106
1107 for (mid, reduced) in build_derived(scope, &plans_arc, &input_colls) {
1108 let res = res.clone();
1109 reduced.inspect(move |((k, val), _t, diff)| {
1110 if *diff > 0 {
1111 res.lock()
1112 .unwrap()
1113 .entry(mid)
1114 .or_default()
1115 .insert(k.clone(), val.clone());
1116 }
1117 });
1118 }
1119 });
1120
1121 for (id, cells) in &input_cells {
1123 let session = sessions.get_mut(id).expect("a session per input measure");
1124 for (k, v) in cells {
1125 session.insert((k.clone(), v.clone()));
1126 }
1127 }
1128 for session in sessions.values_mut() {
1131 session.advance_to(1);
1132 session.flush();
1133 }
1134 });
1135
1136 let out = results.lock().unwrap().clone();
1137 Ok(out)
1138}
1139
1140#[cfg(test)]
1141#[cfg_attr(coverage_nightly, coverage(off))]
1142mod tests {
1143 use super::*;
1144 use crate::decode_coord;
1145 use crate::typed::{Dim, TypeInfo};
1146 use improv_core_model::{
1147 Coordinate, DimensionSpec, Expr, ItemId, Measure, MeasureKind, Name, Value, ValueType,
1148 };
1149
1150 fn bcast_model(
1155 a_cats: &[u32],
1156 a: &[(Vec<u32>, f64)],
1157 b_cats: &[u32],
1158 b: &[(Vec<u32>, f64)],
1159 op: BinaryOp,
1160 a_left: bool,
1161 ) -> Model {
1162 let mut m = Model::new();
1163 for c in 1..=3u32 {
1164 m.add_category(CategoryId(c), format!("C{c}"));
1165 for i in 0..3u32 {
1166 m.add_item(ItemId(10 * c + i), CategoryId(c), format!("I{c}_{i}"));
1167 }
1168 }
1169 let cats = |cs: &[u32]| cs.iter().map(|c| CategoryId(*c)).collect::<Vec<_>>();
1170 let mut union: Vec<u32> = a_cats.iter().chain(b_cats).copied().collect();
1171 union.sort_unstable();
1172 union.dedup();
1173 let input = |id: u32, name: &str, cs: &[u32]| Measure {
1174 id: MeasureId(id),
1175 name: Name(name.into()),
1176 value_type: ValueType::Number,
1177 categories: cats(cs),
1178 kind: MeasureKind::Input,
1179 description: None,
1180 };
1181 m.add_measure(input(100, "A", a_cats));
1182 m.add_measure(input(101, "B", b_cats));
1183 let r = |id: u32| Box::new(Expr::Ref(MeasureId(id), DimensionSpec::default()));
1184 let (l, rr) = if a_left {
1185 (r(100), r(101))
1186 } else {
1187 (r(101), r(100))
1188 };
1189 m.add_measure(Measure {
1190 id: MeasureId(102),
1191 name: Name("R".into()),
1192 value_type: ValueType::Number,
1193 categories: cats(&union),
1194 kind: MeasureKind::Derived(improv_core_model::Formula::new(Expr::BinaryOp(op, l, rr))),
1195 description: None,
1196 });
1197 for (id, cs, cells) in [(100, a_cats, a), (101, b_cats, b)] {
1198 for (items, v) in cells {
1199 let coord = Coordinate::from_pairs(
1200 cs.iter()
1201 .zip(items)
1202 .map(|(c, i)| (CategoryId(*c), ItemId(*i))),
1203 );
1204 m.set_input(MeasureId(id), coord, Value::Number(*v));
1205 }
1206 }
1207 m
1208 }
1209
1210 fn eval_r(m: &Model) -> BTreeMap<CoordKey, f64> {
1212 let out = evaluate(m, &[MeasureId(102)]).expect("evaluate");
1213 out.get(&MeasureId(102))
1214 .map(|r| {
1215 r.iter()
1216 .map(|(k, v)| (k.clone(), v.as_num().unwrap()))
1217 .collect()
1218 })
1219 .unwrap_or_default()
1220 }
1221
1222 #[allow(clippy::type_complexity)]
1224 fn price_discount() -> (
1225 (Vec<u32>, Vec<(Vec<u32>, f64)>),
1226 (Vec<u32>, Vec<(Vec<u32>, f64)>),
1227 ) {
1228 (
1229 (
1230 vec![1, 2],
1231 vec![
1232 (vec![10, 20], 100.0),
1233 (vec![10, 21], 50.0),
1234 (vec![11, 20], 120.0),
1235 ],
1236 ),
1237 (vec![2], vec![(vec![20], 10.0), (vec![21], 5.0)]),
1238 )
1239 }
1240
1241 #[test]
1244 fn broadcast_larger_left_and_larger_right() {
1245 let ((pc, p), (dc, d)) = price_discount();
1246 let at = [
1247 key(&[(1, 10), (2, 20)]),
1248 key(&[(1, 10), (2, 21)]),
1249 key(&[(1, 11), (2, 20)]),
1250 ];
1251 let cases: [(BinaryOp, [f64; 3], [f64; 3]); 4] = [
1252 (BinaryOp::Add, [110.0, 55.0, 130.0], [110.0, 55.0, 130.0]),
1253 (BinaryOp::Sub, [90.0, 45.0, 110.0], [-90.0, -45.0, -110.0]),
1254 (
1255 BinaryOp::Mul,
1256 [1000.0, 250.0, 1200.0],
1257 [1000.0, 250.0, 1200.0],
1258 ),
1259 (
1260 BinaryOp::Div,
1261 [10.0, 10.0, 12.0],
1262 [10.0 / 100.0, 5.0 / 50.0, 10.0 / 120.0],
1263 ),
1264 ];
1265 for (op, larger_left, larger_right) in cases {
1266 for (a_left, want) in [(true, larger_left), (false, larger_right)] {
1267 let got = eval_r(&bcast_model(&pc, &p, &dc, &d, op, a_left));
1268 let want: BTreeMap<CoordKey, f64> = at.iter().cloned().zip(want).collect();
1269 assert_eq!(got, want, "{op:?}, price on the left: {a_left}");
1270 }
1271 }
1272 }
1273
1274 fn run_plan(
1276 plan: PlanNode,
1277 cells: Vec<(MeasureId, Vec<(CoordKey, f64)>)>,
1278 ) -> BTreeMap<CoordKey, f64> {
1279 let out = Arc::new(Mutex::new(BTreeMap::new()));
1280 let out2 = out.clone();
1281 timely::execute::execute_directly(move |worker| {
1282 let mut sessions = Vec::new();
1283 worker.dataflow::<u64, _, _>(|scope| {
1284 let mut inputs = HashMap::new();
1285 for (mid, _) in &cells {
1286 let mut session = InputSession::new();
1287 inputs.insert(*mid, session.to_collection(scope));
1288 sessions.push(session);
1289 }
1290 for (_, coll) in build_derived(scope, &[(MeasureId(102), plan)], &inputs) {
1291 let out = out2.clone();
1292 coll.inspect(move |((k, v), _t, d)| {
1293 if *d > 0 {
1294 out.lock().unwrap().insert(k.clone(), v.as_num().unwrap());
1295 }
1296 });
1297 }
1298 });
1299 for ((_, cs), session) in cells.iter().zip(&mut sessions) {
1300 for (k, v) in cs {
1301 session.insert((k.clone(), CellValue::num(*v)));
1302 }
1303 session.advance_to(1);
1304 session.flush();
1305 }
1306 });
1307 let got = out.lock().unwrap().clone();
1308 got
1309 }
1310
1311 #[test]
1316 fn broadcast_disjoint_extra_dims() {
1317 let a = vec![
1318 (vec![10, 20], 2.0),
1319 (vec![11, 20], 3.0),
1320 (vec![10, 21], 5.0),
1321 ];
1322 let b = vec![
1323 (vec![20, 30], 7.0),
1324 (vec![20, 31], 11.0),
1325 (vec![21, 30], 13.0),
1326 ];
1327 let want: BTreeMap<CoordKey, f64> = [
1328 (key(&[(1, 10), (2, 20), (3, 30)]), 14.0),
1329 (key(&[(1, 10), (2, 20), (3, 31)]), 22.0),
1330 (key(&[(1, 11), (2, 20), (3, 30)]), 21.0),
1331 (key(&[(1, 11), (2, 20), (3, 31)]), 33.0),
1332 (key(&[(1, 10), (2, 21), (3, 30)]), 65.0),
1333 ]
1334 .into_iter()
1335 .collect();
1336 let cells = |cs: [u32; 2], v: &[(Vec<u32>, f64)]| {
1337 let pairs = |items: &[u32]| {
1338 cs.iter()
1339 .copied()
1340 .zip(items.iter().copied())
1341 .collect::<Vec<_>>()
1342 };
1343 v.iter()
1344 .map(|(items, x)| (key(&pairs(items)), *x))
1345 .collect::<Vec<_>>()
1346 };
1347 let input = vec![
1348 (MeasureId(100), cells([1, 2], &a)),
1349 (MeasureId(101), cells([2, 3], &b)),
1350 ];
1351 let ty = |cs: &[u32]| TypeInfo {
1352 value_type: ValueType::Number,
1353 dim: Dim::of(cs.iter().map(|c| CategoryId(*c)).collect()),
1354 };
1355 let leaf = |id: u32, cs: &[u32]| PlanNode {
1356 kind: PlanNodeKind::InputMeasure(MeasureId(id)),
1357 ty: ty(cs),
1358 };
1359 let mul = |l: PlanNode, r: PlanNode| PlanNode {
1360 kind: PlanNodeKind::MapBinary(BinaryOp::Mul, Box::new(l), Box::new(r)),
1361 ty: ty(&[1, 2, 3]),
1362 };
1363 let (pa, pb) = (leaf(100, &[1, 2]), leaf(101, &[2, 3]));
1364 let joined = PlanNode {
1365 kind: PlanNodeKind::Join {
1366 left: Box::new(pa.clone()),
1367 right: Box::new(pb.clone()),
1368 join_keys: vec![CategoryId(2)],
1369 },
1370 ty: ty(&[1, 2, 3]),
1371 };
1372 for plan in [
1373 mul(pa.clone(), pb.clone()),
1374 mul(pb.clone(), pa),
1375 mul(joined, pb),
1376 ] {
1377 assert_eq!(run_plan(plan.clone(), input.clone()), want, "{plan:?}");
1378 }
1379 let m = bcast_model(&[1, 2], &a, &[2, 3], &b, BinaryOp::Mul, true);
1382 let got: BTreeMap<CoordKey, f64> = evaluate(&m, &[MeasureId(102)]).unwrap()
1383 [&MeasureId(102)]
1384 .iter()
1385 .map(|(k, v)| (k.clone(), v.as_num().unwrap()))
1386 .collect();
1387 assert_eq!(got, want);
1388 let m = bcast_model(
1390 &[1],
1391 &[(vec![10], 1.0)],
1392 &[3],
1393 &[(vec![30], 2.0)],
1394 BinaryOp::Mul,
1395 true,
1396 );
1397 assert!(matches!(
1398 evaluate(&m, &[MeasureId(102)]),
1399 Err(EngineError::Compile(
1400 crate::compiler::CompileError::DimensionMismatch(_)
1401 ))
1402 ));
1403 }
1404
1405 #[test]
1408 fn broadcast_session_matches_evaluate() {
1409 let snap_r = |snap: &crate::session::Snapshot| -> BTreeMap<CoordKey, f64> {
1410 snap.get(&MeasureId(102))
1411 .map(|r| {
1412 r.iter()
1413 .map(|(k, v)| (k.clone(), v.as_num().unwrap()))
1414 .collect()
1415 })
1416 .unwrap_or_default()
1417 };
1418 let ((pc, p), (dc, d)) = price_discount();
1419 for op in [BinaryOp::Add, BinaryOp::Sub, BinaryOp::Mul, BinaryOp::Div] {
1420 for a_left in [true, false] {
1421 let mut m = bcast_model(&pc, &p, &dc, &d, op, a_left);
1422 let (mut engine, snap) =
1423 crate::session::Engine::new(&m, &[MeasureId(102)]).expect("engine");
1424 assert_eq!(snap_r(&snap), eval_r(&m), "initial {op:?} {a_left}");
1425 assert_eq!(snap_r(&snap).len(), 3);
1426 engine
1428 .set(MeasureId(101), key(&[(2, 21)]), 4.0)
1429 .expect("edit");
1430 let snap = engine
1431 .set(MeasureId(100), key(&[(1, 11), (2, 21)]), 7.0)
1432 .expect("edit");
1433 let c = |pairs: &[(u32, u32)]| decode_coord(&key(pairs));
1434 m.set_input(MeasureId(101), c(&[(2, 21)]), Value::Number(4.0));
1435 m.set_input(MeasureId(100), c(&[(1, 11), (2, 21)]), Value::Number(7.0));
1436 assert_eq!(snap_r(&snap), eval_r(&m), "edited {op:?} {a_left}");
1437 if (op, a_left) == (BinaryOp::Sub, true) {
1438 let want: BTreeMap<CoordKey, f64> = [
1439 (key(&[(1, 10), (2, 20)]), 90.0),
1440 (key(&[(1, 10), (2, 21)]), 46.0),
1441 (key(&[(1, 11), (2, 20)]), 110.0),
1442 (key(&[(1, 11), (2, 21)]), 3.0),
1443 ]
1444 .into_iter()
1445 .collect();
1446 assert_eq!(snap_r(&snap), want);
1447 }
1448 }
1449 }
1450 }
1451
1452 fn coords(cats: &[u32]) -> Vec<Vec<u32>> {
1454 cats.iter().fold(vec![vec![]], |acc, c| {
1455 acc.into_iter()
1456 .flat_map(|pre| {
1457 (0..2).map(move |i| {
1458 let mut v = pre.clone();
1459 v.push(10 * c + i);
1460 v
1461 })
1462 })
1463 .collect()
1464 })
1465 }
1466
1467 proptest::proptest! {
1468 #[test]
1472 fn broadcast_matches_naive_reference(
1473 a_mask in 0u8..8,
1474 b_mask in 0u8..8,
1475 a_vals in proptest::collection::vec(proptest::option::of(1i32..=9), 8),
1476 b_vals in proptest::collection::vec(proptest::option::of(-9i32..=-1), 8),
1477 op_ix in 0usize..4,
1478 a_left: bool,
1479 ) {
1480 let op = [BinaryOp::Add, BinaryOp::Sub, BinaryOp::Mul, BinaryOp::Div][op_ix];
1481 let cats = |mask: u8| (1..=3u32).filter(|c| mask & (1 << (c - 1)) != 0).collect::<Vec<_>>();
1482 let (ac, bc) = (cats(a_mask), cats(b_mask));
1483 let cells = |cs: &[u32], vals: &[Option<i32>]| -> Vec<(Vec<u32>, f64)> {
1484 coords(cs).into_iter().zip(vals).filter_map(|(c, v)| v.map(|v| (c, f64::from(v)))).collect()
1485 };
1486 let (a, b) = (cells(&ac, &a_vals), cells(&bc, &b_vals));
1487 let m = bcast_model(&ac, &a, &bc, &b, op, a_left);
1488 let subset = |x: u8, y: u8| x & y == x;
1490 if !subset(a_mask, b_mask) && !subset(b_mask, a_mask) && a_mask & b_mask == 0 {
1491 proptest::prop_assert!(evaluate(&m, &[MeasureId(102)]).is_err());
1492 return Ok(());
1493 }
1494 let union = cats(a_mask | b_mask);
1495 let lookup = |cs: &[u32], cells: &[(Vec<u32>, f64)], u: &[u32]| {
1496 let want: Vec<u32> = union.iter().zip(u).filter(|(c, _)| cs.contains(c)).map(|(_, i)| *i).collect();
1497 cells.iter().find(|(c, _)| *c == want).map(|(_, v)| *v)
1498 };
1499 let mut reference = BTreeMap::new();
1500 for u in coords(&union) {
1501 let (Some(x), Some(y)) = (lookup(&ac, &a, &u), lookup(&bc, &b, &u)) else { continue };
1502 let (x, y) = if a_left { (x, y) } else { (y, x) };
1503 let v = match op {
1504 BinaryOp::Add => x + y,
1505 BinaryOp::Sub => x - y,
1506 BinaryOp::Mul => x * y,
1507 _ => x / y,
1508 };
1509 let k: Vec<(u32, u32)> = union.iter().copied().zip(u).collect();
1510 reference.insert(key(&k), v);
1511 }
1512 proptest::prop_assert_eq!(eval_r(&m), reference);
1513 }
1514 }
1515
1516 fn revenue_model() -> Model {
1518 let mut m = Model::new();
1519 let (time, product) = (CategoryId(1), CategoryId(2));
1520 m.add_category(time, "Time");
1521 m.add_category(product, "Product");
1522 m.add_item(ItemId(10), time, "2025");
1523 m.add_item(ItemId(11), time, "2026");
1524 m.add_item(ItemId(20), product, "WidgetA");
1525 m.add_item(ItemId(21), product, "WidgetB");
1526
1527 m.add_measure(Measure {
1529 id: MeasureId(100),
1530 name: Name("Price".into()),
1531 value_type: ValueType::Number,
1532 categories: vec![product],
1533 kind: MeasureKind::Input,
1534 description: None,
1535 });
1536 m.add_measure(Measure {
1538 id: MeasureId(101),
1539 name: Name("Quantity".into()),
1540 value_type: ValueType::Number,
1541 categories: vec![time, product],
1542 kind: MeasureKind::Input,
1543 description: None,
1544 });
1545 m.add_measure(Measure {
1547 id: MeasureId(102),
1548 name: Name("Revenue".into()),
1549 value_type: ValueType::Number,
1550 categories: vec![time, product],
1551 kind: MeasureKind::Derived(improv_core_model::Formula::new(Expr::BinaryOp(
1552 BinaryOp::Mul,
1553 Box::new(Expr::Ref(MeasureId(100), DimensionSpec::default())),
1554 Box::new(Expr::Ref(MeasureId(101), DimensionSpec::default())),
1555 ))),
1556 description: None,
1557 });
1558
1559 let coord = |pairs: &[(CategoryId, ItemId)]| Coordinate::from_pairs(pairs.iter().copied());
1560 m.set_input(
1562 MeasureId(100),
1563 coord(&[(product, ItemId(20))]),
1564 Value::Number(10.0),
1565 );
1566 m.set_input(
1567 MeasureId(100),
1568 coord(&[(product, ItemId(21))]),
1569 Value::Number(20.0),
1570 );
1571 m.set_input(
1573 MeasureId(101),
1574 coord(&[(time, ItemId(10)), (product, ItemId(20))]),
1575 Value::Number(100.0),
1576 );
1577 m.set_input(
1578 MeasureId(101),
1579 coord(&[(time, ItemId(10)), (product, ItemId(21))]),
1580 Value::Number(50.0),
1581 );
1582 m.set_input(
1583 MeasureId(101),
1584 coord(&[(time, ItemId(11)), (product, ItemId(20))]),
1585 Value::Number(120.0),
1586 );
1587 m.set_input(
1588 MeasureId(101),
1589 coord(&[(time, ItemId(11)), (product, ItemId(21))]),
1590 Value::Number(80.0),
1591 );
1592 m
1593 }
1594
1595 fn define(m: &mut Model, id: u32, cats: Vec<CategoryId>, text: &str) {
1597 let f = improv_core_model::parse_formula(m, text).expect("parses");
1598 let vt = crate::compiler::infer(
1599 &CompileContext::for_model(m),
1600 MeasureId(id),
1601 &f.formula.expr,
1602 )
1603 .expect("compiles")
1604 .ty
1605 .value_type;
1606 m.add_measure(Measure {
1607 id: MeasureId(id),
1608 name: f.target,
1609 value_type: vt,
1610 categories: cats,
1611 kind: MeasureKind::Derived(f.formula),
1612 description: None,
1613 });
1614 }
1615
1616 fn people() -> Model {
1620 let mut m = Model::new();
1621 let (person, year) = (CategoryId(1), CategoryId(2));
1622 m.add_category(person, "Person");
1623 m.add_category(year, "Year");
1624 for i in 1..=4 {
1625 m.add_item(ItemId(10 + i), person, format!("p{i}"));
1626 }
1627 m.add_item(ItemId(21), year, "2025");
1628 m.add_item(ItemId(22), year, "2026");
1629 for (id, name, cats) in [
1630 (100, "Authored", vec![person]),
1631 (101, "Vol", vec![person]),
1632 (102, "Mail", vec![person, year]),
1633 ] {
1634 m.add_measure(Measure {
1635 id: MeasureId(id),
1636 name: Name(name.into()),
1637 value_type: ValueType::Number,
1638 categories: cats,
1639 kind: MeasureKind::Input,
1640 description: None,
1641 });
1642 }
1643 let at = |p: u32| Coordinate::from_pairs([(person, ItemId(10 + p))]);
1644 m.set_input(MeasureId(100), at(1), Value::Number(300.0));
1645 m.set_input(MeasureId(100), at(2), Value::Number(2.0));
1646 for p in 1..=4 {
1647 m.set_input(MeasureId(101), at(p), Value::Number(f64::from(p) * 10.0));
1648 }
1649 let py =
1650 |p: u32, y: u32| Coordinate::from_pairs([(person, ItemId(10 + p)), (year, ItemId(y))]);
1651 m.set_input(MeasureId(102), py(1, 21), Value::Number(5.0));
1652 m.set_input(MeasureId(102), py(3, 22), Value::Number(7.0));
1653 m
1654 }
1655
1656 #[test]
1657 fn coalesce_and_fill_give_every_person_a_cell() {
1658 let (person, year) = (CategoryId(1), CategoryId(2));
1659 let mut m = people();
1660 define(&mut m, 200, vec![person], "C = COALESCE(Authored, Vol*0)");
1661 define(&mut m, 201, vec![person], "F = FILL(Authored, 0)");
1662 define(&mut m, 202, vec![person], "N = IFNA(Authored, 0)");
1663 define(
1664 &mut m,
1665 203,
1666 vec![person],
1667 "Merit = 0.3 * Vol + 0.25 * FILL(Authored, 0)",
1668 );
1669 define(
1671 &mut m,
1672 204,
1673 vec![person],
1674 "D = COALESCE(Authored, IF(Vol > 25, Vol), -1)",
1675 );
1676 define(&mut m, 205, vec![person, year], "Y = COALESCE(Mail, Vol)");
1679 define(&mut m, 206, vec![person, year], "G = FILL(Mail, Vol)");
1680 define(&mut m, 207, vec![person, year], "Z = FILL(Mail, 0)");
1681 let out = evaluate(&m, &(200..=207).map(MeasureId).collect::<Vec<_>>()).unwrap();
1682 let col = |id: u32| {
1683 (1..=4)
1684 .map(|p| {
1685 out[&MeasureId(id)]
1686 .get(&key(&[(1, 10 + p)]))
1687 .and_then(CellValue::as_num)
1688 })
1689 .collect::<Vec<_>>()
1690 };
1691 let all = |v: &[f64]| v.iter().map(|x| Some(*x)).collect::<Vec<_>>();
1692 assert_eq!(col(200), all(&[300.0, 2.0, 0.0, 0.0]));
1693 assert_eq!(col(201), all(&[300.0, 2.0, 0.0, 0.0]));
1694 assert_eq!(col(202), [Some(300.0), Some(2.0), None, None]);
1695 assert_eq!(col(203), all(&[78.0, 6.5, 9.0, 12.0]));
1696 assert_eq!(col(204), all(&[300.0, 2.0, 30.0, 40.0]));
1697 let py = |id: u32, p: u32, y: u32| {
1698 out[&MeasureId(id)]
1699 .get(&key(&[(1, 10 + p), (2, y)]))
1700 .and_then(CellValue::as_num)
1701 };
1702 assert_eq!(out[&MeasureId(205)].len(), 2);
1703 assert_eq!(py(205, 1, 21), Some(5.0));
1704 assert_eq!(out[&MeasureId(206)].len(), 8);
1705 assert_eq!(
1706 (py(206, 1, 21), py(206, 1, 22), py(206, 4, 21)),
1707 (Some(5.0), Some(10.0), Some(40.0))
1708 );
1709 assert_eq!(out[&MeasureId(207)].len(), 8);
1710 assert_eq!((py(207, 3, 22), py(207, 2, 21)), (Some(7.0), Some(0.0)));
1711 }
1712
1713 #[test]
1714 fn coalesce_and_fill_type_and_dimension_rules() {
1715 let m = people();
1716 let ctx = CompileContext::for_model(&m);
1717 let ty = |text: &str| {
1718 let f = improv_core_model::parse_formula(&m, text).expect("parses");
1719 crate::compiler::infer(&ctx, MeasureId(300), &f.formula.expr).map(|t| t.ty)
1720 };
1721 assert_eq!(
1723 ty("X = COALESCE(Authored, NA())").unwrap().value_type,
1724 ValueType::Number
1725 );
1726 assert!(ty("X = COALESCE(Authored, \"none\")").is_err());
1727 assert!(ty("X = FILL(Authored, Mail)").is_err());
1729 let e = ty("X = COALESCE(SUM(Mail OVER Person), Authored)");
1731 assert!(
1732 matches!(e, Err(crate::compiler::CompileError::DimensionMismatch(_))),
1733 "{e:?}"
1734 );
1735 let f = improv_core_model::parse_formula(&m, "X = FILL(Authored, 0)").unwrap();
1737 let bare = CompileContext::new(&m.measures);
1738 assert!(crate::compiler::infer(&bare, MeasureId(300), &f.formula.expr).is_err());
1739 let env: HashMap<MeasureId, CellValue> = [(MeasureId(101), CellValue::num(4.0))].into();
1741 let c = improv_core_model::parse_formula(&m, "X = COALESCE(Authored, Vol, 1)").unwrap();
1742 assert_eq!(
1743 crate::scalar::eval(&c.formula.expr, &env),
1744 Ok(Some(CellValue::num(4.0)))
1745 );
1746 }
1747
1748 #[test]
1751 fn coalesce_follows_edits() {
1752 let person = CategoryId(1);
1753 let mut m = people();
1754 define(&mut m, 200, vec![person], "F = FILL(Authored, 0)");
1755 let (mut eng, snap) = crate::session::Engine::new(&m, &[MeasureId(200)]).unwrap();
1756 let k3 = key(&[(1, 13)]);
1757 assert_eq!(snap[&MeasureId(200)].get(&k3), Some(&CellValue::num(0.0)));
1758 let snap = eng.set(MeasureId(100), k3.clone(), 9.0).unwrap();
1759 assert_eq!(snap[&MeasureId(200)].get(&k3), Some(&CellValue::num(9.0)));
1760 let snap = eng.clear(MeasureId(100), key(&[(1, 11)])).unwrap();
1761 assert_eq!(
1762 snap[&MeasureId(200)].get(&key(&[(1, 11)])),
1763 Some(&CellValue::num(0.0))
1764 );
1765 assert_eq!(snap[&MeasureId(200)].len(), 4);
1766 m.add_item(ItemId(15), person, "p5");
1769 let (_, snap) = crate::session::Engine::new(&m, &[MeasureId(200)]).unwrap();
1770 assert_eq!(
1771 snap[&MeasureId(200)].get(&key(&[(1, 15)])),
1772 Some(&CellValue::num(0.0))
1773 );
1774 assert_eq!(snap[&MeasureId(200)].len(), 5);
1775 }
1776
1777 #[test]
1782 fn countunique_and_dense_rank_follow_deletes() {
1783 let person = CategoryId(1);
1784 let mut m = people();
1785 let at = |p: u32| Coordinate::from_pairs([(person, ItemId(10 + p))]);
1787 m.set_input(MeasureId(101), at(3), Value::Number(20.0));
1788 define(&mut m, 200, vec![], "U = COUNTUNIQUE(Vol OVER Person)");
1789 define(&mut m, 201, vec![person], "D = RANK.DENSE(Vol OVER Person)");
1790 let ids = [MeasureId(200), MeasureId(201)];
1791 let (mut eng, snap) = crate::session::Engine::new(&m, &ids).unwrap();
1792 let u = |s: &crate::session::Snapshot| s[&MeasureId(200)][&Vec::new()].clone();
1793 let d = |s: &crate::session::Snapshot| {
1794 (1..=4)
1795 .map(|p| s[&MeasureId(201)].get(&key(&[(1, 10 + p)])).cloned())
1796 .collect::<Vec<_>>()
1797 };
1798 let n = |x: f64| Some(CellValue::num(x));
1799 assert_eq!(u(&snap), CellValue::num(3.0));
1800 assert_eq!(d(&snap), [n(3.0), n(2.0), n(2.0), n(1.0)]);
1801 let snap = eng.clear(MeasureId(101), key(&[(1, 12)])).unwrap();
1803 assert_eq!(u(&snap), CellValue::num(3.0));
1804 assert_eq!(d(&snap), [n(3.0), None, n(2.0), n(1.0)]);
1805 m.inputs.remove(&(MeasureId(101), at(2)));
1806 let fresh = evaluate(&m, &ids).unwrap();
1807 assert_eq!(fresh[&MeasureId(200)][&Vec::new()], CellValue::num(3.0));
1808 let snap = eng.clear(MeasureId(101), key(&[(1, 13)])).unwrap();
1810 assert_eq!(u(&snap), CellValue::num(2.0));
1811 assert_eq!(d(&snap), [n(2.0), None, None, n(1.0)]);
1812 let snap = eng.set(MeasureId(101), key(&[(1, 12)]), 40.0).unwrap();
1814 assert_eq!(u(&snap), CellValue::num(2.0));
1815 assert_eq!(d(&snap), [n(2.0), n(1.0), None, n(1.0)]);
1816
1817 let mut cells: BTreeMap<u32, u64> = [(1, 10), (4, 40), (2, 40)].into();
1820 let mut seed = 0x2545_f491_4f6c_dd1du64;
1821 let mut rand = |n: u64| {
1822 seed ^= seed << 13;
1823 seed ^= seed >> 7;
1824 seed ^= seed << 17;
1825 seed % n
1826 };
1827 for _ in 0..60 {
1828 let mut batch = Vec::new();
1829 for _ in 0..1 + rand(3) {
1830 let p = 1 + rand(4) as u32;
1831 let value = (rand(3) > 0).then(|| rand(4));
1832 match value {
1833 Some(v) => cells.insert(p, v),
1834 None => cells.remove(&p),
1835 };
1836 batch.push(crate::session::Edit {
1837 measure: MeasureId(101),
1838 coord: key(&[(1, 10 + p)]),
1839 value: value.map(|v| CellValue::num(v as f64)),
1840 });
1841 }
1842 let snap = eng.apply(batch).unwrap();
1843 let distinct: std::collections::BTreeSet<_> = cells.values().collect();
1844 let want = (!cells.is_empty()).then(|| CellValue::num(distinct.len() as f64));
1845 assert_eq!(snap[&MeasureId(200)].get(&Vec::new()).cloned(), want);
1846 }
1847 }
1848
1849 fn roles() -> Model {
1852 let mut m = Model::new();
1853 let (src, dst, iter) = (CategoryId(1), CategoryId(2), CategoryId(3));
1854 m.add_category(src, "Src");
1855 m.add_category(dst, "Dst");
1856 m.add_category(iter, "Iter");
1857 for (i, n) in ["a", "b", "c"].iter().enumerate() {
1858 m.add_item(ItemId(10 + i as u32), src, *n);
1859 }
1860 for (id, n) in [(23, "z"), (22, "c"), (20, "a"), (21, "b")] {
1862 m.add_item(ItemId(id), dst, n);
1863 }
1864 m.add_item(ItemId(30), iter, "1");
1865 m.add_item(ItemId(31), iter, "2");
1866 for (id, name, cats) in [(100, "R", vec![dst, iter]), (101, "W", vec![src, dst])] {
1867 m.add_measure(Measure {
1868 id: MeasureId(id),
1869 name: Name(name.into()),
1870 value_type: ValueType::Number,
1871 categories: cats,
1872 kind: MeasureKind::Input,
1873 description: None,
1874 });
1875 }
1876 let c2 = |a: (CategoryId, u32), b: (CategoryId, u32)| {
1877 Coordinate::from_pairs([(a.0, ItemId(a.1)), (b.0, ItemId(b.1))])
1878 };
1879 for (d, it, v) in [(20, 30, 1.0), (21, 30, 2.0), (23, 30, 9.0), (22, 31, 3.0)] {
1880 m.set_input(MeasureId(100), c2((dst, d), (iter, it)), Value::Number(v));
1881 }
1882 for (s, d, v) in [(10, 21, 4.0), (11, 20, 1.0), (10, 22, 2.0), (12, 20, 0.0)] {
1884 m.set_input(MeasureId(101), c2((src, s), (dst, d)), Value::Number(v));
1885 }
1886 m
1887 }
1888
1889 #[test]
1890 fn relabel_and_swap_map_items_by_name() {
1891 let (src, dst, iter) = (CategoryId(1), CategoryId(2), CategoryId(3));
1892 let mut m = roles();
1893 define(
1894 &mut m,
1895 200,
1896 vec![src, iter],
1897 "Rs = RELABEL(R[Dst, Iter], Dst, Src)",
1898 );
1899 define(
1900 &mut m,
1901 201,
1902 vec![src, dst],
1903 "Recip = W > 0 AND SWAP(W, Src, Dst) > 0",
1904 );
1905 define(&mut m, 202, vec![src, dst], "Back = SWAP(W, Dst, Src)");
1906 define(&mut m, 203, vec![iter], "Tot = SUM(Rs OVER Src)");
1907 let out = evaluate(&m, &(200..=203).map(MeasureId).collect::<Vec<_>>()).unwrap();
1908 let rs: BTreeMap<_, _> = out[&MeasureId(200)].iter().collect();
1909 assert_eq!(
1911 rs,
1912 BTreeMap::from([
1913 (&key(&[(1, 10), (3, 30)]), &CellValue::num(1.0)),
1914 (&key(&[(1, 11), (3, 30)]), &CellValue::num(2.0)),
1915 (&key(&[(1, 12), (3, 31)]), &CellValue::num(3.0)),
1916 ])
1917 );
1918 let at =
1919 |id: u32, s: u32, d: u32| out[&MeasureId(id)].get(&key(&[(1, s), (2, d)])).cloned();
1920 let t = CellValue::Bool(true);
1921 let f = CellValue::Bool(false);
1922 assert_eq!(at(201, 10, 21), Some(t.clone()));
1924 assert_eq!(at(201, 11, 20), Some(t));
1925 assert_eq!(at(201, 10, 22), Some(f));
1926 assert_eq!(at(201, 12, 20), Some(CellValue::Bool(false)));
1927 assert_eq!(out[&MeasureId(201)].len(), 4);
1928 assert_eq!(at(202, 11, 20), Some(CellValue::num(4.0)));
1930 assert_eq!(at(202, 12, 20), Some(CellValue::num(2.0)));
1931 assert_eq!(
1932 out[&MeasureId(203)].get(&key(&[(3, 30)])),
1933 Some(&CellValue::num(3.0))
1934 );
1935 let ctx = CompileContext::for_model(&m);
1937 let ty = |text: &str| {
1938 let f = improv_core_model::parse_formula(&m, text).expect("parses");
1939 crate::compiler::infer(&ctx, MeasureId(300), &f.formula.expr)
1940 };
1941 assert_eq!(
1942 ty("X = RELABEL(R, Dst, Src)").unwrap().ty.dim,
1943 Dim::of(vec![src, iter])
1944 );
1945 assert_eq!(
1946 ty("X = SWAP(W, Src, Dst)").unwrap().ty.dim,
1947 Dim::of(vec![src, dst])
1948 );
1949 for (bad, why) in [
1950 ("X = RELABEL(R, Src, Dst)", "does not range over Src"),
1951 ("X = RELABEL(W, Src, Dst)", "already ranges over Dst"),
1952 ("X = SWAP(R, Dst, Src)", "does not range over Src"),
1953 ("X = SWAP(W, Src, Src)", "both categories"),
1954 ] {
1955 let e = ty(bad).unwrap_err().to_string();
1956 assert!(e.contains(why), "{bad}: {e}");
1957 }
1958 let f = improv_core_model::parse_formula(&m, "X = RELABEL(R, Dst, Src)").unwrap();
1959 let bare = CompileContext::new(&m.measures);
1960 assert!(crate::compiler::infer(&bare, MeasureId(300), &f.formula.expr).is_err());
1961 }
1962
1963 #[test]
1968 fn reduced_measures_broadcast_their_other_categories() {
1969 let (src, dst, iter) = (CategoryId(1), CategoryId(2), CategoryId(3));
1970 let mut m = roles();
1971 define(
1972 &mut m,
1973 200,
1974 vec![src, iter],
1975 "Rs = RELABEL(R[Dst, Iter], Dst, Src)",
1976 );
1977 define(
1978 &mut m,
1979 201,
1980 vec![dst, iter],
1981 "P = SUMPRODUCT(Rs, W OVER Src)",
1982 );
1983 let out = evaluate(&m, &[MeasureId(201)]).unwrap();
1984 let p: BTreeMap<_, _> = out[&MeasureId(201)].iter().collect();
1985 let at = |d: u32, i: u32, v: f64| (key(&[(2, d), (3, i)]), CellValue::num(v));
1986 let want = [
1987 at(21, 30, 4.0),
1988 at(20, 30, 2.0),
1989 at(22, 30, 2.0),
1990 at(20, 31, 0.0),
1991 ];
1992 assert_eq!(p, want.iter().map(|(k, v)| (k, v)).collect());
1993 let ctx = CompileContext::for_model(&m);
1996 let err = |text: &str| {
1997 let f = improv_core_model::parse_formula(&m, text).expect("parses");
1998 crate::compiler::infer(&ctx, MeasureId(300), &f.formula.expr)
1999 .unwrap_err()
2000 .to_string()
2001 };
2002 assert!(err("X = SUMPRODUCT(Rs, W OVER Iter)").contains("reduce the same categories"));
2003 assert_eq!(
2005 crate::compiler::infer(
2006 &ctx,
2007 MeasureId(300),
2008 &improv_core_model::parse_formula(&m, "X = SUMPRODUCT(R, W OVER Dst)")
2009 .unwrap()
2010 .formula
2011 .expr
2012 )
2013 .unwrap()
2014 .ty
2015 .dim,
2016 Dim::of(vec![src, iter])
2017 );
2018 define(&mut m, 202, vec![src], "SrcName = @ITEM(Src)");
2021 define(&mut m, 203, vec![dst], "DstName = @ITEM(Dst)");
2022 define(
2023 &mut m,
2024 204,
2025 vec![src, iter],
2026 "L = XLOOKUP(SrcName, DstName OVER Dst, R)",
2027 );
2028 let out = evaluate(&m, &[MeasureId(204)]).unwrap();
2029 assert_eq!(
2030 out[&MeasureId(204)].get(&key(&[(1, 11), (3, 30)])),
2031 Some(&CellValue::num(2.0))
2032 );
2033 let _ = dst;
2034 }
2035
2036 #[test]
2041 fn if_family_over_dimensions() {
2042 let (time, product) = (CategoryId(1), CategoryId(2));
2043 let mut m = revenue_model();
2044 m.inputs.remove(&(
2046 MeasureId(101),
2047 Coordinate::from_pairs([(time, ItemId(11)), (product, ItemId(21))]),
2048 ));
2049 define(
2050 &mut m,
2051 200,
2052 vec![time, product],
2053 "Pick = IF(Price > 15, Revenue, 0)",
2054 );
2055 define(
2056 &mut m,
2057 201,
2058 vec![time, product],
2059 "Ratio = Revenue / (Price - 10)",
2060 );
2061 define(
2062 &mut m,
2063 202,
2064 vec![time, product],
2065 "Safe = IFERROR(Ratio, -1)",
2066 );
2067 define(
2068 &mut m,
2069 203,
2070 vec![time, product],
2071 "Lazy = IF(Price > 15, Ratio, 0)",
2072 );
2073 define(
2074 &mut m,
2075 204,
2076 vec![product],
2077 "Label = IF(Price > 15, \"dear\", \"cheap\")",
2078 );
2079 define(&mut m, 205, vec![time, product], "Kept = CLEARERROR(Ratio)");
2080 let out = evaluate(&m, &(200..=205).map(MeasureId).collect::<Vec<_>>()).unwrap();
2081 let at =
2082 |id: u32, t: u32, p: u32| out[&MeasureId(id)].get(&key(&[(1, t), (2, p)])).cloned();
2083 let n = CellValue::num;
2084 assert_eq!(at(200, 10, 20), Some(n(0.0)));
2086 assert_eq!(at(200, 11, 20), Some(n(0.0)));
2087 assert_eq!(at(200, 10, 21), Some(n(1000.0)));
2089 assert_eq!(at(200, 11, 21), None);
2090 assert_eq!(out[&MeasureId(200)].len(), 3);
2091 assert_eq!(at(201, 10, 20), Some(CellValue::Err(err::DIV0)));
2093 assert_eq!(at(202, 10, 20), Some(n(-1.0)));
2094 assert_eq!(at(202, 10, 21), Some(n(100.0)));
2095 assert_eq!(at(203, 10, 20), Some(n(0.0)));
2097 assert_eq!(at(203, 10, 21), Some(n(100.0)));
2098 let label = &out[&MeasureId(204)];
2100 assert_eq!(
2101 label.get(&key(&[(2, 20)])),
2102 Some(&CellValue::Text("cheap".into()))
2103 );
2104 assert_eq!(
2105 label.get(&key(&[(2, 21)])),
2106 Some(&CellValue::Text("dear".into()))
2107 );
2108 assert_eq!(at(205, 10, 20), None);
2110 assert_eq!(at(205, 10, 21), Some(n(100.0)));
2111 }
2112
2113 #[test]
2117 fn dimensional_forms() {
2118 let (time, product) = (CategoryId(1), CategoryId(2));
2119 let mut m = revenue_model();
2120 let tp = vec![time, product];
2121 define(&mut m, 200, tp.clone(), "Pos = Quantity * POSITION(Time)");
2122 define(&mut m, 201, vec![time], "Year = @ITEM(Time)");
2123 define(&mut m, 202, tp.clone(), "Before = PREV(Quantity, Time)");
2124 define(
2125 &mut m,
2126 203,
2127 tp.clone(),
2128 "Growth = Quantity - PREV(Quantity, Time, 1, 0)",
2129 );
2130 define(&mut m, 204, tp.clone(), "After = NEXT(Quantity, Time)");
2131 define(
2132 &mut m,
2133 205,
2134 tp.clone(),
2135 "Run = SUM(Quantity OVER Time[FIRST..THIS])",
2136 );
2137 define(
2138 &mut m,
2139 206,
2140 vec![],
2141 "All = SUM(Quantity OVER Time, Product)",
2142 );
2143 define(
2144 &mut m,
2145 207,
2146 vec![product],
2147 "Ex = SUM(Quantity OVER ALL EXCEPT Product)",
2148 );
2149 define(
2150 &mut m,
2151 208,
2152 vec![product],
2153 "Big = SUM(Quantity OVER Time WHERE Quantity > 90)",
2154 );
2155 define(&mut m, 209, vec![product], "F = FIRST(Quantity OVER Time)");
2156 define(&mut m, 210, vec![product], "L = LAST(Quantity OVER Time)");
2157 define(
2158 &mut m,
2159 211,
2160 vec![],
2161 "J = TEXTJOIN(\"-\", TRUE, Year OVER Time)",
2162 );
2163 define(&mut m, 212, vec![], "N = @ITEMCOUNT(Product)");
2164 define(
2165 &mut m,
2166 213,
2167 tp.clone(),
2168 "ToGo = SUM(Quantity OVER Time[THIS..LAST])",
2169 );
2170 define(
2171 &mut m,
2172 214,
2173 tp,
2174 "Prior = SUM(Quantity OVER Time[THIS-1..THIS-1])",
2175 );
2176 let out = evaluate(&m, &(200..=214).map(MeasureId).collect::<Vec<_>>()).unwrap();
2177 let at =
2178 |id: u32, t: u32, p: u32| out[&MeasureId(id)].get(&key(&[(1, t), (2, p)])).cloned();
2179 let one = |id: u32, k: &[(u32, u32)]| out[&MeasureId(id)].get(&key(k)).cloned();
2180 let n = CellValue::num;
2181 assert_eq!(at(200, 11, 20), Some(n(240.0)), "2026 is position 2");
2183 assert_eq!(one(201, &[(1, 11)]), Some(CellValue::Text("2026".into())));
2184 assert_eq!(at(202, 10, 20), None, "no year before 2025");
2185 assert_eq!(at(202, 11, 20), Some(n(100.0)));
2186 assert_eq!(
2187 at(203, 10, 20),
2188 Some(n(100.0)),
2189 "default 0 before the first year"
2190 );
2191 assert_eq!(at(203, 11, 21), Some(n(30.0)));
2192 assert_eq!(at(204, 10, 21), Some(n(80.0)));
2193 assert_eq!(at(204, 11, 21), None);
2194 assert_eq!(at(205, 10, 20), Some(n(100.0)));
2195 assert_eq!(at(205, 11, 20), Some(n(220.0)), "running total");
2196 assert_eq!(one(206, &[]), Some(n(350.0)));
2197 assert_eq!(one(207, &[(2, 20)]), Some(n(220.0)));
2198 assert_eq!(one(208, &[(2, 20)]), Some(n(220.0)));
2199 assert_eq!(one(208, &[(2, 21)]), None, "no row passes WHERE: no cell");
2200 assert_eq!(one(209, &[(2, 21)]), Some(n(50.0)));
2201 assert_eq!(one(210, &[(2, 21)]), Some(n(80.0)));
2202 assert_eq!(one(211, &[]), Some(CellValue::Text("2025-2026".into())));
2203 assert_eq!(one(212, &[]), Some(n(2.0)));
2204 assert_eq!(at(213, 10, 20), Some(n(220.0)), "to go");
2205 assert_eq!(at(213, 11, 20), Some(n(120.0)));
2206 assert_eq!(
2207 at(214, 10, 20),
2208 Some(CellValue::Err(err::NA)),
2209 "an empty window"
2210 );
2211 assert_eq!(at(214, 11, 20), Some(n(100.0)));
2212 }
2213
2214 #[test]
2217 fn recurrence_carries_a_balance_forward() {
2218 let (time, product) = (CategoryId(1), CategoryId(2));
2219 let mut m = revenue_model();
2220 m.add_item(ItemId(12), time, "2027");
2221 let set = |m: &mut Model, id: u32, pairs: &[(CategoryId, ItemId)], v: f64| {
2222 m.set_input(
2223 MeasureId(id),
2224 Coordinate::from_pairs(pairs.iter().copied()),
2225 Value::Number(v),
2226 )
2227 };
2228 set(
2229 &mut m,
2230 101,
2231 &[(time, ItemId(12)), (product, ItemId(20))],
2232 10.0,
2233 );
2234 m.add_measure(Measure {
2236 id: MeasureId(300),
2237 name: Name("Bal".into()),
2238 value_type: ValueType::Number,
2239 categories: vec![time, product],
2240 kind: MeasureKind::Input,
2241 description: None,
2242 });
2243 let f = improv_core_model::parse_formula(&m, "Bal = PREV(Bal, Time, 1, Price) + Quantity")
2244 .unwrap();
2245 m.measures.get_mut(&MeasureId(300)).unwrap().kind = MeasureKind::Derived(f.formula);
2246 let out = evaluate(&m, &[MeasureId(300)]).unwrap();
2247 let bal = &out[&MeasureId(300)];
2248 let at = |t: u32, p: u32| bal.get(&key(&[(1, t), (2, p)])).cloned();
2249 assert_eq!(at(10, 20), Some(CellValue::num(110.0)));
2251 assert_eq!(at(11, 20), Some(CellValue::num(230.0)));
2252 assert_eq!(at(12, 20), Some(CellValue::num(240.0)));
2253 assert_eq!(at(11, 21), Some(CellValue::num(150.0)));
2255 assert_eq!(at(12, 21), None);
2256 let (mut engine, _) = crate::session::Engine::new(&m, &[MeasureId(300)]).unwrap();
2258 let snap = engine
2259 .set(MeasureId(101), key(&[(1, 10), (2, 20)]), 0.0)
2260 .unwrap();
2261 assert_eq!(
2262 snap[&MeasureId(300)][&key(&[(1, 12), (2, 20)])],
2263 CellValue::num(140.0)
2264 );
2265 let mut bad = m.clone();
2268 let f = improv_core_model::parse_formula(&bad, "Bal = Bal + 1").unwrap();
2269 bad.measures.get_mut(&MeasureId(300)).unwrap().kind = MeasureKind::Derived(f.formula);
2270 let e = evaluate(&bad, &[MeasureId(300)]).unwrap_err().to_string();
2271 assert!(e.contains("only PREV"), "{e}");
2272 }
2273
2274 #[test]
2278 fn recurrence_gaps_errors_and_no_default() {
2279 let (time, product) = (CategoryId(1), CategoryId(2));
2280 let mut m = revenue_model();
2281 m.add_item(ItemId(12), time, "2027");
2282 let q = |t: u32, p: u32| Coordinate::from_pairs([(time, ItemId(t)), (product, ItemId(p))]);
2283 m.inputs.remove(&(MeasureId(101), q(11, 20)));
2285 m.set_input(MeasureId(101), q(12, 20), Value::Number(5.0));
2286 for (id, name, text) in [
2287 (300, "Bal", "Bal = PREV(Bal, Time, 1, 1000) + Quantity"),
2288 (
2289 301,
2290 "Inv",
2291 "Inv = PREV(Inv, Time, 1, 0) + 1 / (Quantity - 50)",
2292 ),
2293 (302, "Bare", "Bare = PREV(Bare, Time) + Quantity"),
2294 ] {
2295 m.add_measure(Measure {
2296 id: MeasureId(id),
2297 name: Name(name.into()),
2298 value_type: ValueType::Number,
2299 categories: vec![time, product],
2300 kind: MeasureKind::Input,
2301 description: None,
2302 });
2303 let f = improv_core_model::parse_formula(&m, text).unwrap();
2304 m.measures.get_mut(&MeasureId(id)).unwrap().kind = MeasureKind::Derived(f.formula);
2305 }
2306 let out = evaluate(&m, &[MeasureId(300), MeasureId(301), MeasureId(302)]).unwrap();
2307 let at =
2308 |id: u32, t: u32, p: u32| out[&MeasureId(id)].get(&key(&[(1, t), (2, p)])).cloned();
2309 assert_eq!(at(300, 10, 20), Some(CellValue::num(1100.0)));
2310 assert_eq!(at(300, 11, 20), None, "no 2026 cell");
2311 assert_eq!(
2312 at(300, 12, 20),
2313 Some(CellValue::num(1005.0)),
2314 "restarts from the default"
2315 );
2316 assert_eq!(at(301, 10, 21), Some(CellValue::Err(err::DIV0)));
2318 assert_eq!(at(301, 11, 21), Some(CellValue::Err(err::DIV0)));
2319 assert!(
2320 out.get(&MeasureId(302)).is_none_or(|c| c.is_empty()),
2321 "no default: no chain"
2322 );
2323 }
2324
2325 #[test]
2328 fn window_bounds_and_edges() {
2329 let (time, product) = (CategoryId(1), CategoryId(2));
2330 let mut m = revenue_model();
2331 let tp = vec![time, product];
2332 define(
2333 &mut m,
2334 200,
2335 tp.clone(),
2336 "A = SUM(Quantity OVER Time[LAST..LAST])",
2337 );
2338 define(
2339 &mut m,
2340 201,
2341 tp.clone(),
2342 "B = SUM(Quantity OVER Time[FIRST..FIRST])",
2343 );
2344 define(&mut m, 202, tp, "C = MAX(Quantity OVER Time[THIS+1..LAST])");
2345 define(&mut m, 204, vec![time, product], "Big = Quantity > 90");
2346 define(
2347 &mut m,
2348 203,
2349 vec![product],
2350 "D = TEXTJOIN(\",\", FALSE, Big OVER Time)",
2351 );
2352 let out = evaluate(&m, &(200..=204).map(MeasureId).collect::<Vec<_>>()).unwrap();
2353 let at =
2354 |id: u32, t: u32, p: u32| out[&MeasureId(id)].get(&key(&[(1, t), (2, p)])).cloned();
2355 assert_eq!(at(200, 10, 20), Some(CellValue::num(120.0)));
2356 assert_eq!(at(201, 11, 20), Some(CellValue::num(100.0)));
2357 assert_eq!(at(202, 10, 20), Some(CellValue::num(120.0)));
2358 assert_eq!(at(202, 11, 20), Some(CellValue::Err(err::NA)));
2359 assert_eq!(
2360 out[&MeasureId(203)].get(&key(&[(2, 20)])),
2361 Some(&CellValue::Text("TRUE,TRUE".into()))
2362 );
2363 }
2364
2365 #[test]
2368 fn let_lambda_map_scan_reduce() {
2369 let (time, product) = (CategoryId(1), CategoryId(2));
2370 let mut m = revenue_model();
2371 let tp = vec![time, product];
2372 define(
2373 &mut m,
2374 200,
2375 tp.clone(),
2376 "A = LET(q, Quantity, p, Price, q * p + q)",
2377 );
2378 define(
2379 &mut m,
2380 201,
2381 tp.clone(),
2382 "B = MAP(Quantity, LAMBDA(v, v * 2))",
2383 );
2384 define(
2385 &mut m,
2386 202,
2387 tp.clone(),
2388 "C = LAMBDA(a, b, a - b)(Quantity, Price)",
2389 );
2390 define(
2391 &mut m,
2392 203,
2393 tp.clone(),
2394 "D = SCAN(0, Quantity OVER Time, LAMBDA(acc, v, acc + v))",
2395 );
2396 define(
2397 &mut m,
2398 204,
2399 vec![product],
2400 "E = REDUCE(1, Quantity OVER Time, LAMBDA(acc, v, acc * v))",
2401 );
2402 let out = evaluate(&m, &(200..=204).map(MeasureId).collect::<Vec<_>>()).unwrap();
2403 let at = |id: u32, k: &[(u32, u32)]| out[&MeasureId(id)].get(&key(k)).cloned();
2404 let n = CellValue::num;
2405 assert_eq!(at(200, &[(1, 10), (2, 20)]), Some(n(1100.0)));
2406 assert_eq!(at(201, &[(1, 10), (2, 21)]), Some(n(100.0)));
2407 assert_eq!(at(202, &[(1, 11), (2, 20)]), Some(n(110.0)));
2408 assert_eq!(at(203, &[(1, 10), (2, 20)]), Some(n(100.0)));
2409 assert_eq!(at(203, &[(1, 11), (2, 20)]), Some(n(220.0)), "running sum");
2410 assert_eq!(at(204, &[(2, 21)]), Some(n(4000.0)), "50 * 80");
2411 for text in [
2414 "X = LET(q, Quantity)",
2415 "X = LAMBDA(v, v + 1)",
2416 "X = SCAN(0, Quantity, LAMBDA(a, v, a + v))",
2417 "X = SCAN(0, Quantity OVER Time, LAMBDA(v, v))",
2418 "X = MAP(Quantity, LAMBDA(a, b, a + b))",
2419 "X = SCAN(0, Quantity OVER Time, 1)",
2420 ] {
2421 assert!(
2422 improv_core_model::parse_formula(&m, text).is_err(),
2423 "{text}"
2424 );
2425 }
2426 }
2427
2428 #[test]
2431 fn matrix_product_by_join() {
2432 let (r, k, c) = (CategoryId(1), CategoryId(2), CategoryId(3));
2433 let mut m = Model::default();
2434 for (cat, name, base) in [(r, "R", 10), (k, "K", 20), (c, "C", 30)] {
2435 m.add_category(cat, name);
2436 for i in 0..2 {
2437 m.add_item(ItemId(base + i), cat, format!("{name}{i}"));
2438 }
2439 }
2440 let input = |m: &mut Model, id: u32, name: &str, cats: Vec<CategoryId>| {
2441 m.add_measure(Measure {
2442 id: MeasureId(id),
2443 name: Name(name.into()),
2444 value_type: ValueType::Number,
2445 categories: cats,
2446 kind: MeasureKind::Input,
2447 description: None,
2448 });
2449 };
2450 input(&mut m, 100, "A", vec![r, k]);
2451 input(&mut m, 101, "B", vec![k, c]);
2452 let at = |a: (CategoryId, u32), b: (CategoryId, u32)| {
2454 Coordinate::from_pairs([(a.0, ItemId(a.1)), (b.0, ItemId(b.1))])
2455 };
2456 for (i, j, v) in [(0, 0, 1.0), (0, 1, 2.0), (1, 0, 3.0), (1, 1, 4.0)] {
2457 m.set_input(
2458 MeasureId(100),
2459 at((r, 10 + i), (k, 20 + j)),
2460 Value::Number(v),
2461 );
2462 }
2463 for (i, j, v) in [(0, 0, 5.0), (0, 1, 6.0), (1, 0, 7.0), (1, 1, 8.0)] {
2464 m.set_input(
2465 MeasureId(101),
2466 at((k, 20 + i), (c, 30 + j)),
2467 Value::Number(v),
2468 );
2469 }
2470 define(&mut m, 200, vec![r, k, c], "AB = A * B");
2471 define(&mut m, 201, vec![r, c], "P = SUM(AB OVER K)");
2472 let out = evaluate(&m, &[MeasureId(201)]).unwrap();
2473 let p = |i: u32, j: u32| {
2474 out[&MeasureId(201)]
2475 .get(&key(&[(1, 10 + i), (3, 30 + j)]))
2476 .cloned()
2477 };
2478 assert_eq!(p(0, 0), Some(CellValue::num(19.0)));
2480 assert_eq!(p(0, 1), Some(CellValue::num(22.0)));
2481 assert_eq!(p(1, 0), Some(CellValue::num(43.0)));
2482 assert_eq!(p(1, 1), Some(CellValue::num(50.0)));
2483 }
2484
2485 #[test]
2488 fn named_lambdas() {
2489 use improv_core_model::{parse_definition, Definition};
2490 let (time, product) = (CategoryId(1), CategoryId(2));
2491 let mut m = revenue_model();
2492 let def = |m: &Model, t: &str| match parse_definition(m, t).unwrap() {
2493 Definition::Lambda(l) => l.lambda,
2494 other => panic!("{other:?}"),
2495 };
2496 let l = def(&m, "Margin = LAMBDA(p, q, (p - q) / p)");
2497 m.lambdas.insert(7, l);
2498 let l = def(&m, "Twice = LAMBDA(x, Margin(x, 1) * 2)");
2499 m.lambdas.insert(8, l);
2500 define(
2501 &mut m,
2502 200,
2503 vec![time, product],
2504 "A = Margin(Quantity, Price)",
2505 );
2506 define(&mut m, 201, vec![time, product], "B = Twice(Quantity)");
2507 let at = |m: &Model, id: u32| {
2508 let out = evaluate(m, &[MeasureId(id)]).unwrap();
2509 out[&MeasureId(id)].get(&key(&[(1, 10), (2, 20)])).cloned()
2510 };
2511 assert_eq!(at(&m, 200), Some(CellValue::num(0.9)));
2513 assert_eq!(at(&m, 201), Some(CellValue::num(1.98)));
2514 let l = def(&m, "Margin = LAMBDA(p, q, p - q)");
2516 m.lambdas.insert(7, l);
2517 assert_eq!(at(&m, 200), Some(CellValue::num(90.0)));
2518 assert_eq!(at(&m, 201), Some(CellValue::num(198.0)));
2519 let mut gone = m.clone();
2521 gone.lambdas.remove(&7);
2522 assert!(evaluate(&gone, &[MeasureId(200)]).is_err());
2523 for (text, why) in [
2524 ("X = LAMBDA(p, p + Price)", "reads only its parameters"),
2525 ("SUM = LAMBDA(p, p)", "built-in function"),
2526 ("X = LAMBDA(p, p)(1)", "not called here"),
2527 ("Margin = LAMBDA(p, Margin(p, 1))", "unknown"),
2528 (
2529 "X = LAMBDA(a, SCAN(0, a OVER Time, LAMBDA(s, v, s + v)))",
2530 "",
2531 ),
2532 ] {
2533 let e = parse_definition(&m, text).unwrap_err();
2534 assert!(e.message.contains(why), "{text}: {}", e.message);
2535 }
2536 for text in ["Y = Margin(Quantity)", "Y = Margin(Quantity, Price, 1)"] {
2537 let e = improv_core_model::parse_formula(&m, text).unwrap_err();
2538 assert!(e.message.contains("takes 2"), "{text}: {}", e.message);
2539 }
2540 let mut cyc = m.clone();
2542 let body = Expr::Call(improv_core_model::functions::named_lambda(9), vec![]);
2543 cyc.lambdas.insert(
2544 9,
2545 improv_core_model::NamedLambda {
2546 name: "Loop".into(),
2547 params: vec![],
2548 body: body.clone(),
2549 },
2550 );
2551 assert!(cyc.expand_lambdas(&body).unwrap_err().contains("cycle"));
2552 let bad = Expr::Call(improv_core_model::functions::named_lambda(7), vec![]);
2553 assert!(m.expand_lambdas(&bad).unwrap_err().contains("takes 2"));
2554 assert!(m
2555 .expand_lambdas(&Expr::Call(
2556 improv_core_model::functions::named_lambda(99),
2557 vec![]
2558 ))
2559 .unwrap_err()
2560 .contains("does not exist"));
2561 }
2562
2563 #[test]
2567 fn into_two_categories() {
2568 let (time, product) = (CategoryId(1), CategoryId(2));
2569 let (row, col) = (CategoryId(3), CategoryId(4));
2570 let mut m = revenue_model();
2571 m.add_category(row, "Row");
2572 m.add_category(col, "Col");
2573 for i in 0..5 {
2574 m.add_item(ItemId(30 + i), row, format!("r{i}"));
2575 }
2576 for i in 0..2 {
2577 m.add_item(ItemId(40 + i), col, format!("c{i}"));
2578 }
2579 define(
2580 &mut m,
2581 200,
2582 vec![row, col],
2583 "S = SORT(Quantity OVER Time, Product INTO Row, Col)",
2584 );
2585 define(
2586 &mut m,
2587 201,
2588 vec![time, product],
2589 "X = POSITION(Time) + Quantity * 0",
2590 );
2591 define(
2592 &mut m,
2593 202,
2594 vec![product, row, col],
2595 "L = LINEST(Quantity, X OVER Time, TRUE, TRUE INTO Row, Col)",
2596 );
2597 let out = evaluate(&m, &[MeasureId(200), MeasureId(202)]).unwrap();
2598 let at = |id: u32, k: &[(u32, u32)]| out[&MeasureId(id)].get(&key(k)).cloned();
2599 let n = CellValue::num;
2600 assert_eq!(at(200, &[(3, 30), (4, 40)]), Some(n(50.0)));
2602 assert_eq!(at(200, &[(3, 30), (4, 41)]), Some(n(80.0)));
2603 assert_eq!(at(200, &[(3, 31), (4, 40)]), Some(n(100.0)));
2604 assert_eq!(at(200, &[(3, 32), (4, 40)]), None, "four values only");
2605 assert_eq!(at(202, &[(2, 20), (3, 30), (4, 40)]), Some(n(20.0)));
2607 assert_eq!(at(202, &[(2, 20), (3, 30), (4, 41)]), Some(n(80.0)));
2608 assert_eq!(at(202, &[(2, 20), (3, 32), (4, 40)]), Some(n(1.0)));
2609 assert_eq!(out[&MeasureId(202)].len(), 20, "two 5 × 2 blocks");
2610 let mut m3 = m.clone();
2613 define(&mut m3, 203, vec![time, product], "X2 = X * X");
2614 define(
2615 &mut m3,
2616 204,
2617 vec![product, col],
2618 "Two = LINEST(Quantity, X, X2 OVER Time, FALSE INTO Col)",
2619 );
2620 let f = improv_core_model::parse_formula(
2621 &m3,
2622 "Q = LINEST(Quantity, X, X2 OVER Time, FALSE INTO Col)",
2623 )
2624 .unwrap();
2625 let Expr::Call(_, a) = &f.formula.expr else {
2626 panic!()
2627 };
2628 let def = improv_core_model::functions::by_id(FuncId(1257)).unwrap();
2629 assert_eq!(
2630 (0..a.len()).filter(|i| def.reduced(*i, &a[*i])).count(),
2631 3,
2632 "y, x1, x2 reduced; FALSE a parameter"
2633 );
2634 assert!(
2635 improv_core_model::parse_formula(&m3, "Q = LINEST(Quantity OVER Time INTO Col)")
2636 .is_err()
2637 );
2638 let f = improv_core_model::parse_formula(&m, "Z = SORT(Quantity OVER Time INTO Row, Col)")
2640 .unwrap();
2641 let Expr::Call(_, args) = &f.formula.expr else {
2642 panic!()
2643 };
2644 assert_eq!(
2645 args.last(),
2646 Some(&Expr::Call(
2647 improv_core_model::functions::INTO,
2648 vec![Expr::Category(row), Expr::Category(col)]
2649 ))
2650 );
2651 for (text, why) in [
2652 (
2653 "X = SORT(Quantity OVER Time INTO Row, Product)",
2654 "does not range over",
2655 ),
2656 (
2657 "X = SORT(Quantity OVER Time INTO Row, Col, Row)",
2658 "malformed INTO",
2659 ),
2660 ] {
2661 let e = improv_core_model::parse_formula(&m, text)
2662 .map_err(|e| e.message)
2663 .and_then(|f| {
2664 crate::compiler::infer(
2665 &CompileContext::for_model(&m),
2666 MeasureId(209),
2667 &f.formula.expr,
2668 )
2669 .map(|_| ())
2670 .map_err(|e| e.to_string())
2671 })
2672 .unwrap_err();
2673 assert!(e.contains(why), "{text}: {e}");
2674 }
2675 }
2676
2677 #[test]
2680 fn sequence_makearray_selectlist_sortby() {
2681 let (time, product, rank) = (CategoryId(1), CategoryId(2), CategoryId(3));
2682 let mut m = revenue_model();
2683 m.add_category(rank, "Rank");
2684 for i in 0..2 {
2685 m.add_item(ItemId(30 + i), rank, format!("#{}", i + 1));
2686 }
2687 define(&mut m, 200, vec![time], "S = SEQUENCE(Time, 10, 5)");
2688 define(
2689 &mut m,
2690 201,
2691 vec![time, product],
2692 "G = MAKEARRAY(Time, Product, LAMBDA(r, c, r * 10 + c))",
2693 );
2694 define(
2697 &mut m,
2698 205,
2699 vec![time, product],
2700 "P2 = Price + Quantity * 0",
2701 );
2702 define(&mut m, 202, vec![rank], "Want = POSITION(Rank) * 10");
2703 define(
2704 &mut m,
2705 203,
2706 vec![time, rank],
2707 "L = SELECTLIST(Quantity, P2 OVER Product, Want)",
2708 );
2709 define(
2711 &mut m,
2712 204,
2713 vec![time, rank],
2714 "O = SORTBY(Quantity, P2 OVER Product, -1, Quantity, 1 INTO Rank)",
2715 );
2716 let ids: Vec<MeasureId> = (200..=205).map(MeasureId).collect();
2717 let out = evaluate(&m, &ids).unwrap();
2718 let at = |id: u32, k: &[(u32, u32)]| out[&MeasureId(id)].get(&key(k)).cloned();
2719 let n = CellValue::num;
2720 assert_eq!(at(200, &[(1, 10)]), Some(n(10.0)));
2721 assert_eq!(at(200, &[(1, 11)]), Some(n(15.0)));
2722 assert_eq!(at(201, &[(1, 11), (2, 20)]), Some(n(21.0)));
2723 assert_eq!(
2724 at(203, &[(1, 10), (3, 30)]),
2725 Some(n(100.0)),
2726 "Price 10 is A"
2727 );
2728 assert_eq!(at(203, &[(1, 11), (3, 31)]), Some(n(80.0)), "Price 20 is B");
2729 assert_eq!(at(204, &[(1, 10), (3, 30)]), Some(n(50.0)));
2730 assert_eq!(at(204, &[(1, 10), (3, 31)]), Some(n(100.0)));
2731 define(
2733 &mut m,
2734 206,
2735 vec![time],
2736 "R = BYROW(Quantity OVER Product, LAMBDA(row, MAX(row) - MIN(row)))",
2737 );
2738 define(
2739 &mut m,
2740 207,
2741 vec![product],
2742 "C = BYCOL(Quantity OVER Time, LAMBDA(col, SUM(col)))",
2743 );
2744 let out = evaluate(&m, &[MeasureId(206), MeasureId(207)]).unwrap();
2745 assert_eq!(out[&MeasureId(206)].get(&key(&[(1, 10)])), Some(&n(50.0)));
2746 assert_eq!(out[&MeasureId(207)].get(&key(&[(2, 21)])), Some(&n(130.0)));
2747 for text in [
2748 "X = BYROW(Quantity OVER Product, LAMBDA(row, row * 2))",
2749 "X = BYROW(Quantity, LAMBDA(row, SUM(row)))",
2750 "X = BYROW(Quantity OVER Product, 1)",
2751 "X = BYROW(Quantity OVER Product LAMBDA(r, SUM(r)))",
2752 "X = BYROW(Quantity OVER Product, LAMBDA(r, SUM(r))",
2753 "X = SEQUENCE(Time, 1, 2, 3)",
2754 "X = SEQUENCE(Quantity)",
2755 "X = MAKEARRAY(Time, Product, 1)",
2756 "X = MAKEARRAY(Time Product, LAMBDA(r, c, r))",
2757 "X = MAKEARRAY(Time, Product LAMBDA(r, c, r))",
2758 "X = MAKEARRAY(Time, Product, LAMBDA(r, r))",
2759 ] {
2760 assert!(
2761 improv_core_model::parse_formula(&m, text).is_err(),
2762 "{text}"
2763 );
2764 }
2765 }
2766
2767 #[test]
2770 fn array_results_land_on_the_into_category() {
2771 let (time, product, rank) = (CategoryId(1), CategoryId(2), CategoryId(3));
2772 let mut m = revenue_model();
2773 m.add_category(rank, "Rank");
2774 for i in 0..3 {
2775 m.add_item(ItemId(30 + i), rank, format!("#{}", i + 1));
2776 }
2777 define(
2778 &mut m,
2779 200,
2780 vec![product, rank],
2781 "Hi = SORT(Quantity OVER Time, -1 INTO Rank)",
2782 );
2783 define(
2784 &mut m,
2785 201,
2786 vec![rank],
2787 "U = UNIQUE(Price OVER Product INTO Rank)",
2788 );
2789 let out = evaluate(&m, &[MeasureId(200), MeasureId(201)]).unwrap();
2790 let hi = &out[&MeasureId(200)];
2791 assert_eq!(
2792 hi.get(&key(&[(2, 20), (3, 30)])),
2793 Some(&CellValue::num(120.0))
2794 );
2795 assert_eq!(
2796 hi.get(&key(&[(2, 20), (3, 31)])),
2797 Some(&CellValue::num(100.0))
2798 );
2799 assert_eq!(
2800 hi.get(&key(&[(2, 20), (3, 32)])),
2801 None,
2802 "two values, three items"
2803 );
2804 assert_eq!(out[&MeasureId(201)].len(), 2);
2805 for (text, why) in [
2808 ("X = SORT(Quantity OVER Time)", "INTO"),
2809 ("X = SUM(Quantity OVER Time INTO Rank)", "INTO is for list"),
2810 (
2811 "X = SORT(Quantity OVER Time INTO Product)",
2812 "does not range over",
2813 ),
2814 ] {
2815 let f = improv_core_model::parse_formula(&m, text).unwrap();
2816 let e = crate::compiler::infer(
2817 &CompileContext::for_model(&m),
2818 MeasureId(300),
2819 &f.formula.expr,
2820 )
2821 .unwrap_err()
2822 .to_string();
2823 assert!(e.contains(why), "{text}: {e}");
2824 }
2825 let _ = time;
2826 }
2827
2828 #[test]
2831 fn parameters_beyond_the_table_broadcast() {
2832 let (time, product) = (CategoryId(1), CategoryId(2));
2833 let mut m = revenue_model();
2834 define(&mut m, 199, vec![time], "Sep = @ITEM(Time)");
2836 define(
2837 &mut m,
2838 200,
2839 vec![time],
2840 "J = TEXTJOIN(Sep, TRUE, Price OVER Product)",
2841 );
2842 let out = evaluate(&m, &[MeasureId(200)]).unwrap();
2843 let j = &out[&MeasureId(200)];
2844 assert_eq!(
2845 j.get(&key(&[(1, 10)])),
2846 Some(&CellValue::Text("10202520".into()))
2847 );
2848 assert_eq!(
2849 j.get(&key(&[(1, 11)])),
2850 Some(&CellValue::Text("10202620".into()))
2851 );
2852 let _ = product;
2853 }
2854
2855 #[test]
2857 fn measure_parameters_join_per_group() {
2858 let product = CategoryId(2);
2859 let mut m = revenue_model();
2860 define(
2861 &mut m,
2862 200,
2863 vec![product],
2864 "J = TEXTJOIN(@ITEM(Product), TRUE, Quantity OVER Time)",
2865 );
2866 let out = evaluate(&m, &[MeasureId(200)]).unwrap();
2867 assert_eq!(
2868 out[&MeasureId(200)].get(&key(&[(2, 20)])),
2869 Some(&CellValue::Text("100WidgetA120".into()))
2870 );
2871 }
2872
2873 #[test]
2876 fn if_else_spans_and_errors_flow() {
2877 let (time, product) = (CategoryId(1), CategoryId(2));
2878 let mut m = revenue_model();
2879 define(&mut m, 200, vec![product], "Ratio = 1 / (Price - 10)");
2881 define(
2882 &mut m,
2883 201,
2884 vec![time, product],
2885 "Pick = IF(Ratio > 0, 0, Revenue)",
2886 );
2887 define(&mut m, 202, vec![], "Total = SUM(Ratio OVER Product)");
2888 define(
2889 &mut m,
2890 203,
2891 vec![time, product],
2892 "Gate = IF(Price > 15, Revenue)",
2893 );
2894 let out = evaluate(&m, &(200..=203).map(MeasureId).collect::<Vec<_>>()).unwrap();
2895 let at =
2896 |id: u32, t: u32, p: u32| out[&MeasureId(id)].get(&key(&[(1, t), (2, p)])).cloned();
2897 assert_eq!(at(201, 10, 20), Some(CellValue::Err(err::DIV0)));
2899 assert_eq!(at(201, 11, 20), Some(CellValue::Err(err::DIV0)));
2900 assert_eq!(at(201, 10, 21), Some(CellValue::num(0.0)));
2902 assert_eq!(out[&MeasureId(201)].len(), 4);
2903 assert_eq!(out[&MeasureId(202)][&Vec::new()], CellValue::Err(err::DIV0));
2904 assert_eq!(at(203, 10, 20), None);
2906 assert_eq!(at(203, 10, 21), Some(CellValue::num(1000.0)));
2907 }
2908
2909 #[test]
2912 fn n_ary_calls_and_typed_aggregations() {
2913 let (time, product) = (CategoryId(1), CategoryId(2));
2914 let mut m = revenue_model();
2915 define(
2916 &mut m,
2917 200,
2918 vec![time, product],
2919 "S = SUM(Price, Quantity, 1, Price)",
2920 );
2921 define(
2922 &mut m,
2923 201,
2924 vec![product],
2925 "P = PRODUCT(Quantity OVER Time)",
2926 );
2927 define(&mut m, 202, vec![product], "Big = Price > 15");
2928 define(&mut m, 203, vec![], "AnyBig = OR(Big OVER Product)");
2929 define(&mut m, 204, vec![], "AllBig = AND(Big OVER Product)");
2930 define(&mut m, 205, vec![product], "R = ROUND(Price / 3, 2)");
2931 define(&mut m, 206, vec![], "Pi = PI()");
2932 let out = evaluate(&m, &(200..=206).map(MeasureId).collect::<Vec<_>>()).unwrap();
2933 let n = CellValue::num;
2934 assert_eq!(out[&MeasureId(200)][&key(&[(1, 10), (2, 20)])], n(121.0));
2935 assert_eq!(out[&MeasureId(201)][&key(&[(2, 21)])], n(4000.0));
2936 assert_eq!(out[&MeasureId(203)][&Vec::new()], CellValue::Bool(true));
2937 assert_eq!(out[&MeasureId(204)][&Vec::new()], CellValue::Bool(false));
2938 assert_eq!(out[&MeasureId(205)][&key(&[(2, 20)])], n(3.33));
2939 assert_eq!(out[&MeasureId(206)][&Vec::new()], n(std::f64::consts::PI));
2940 }
2941
2942 fn key(pairs: &[(u32, u32)]) -> CoordKey {
2943 let mut k: Vec<(u32, u32)> = pairs.to_vec();
2944 k.sort();
2945 k
2946 }
2947
2948 #[test]
2949 fn evaluate_revenue_matches_known_results() {
2950 let model = revenue_model();
2951 let out = evaluate(&model, &[MeasureId(102)]).expect("evaluate");
2952 let rev = out.get(&MeasureId(102)).expect("revenue computed");
2953
2954 assert_eq!(
2956 rev.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
2957 Some(1000.0)
2958 );
2959 assert_eq!(
2961 rev.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
2962 Some(1000.0)
2963 );
2964 assert_eq!(
2966 rev.get(&key(&[(1, 11), (2, 20)])).and_then(|v| v.as_num()),
2967 Some(1200.0)
2968 );
2969 assert_eq!(
2971 rev.get(&key(&[(1, 11), (2, 21)])).and_then(|v| v.as_num()),
2972 Some(1600.0)
2973 );
2974 assert_eq!(rev.len(), 4);
2975 }
2976
2977 #[test]
2978 fn evaluate_multi_layer_derivation() {
2979 let mut model = revenue_model();
2982 let (time, product) = (CategoryId(1), CategoryId(2));
2983 let over_time = improv_core_model::DimensionSpec {
2984 by: vec![product],
2985 over: vec![time],
2986 except: vec![],
2987 };
2988 model.add_measure(Measure {
2989 id: MeasureId(103),
2990 name: Name("RevenueByProduct".into()),
2991 value_type: ValueType::Number,
2992 categories: vec![product],
2993 kind: MeasureKind::Derived(improv_core_model::Formula::new(Expr::Call(
2994 improv_core_model::FuncId(1), vec![Expr::Ref(MeasureId(102), over_time)],
2996 ))),
2997 description: None,
2998 });
2999
3000 let out = evaluate(&model, &[MeasureId(103)]).expect("evaluate");
3001 let rbp = out.get(&MeasureId(103)).expect("RevenueByProduct computed");
3002 assert_eq!(
3004 rbp.get(&key(&[(2, 20)])).and_then(|v| v.as_num()),
3005 Some(2200.0)
3006 );
3007 assert_eq!(
3008 rbp.get(&key(&[(2, 21)])).and_then(|v| v.as_num()),
3009 Some(2600.0)
3010 );
3011 assert_eq!(rbp.len(), 2);
3012 }
3013
3014 #[test]
3017 fn evaluate_scalar_funcs_and_comparison() {
3018 let mut model = revenue_model();
3019 let (time, product) = (CategoryId(1), CategoryId(2));
3020 let refr = |id| Expr::Ref(id, DimensionSpec::default());
3021 let mk = |id: u32, name: &str, cats: Vec<CategoryId>, f: Expr| Measure {
3022 id: MeasureId(id),
3023 name: Name(name.into()),
3024 value_type: ValueType::Number,
3025 categories: cats,
3026 kind: MeasureKind::Derived(improv_core_model::Formula::new(f)),
3027 description: None,
3028 };
3029
3030 model.add_measure(mk(
3033 110,
3034 "Profit",
3035 vec![time, product],
3036 Expr::BinaryOp(
3037 BinaryOp::Sub,
3038 Box::new(refr(MeasureId(100))),
3039 Box::new(refr(MeasureId(101))),
3040 ),
3041 ));
3042 model.add_measure(mk(
3044 111,
3045 "AbsProfit",
3046 vec![time, product],
3047 Expr::Call(FuncId(10), vec![refr(MeasureId(110))]),
3048 ));
3049 model.add_measure(mk(
3051 113,
3052 "MinPQ",
3053 vec![time, product],
3054 Expr::Call(FuncId(20), vec![refr(MeasureId(100)), refr(MeasureId(101))]),
3055 ));
3056
3057 let out = evaluate(&model, &[MeasureId(111), MeasureId(113)]).expect("evaluate");
3058
3059 let ap = out.get(&MeasureId(111)).expect("AbsProfit");
3061 assert_eq!(
3062 ap.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
3063 Some(90.0)
3064 ); assert_eq!(
3066 ap.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
3067 Some(30.0)
3068 ); assert_eq!(
3070 ap.get(&key(&[(1, 11), (2, 20)])).and_then(|v| v.as_num()),
3071 Some(110.0)
3072 ); assert_eq!(
3074 ap.get(&key(&[(1, 11), (2, 21)])).and_then(|v| v.as_num()),
3075 Some(60.0)
3076 ); let mp = out.get(&MeasureId(113)).expect("MinPQ");
3080 assert_eq!(
3081 mp.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
3082 Some(10.0)
3083 );
3084 assert_eq!(
3085 mp.get(&key(&[(1, 11), (2, 21)])).and_then(|v| v.as_num()),
3086 Some(20.0)
3087 );
3088
3089 let out2 = evaluate(&model, &[MeasureId(111), MeasureId(113)]).expect("evaluate");
3091 assert_eq!(out.get(&MeasureId(111)), out2.get(&MeasureId(111)));
3092 assert_eq!(out.get(&MeasureId(113)), out2.get(&MeasureId(113)));
3093 }
3094
3095 #[test]
3098 fn evaluate_comparison_yields_boolean() {
3099 let mut model = revenue_model();
3100 let product = CategoryId(2);
3101 let refr = |id| Expr::Ref(id, DimensionSpec::default());
3102
3103 model.add_measure(Measure {
3105 id: MeasureId(120),
3106 name: Name("Threshold".into()),
3107 value_type: ValueType::Number,
3108 categories: vec![product],
3109 kind: MeasureKind::Input,
3110 description: None,
3111 });
3112 let coord = |pairs: &[(CategoryId, ItemId)]| Coordinate::from_pairs(pairs.iter().copied());
3113 model.set_input(
3114 MeasureId(120),
3115 coord(&[(product, ItemId(20))]),
3116 Value::Number(15.0),
3117 );
3118 model.set_input(
3119 MeasureId(120),
3120 coord(&[(product, ItemId(21))]),
3121 Value::Number(15.0),
3122 );
3123
3124 model.add_measure(Measure {
3126 id: MeasureId(121),
3127 name: Name("Hot".into()),
3128 value_type: ValueType::Boolean,
3129 categories: vec![product],
3130 kind: MeasureKind::Derived(improv_core_model::Formula::new(Expr::BinaryOp(
3131 BinaryOp::Gt,
3132 Box::new(refr(MeasureId(100))),
3133 Box::new(refr(MeasureId(120))),
3134 ))),
3135 description: None,
3136 });
3137
3138 let out = evaluate(&model, &[MeasureId(121)]).expect("evaluate");
3139 let hot = out.get(&MeasureId(121)).expect("Hot");
3140 assert_eq!(
3141 hot.get(&key(&[(2, 20)])),
3142 Some(&CellValue::Bool(false)),
3143 "Price 10 not > 15"
3144 );
3145 assert_eq!(
3146 hot.get(&key(&[(2, 21)])),
3147 Some(&CellValue::Bool(true)),
3148 "Price 20 > 15"
3149 );
3150 }
3151
3152 #[test]
3153 fn evaluate_comparison_against_literal_broadcasts() {
3154 let mut model = Model::new();
3157 let product = CategoryId(2);
3158 model.add_category(product, "Product");
3159 model.add_item(ItemId(20), product, "WidgetA");
3160 model.add_item(ItemId(21), product, "WidgetB");
3161 model.add_measure(Measure {
3162 id: MeasureId(100),
3163 name: Name("Price".into()),
3164 value_type: ValueType::Number,
3165 categories: vec![product],
3166 kind: MeasureKind::Input,
3167 description: None,
3168 });
3169 model.add_measure(Measure {
3170 id: MeasureId(130),
3171 name: Name("Expensive".into()),
3172 value_type: ValueType::Boolean,
3173 categories: vec![product],
3174 kind: MeasureKind::Derived(improv_core_model::Formula::new(Expr::BinaryOp(
3175 BinaryOp::Gt,
3176 Box::new(Expr::Ref(MeasureId(100), DimensionSpec::default())),
3177 Box::new(Expr::Literal(Value::Number(15.0))),
3178 ))),
3179 description: None,
3180 });
3181 let c = |p: &[(CategoryId, ItemId)]| Coordinate::from_pairs(p.iter().copied());
3182 model.set_input(
3183 MeasureId(100),
3184 c(&[(product, ItemId(20))]),
3185 Value::Number(10.0),
3186 );
3187 model.set_input(
3188 MeasureId(100),
3189 c(&[(product, ItemId(21))]),
3190 Value::Number(20.0),
3191 );
3192
3193 let out = evaluate(&model, &[MeasureId(130)]).expect("evaluate");
3194 let exp = out.get(&MeasureId(130)).expect("Expensive");
3195 assert_eq!(exp.get(&key(&[(2, 20)])), Some(&CellValue::Bool(false)));
3196 assert_eq!(exp.get(&key(&[(2, 21)])), Some(&CellValue::Bool(true)));
3197 }
3198
3199 #[test]
3200 fn evaluate_standalone_literal_is_a_scalar_measure() {
3201 let mut model = Model::new();
3204 model.add_measure(Measure {
3205 id: MeasureId(140),
3206 name: Name("TaxRate".into()),
3207 value_type: ValueType::Number,
3208 categories: vec![],
3209 kind: MeasureKind::Derived(improv_core_model::Formula::new(Expr::Literal(
3210 Value::Number(0.08),
3211 ))),
3212 description: None,
3213 });
3214 let out = evaluate(&model, &[MeasureId(140)]).expect("evaluate");
3215 let tr = out.get(&MeasureId(140)).expect("TaxRate");
3216 assert_eq!(tr.len(), 1);
3218 assert_eq!(tr.get(&key(&[])), Some(&CellValue::num(0.08)));
3219 }
3220
3221 #[test]
3222 fn cyclic_dependency_is_rejected() {
3223 let mut model = Model::new();
3225 let mul = |a, b| {
3226 improv_core_model::Formula::new(Expr::BinaryOp(
3227 BinaryOp::Mul,
3228 Box::new(Expr::Ref(a, DimensionSpec::default())),
3229 Box::new(Expr::Ref(b, DimensionSpec::default())),
3230 ))
3231 };
3232 model.add_measure(Measure {
3233 id: MeasureId(1),
3234 name: Name("A".into()),
3235 value_type: ValueType::Number,
3236 categories: vec![],
3237 kind: MeasureKind::Derived(mul(MeasureId(2), MeasureId(2))),
3238 description: None,
3239 });
3240 model.add_measure(Measure {
3241 id: MeasureId(2),
3242 name: Name("B".into()),
3243 value_type: ValueType::Number,
3244 categories: vec![],
3245 kind: MeasureKind::Derived(mul(MeasureId(1), MeasureId(1))),
3246 description: None,
3247 });
3248 assert!(
3249 evaluate(&model, &[MeasureId(1)]).is_err(),
3250 "cycle must be rejected"
3251 );
3252 }
3253
3254 #[test]
3258 fn hand_built_plans() {
3259 use crate::typed::{Dim, TypeInfo};
3260 let ty = TypeInfo {
3261 value_type: ValueType::Number,
3262 dim: Dim::scalar(),
3263 };
3264 let unknown = PlanNode {
3265 kind: PlanNodeKind::FuncCall {
3266 func: FuncId(999),
3267 args: vec![],
3268 },
3269 ty: ty.clone(),
3270 };
3271 let missing = PlanNode {
3272 kind: PlanNodeKind::InputMeasure(MeasureId(77)),
3273 ty,
3274 };
3275 let (ok, err) = timely::execute::execute_directly(move |worker| {
3276 worker.dataflow::<u64, _, _>(|scope| {
3277 let ok = build_coll(&unknown, scope, &HashMap::new(), &HashMap::new()).is_ok();
3278 let err = build_coll(&missing, scope, &HashMap::new(), &HashMap::new())
3279 .err()
3280 .map(|e| e.to_string());
3281 (ok, err)
3282 })
3283 });
3284 assert!(ok);
3285 assert!(err.unwrap().contains("no collection for measure"));
3286 assert_eq!(
3287 crate::builtins::eval(FuncId(999), &[]),
3288 CellValue::Err(err::NAME)
3289 );
3290 }
3291
3292 #[test]
3293 fn cell_ops_cover_every_operator() {
3294 let n = CellValue::num;
3295 let b = CellValue::Bool;
3296 assert_eq!(apply_binary(BinaryOp::Add, &n(2.0), &n(3.0)), n(5.0));
3297 assert_eq!(apply_binary(BinaryOp::Div, &n(1.0), &n(4.0)), n(0.25));
3298 let div0 = CellValue::Err(err::DIV0);
3299 assert_eq!(apply_binary(BinaryOp::Div, &n(1.0), &n(0.0)), div0);
3300 assert_eq!(apply_binary(BinaryOp::Div, &n(0.0), &n(0.0)), div0);
3301 let na = CellValue::Err(err::NA);
3303 assert_eq!(apply_binary(BinaryOp::Add, &na, &div0), na);
3304 assert_eq!(apply_binary(BinaryOp::Mul, &n(1.0), &div0), div0);
3305 assert_eq!(apply_binary(BinaryOp::Eq, &n(1.0), &na), na);
3306 assert_eq!(apply_binary(BinaryOp::And, &b(true), &na), na);
3307 let txt = |s: &str| CellValue::Text(s.into());
3308 assert_eq!(
3309 apply_binary(BinaryOp::Add, &txt("a"), &n(1.0)),
3310 CellValue::Err(err::VALUE)
3311 );
3312 assert_eq!(apply_binary(BinaryOp::Lt, &txt("a"), &txt("b")), b(true));
3313 assert_eq!(apply_binary(BinaryOp::Ge, &txt("a"), &txt("b")), b(false));
3314 assert_eq!(apply_binary(BinaryOp::Le, &b(false), &b(true)), b(true));
3315 assert_eq!(
3316 apply_binary(BinaryOp::Gt, &CellValue::Date(2), &CellValue::Date(1)),
3317 b(true)
3318 );
3319 assert_eq!(
3320 apply_binary(BinaryOp::Lt, &txt("a"), &n(1.0)),
3321 CellValue::Err(err::VALUE)
3322 );
3323 assert_eq!(
3324 apply_binary(BinaryOp::Mul, &n(1e308), &n(10.0)),
3325 CellValue::Err(err::NUM)
3326 );
3327 assert_eq!(apply_unary(UnaryOp::Neg, &na), na);
3328 assert_eq!(apply_binary(BinaryOp::Pow, &n(2.0), &n(10.0)), n(1024.0));
3329 let day = 86_400_000;
3330 let dt = CellValue::Date;
3331 assert_eq!(apply_binary(BinaryOp::Add, &dt(0), &n(2.0)), dt(2 * day));
3332 assert_eq!(apply_binary(BinaryOp::Add, &n(1.0), &dt(0)), dt(day));
3333 assert_eq!(
3334 apply_binary(BinaryOp::Sub, &dt(3 * day), &n(1.0)),
3335 dt(2 * day)
3336 );
3337 assert_eq!(apply_binary(BinaryOp::Sub, &dt(3 * day), &dt(day)), n(2.0));
3338 assert_eq!(
3339 apply_binary(BinaryOp::Mul, &dt(day), &n(2.0)),
3340 CellValue::Err(err::VALUE)
3341 );
3342 assert_eq!(apply_binary(BinaryOp::Pow, &n(0.0), &n(0.0)), n(1.0));
3343 assert_eq!(apply_binary(BinaryOp::Pow, &n(0.0), &n(-1.0)), div0);
3344 assert_eq!(
3345 apply_binary(BinaryOp::Pow, &n(-8.0), &n(0.5)),
3346 CellValue::Err(err::NUM)
3347 );
3348 assert_eq!(
3349 apply_binary(BinaryOp::Concat, &txt("a"), &n(1.5)),
3350 txt("a1.5")
3351 );
3352 assert_eq!(
3353 apply_binary(BinaryOp::Concat, &b(true), &txt("!")),
3354 txt("TRUE!")
3355 );
3356 assert_eq!(apply_binary(BinaryOp::Concat, &txt("a"), &na), na);
3357 assert_eq!(
3358 apply_unary(UnaryOp::Neg, &txt("a")),
3359 CellValue::Err(err::VALUE)
3360 );
3361 for (x, y) in [(true, true), (true, false), (false, true), (false, false)] {
3362 assert_eq!(apply_binary(BinaryOp::And, &b(x), &b(y)), b(x && y));
3363 assert_eq!(apply_binary(BinaryOp::Or, &b(x), &b(y)), b(x || y));
3364 }
3365 for (op, want) in [
3366 (BinaryOp::Lt, true),
3367 (BinaryOp::Le, true),
3368 (BinaryOp::Gt, false),
3369 (BinaryOp::Ge, false),
3370 (BinaryOp::Eq, false),
3371 (BinaryOp::Ne, true),
3372 ] {
3373 assert_eq!(apply_binary(op, &n(1.0), &n(2.0)), b(want), "{op:?}");
3374 }
3375 assert_eq!(apply_unary(UnaryOp::Neg, &n(2.0)), n(-2.0));
3376 assert_eq!(apply_unary(UnaryOp::Not, &b(true)), b(false));
3377 assert!(truthy(&n(1.0)) && !truthy(&n(0.0)));
3378 assert!(truthy(&CellValue::Text("x".into())) && !truthy(&CellValue::Text(String::new())));
3379 assert!(truthy(&CellValue::Date(1)) && !truthy(&CellValue::Date(0)));
3380 assert!(!truthy(&CellValue::Err(0)));
3381 }
3382
3383 #[test]
3384 fn aggregates() {
3385 let rows = |vals: Vec<CellValue>| Rows {
3386 pos: (0..vals.len() as u32).map(|i| vec![i]).collect(),
3387 vals: vals.into_iter().map(|v| vec![v]).collect(),
3388 capacity: 3,
3389 };
3390 let r = rows(vec![
3391 CellValue::num(4.0),
3392 CellValue::num(1.0),
3393 CellValue::num(7.0),
3394 ]);
3395 assert_eq!(
3396 [1, 2, 3, 4, 5, 6].map(|f| reduce::reduce(FuncId(f), &r, &[])),
3397 [12.0, 4.0, 1.0, 7.0, 28.0, 66.0].map(CellValue::num)
3398 );
3399 let b = rows(vec![
3400 CellValue::Bool(true),
3401 CellValue::Bool(false),
3402 CellValue::Bool(true),
3403 ]);
3404 assert_eq!(
3405 [7, 8, 9].map(|f| reduce::reduce(FuncId(f), &b, &[])),
3406 [false, true, false].map(CellValue::Bool)
3407 );
3408 let e = rows(vec![
3410 CellValue::num(1.0),
3411 CellValue::Err(err::NA),
3412 CellValue::Err(err::DIV0),
3413 ]);
3414 assert_eq!(reduce::reduce(FuncId(1), &e, &[]), CellValue::Err(err::NA));
3415 }
3416
3417 #[test]
3418 fn evaluate_unary_literal_operands_and_missing_inputs() {
3419 let mut model = revenue_model();
3420 let (time, product) = (CategoryId(1), CategoryId(2));
3421 let refr = |id| Expr::Ref(MeasureId(id), DimensionSpec::default());
3422 let lit = |n: f64| Expr::Literal(Value::Number(n));
3423 let bin = |op, l, r| Expr::BinaryOp(op, Box::new(l), Box::new(r));
3424 let mk = |id: u32, cats: Vec<CategoryId>, kind| Measure {
3425 id: MeasureId(id),
3426 name: Name(format!("M{id}")),
3427 value_type: ValueType::Number,
3428 categories: cats,
3429 kind,
3430 description: None,
3431 };
3432 let derived = |f| MeasureKind::Derived(improv_core_model::Formula::new(f));
3433 model.add_measure(mk(
3434 150,
3435 vec![product],
3436 derived(Expr::UnaryOp(UnaryOp::Neg, Box::new(refr(100)))),
3437 ));
3438 model.add_measure(mk(
3439 151,
3440 vec![product],
3441 derived(bin(BinaryOp::Mul, lit(2.0), refr(100))),
3442 ));
3443 model.add_measure(mk(
3444 152,
3445 vec![],
3446 derived(bin(BinaryOp::Add, lit(2.0), lit(3.0))),
3447 ));
3448 model.add_measure(mk(160, vec![time], MeasureKind::Input));
3450 model.add_measure(mk(153, vec![time], derived(refr(160))));
3451 let ids = [150, 151, 152, 153].map(MeasureId);
3452 let out = evaluate(&model, &ids).unwrap();
3453 let a = key(&[(2, 20)]);
3454 assert_eq!(out[&MeasureId(150)][&a], CellValue::num(-10.0));
3455 assert_eq!(out[&MeasureId(151)][&a], CellValue::num(20.0));
3456 assert_eq!(out[&MeasureId(152)][&key(&[])], CellValue::num(5.0));
3457 assert!(!out.contains_key(&MeasureId(153)));
3458 assert!(evaluate(&model, &[MeasureId(999)]).unwrap().is_empty());
3460 }
3461}