Skip to main content

improv_engine/
dataflow.rs

1//! Build a differential-dataflow graph from compiled `PlanNode`s and run a
2//! whole `Model` to a set of computed derived-measure values.
3//!
4//! This connects the formula compiler (`compiler`) to the DD substrate proven
5//! by the spike in `lib.rs`. Encoding is the same the spike validated:
6//!   * DD key   = serialized coordinate `CoordKey` = `Vec<(u32,u32)>`
7//!   * DD value = `f64::to_bits()` as `u64` (numeric measures only, v1)
8//!   * diff `R` = `isize`; a final `reduce` collapses to one value per key.
9//!
10//! Scope (v1, numeric): InputMeasure, Literal, MapUnary(Neg), MapBinary
11//! (arithmetic + comparison/logical encoded as 1.0/0.0), Join (dimension
12//! alignment), Aggregate (SUM/AVG/MIN/MAX), and scalar FuncCall built-ins
13//! (ABS/ROUND/FLOOR/CEIL/SQRT/NEG, MIN2/MAX2). Non-numeric derived values
14//! (Text/Boolean stored as such) and Date values are carried in `CellValue`;
15//! comparison/logical results live in the numeric lane as 1.0/0.0.
16
17use 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
44/// A DD collection of `(coordinate, cell value)`.
45pub(crate) type Coll<G> = Collection<G, (CoordKey, CellValue), isize>;
46
47/// Recursively build a collection for a plan node.
48///
49/// `inputs` maps input-measure ids to their base DD collections. A `Ref` to a
50/// *derived* measure resolves to its already-built collection in `derived`.
51pub(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            // Operands are dimension-aligned by an enclosing Join (or equal
75            // dims). A *literal* operand has no collection of its own; fold it
76            // into the map closure so it broadcasts over the other operand.
77            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                    // Both literals: a scalar result at the empty coordinate.
89                    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            // Broadcast the left value onto the union coordinate. A MapBinary
111            // above re-aligns this with its right operand on their shared
112            // categories, so it sees both values.
113            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            // The domain: every item combination of the grid, or the cells of
164            // the arguments over all the result's categories.
165            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            // Each argument's cells on the domain, tagged with its position;
187            // per coordinate the lowest position wins.
188            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                // Values arrive sorted, so the first is the lowest position.
202                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            // Rows: every read measure aligned on the coordinate.
226            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            // Each cell moves to the item `-offset` away: the value at item i
304            // is the input's value at item i + offset.
305            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            // Where nothing moved in, the default, at the input's own cells.
317            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            // A standalone literal is a *scalar* measure: one value at the empty
331            // coordinate, which broadcasts (via Join) into any formula that
332            // references it. Build a constant single-cell collection.
333            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            // Literal arguments fold into the map; the others are aligned by
344            // successive broadcast joins into one row of values per coordinate.
345            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                // No collection argument: a constant at the empty coordinate.
354                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            // The test over the result's categories: joined with the spanning
388            // branch when the test lacks some of them.
389            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                // The branch's categories are a subset of the domain's, so the
405                // joined coordinate is the domain's.
406                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
417/// A reduction row as the `reduce` sees it: (item positions along the
418/// reduced categories, the first reduced category's item), then the values.
419pub(crate) type AggRow<V = CellValue> = ((Vec<u32>, u32), Vec<V>);
420
421/// The pieces of an `Aggregate` plan node, for [`build_aggregate`].
422pub(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
434/// Each reduced category's item positions, for [`agg_key`].
435pub(crate) type AggOrder = Vec<(u32, HashMap<u32, u32>)>;
436
437/// The item positions along `order`'s categories.
438pub(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
452/// A reduction row at `k`, values `vs`: its group key (`k` on `gb`) and the
453/// row as the reduction sees it (its item positions along `order`, its item
454/// of the first reduced category, the values).
455pub(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            // Unknown items (no model order) sort by id after known ones.
466            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
476/// A group's reduction input (sorted rows, with multiplicities) as the
477/// kernels take it: the rows, the parameters (`consts`, the joined ones
478/// filled from the first row), and each row's item.
479pub(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                // Joined parameters follow the inputs, in order.
499                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
519/// A reduction row under a `WHERE` condition `t`: kept where it is true,
520/// every value the error (as `err` makes it a value) where it is an error,
521/// else dropped.
522pub(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
534/// Every item combination of `grid`, row-major (the last category fastest).
535pub(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
549/// A RELABEL's maps as the renamed categories and each renamed item's target.
550pub(crate) type RemapTable = (Vec<u32>, HashMap<(u32, u32), (u32, u32)>);
551
552/// [`RemapTable`] of `maps`.
553pub(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
561/// `k` renamed by a [`RemapTable`]; `None` where a renamed item has no target.
562pub(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
579/// A scalar call: `template`'s literals with `vals` in the other slots.
580pub(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
596/// A reduction's rows: the reduced measures aligned on their coordinate
597/// (and filtered by `pred`), parameters appended, re-keyed to the group,
598/// each row carrying its item positions so the reduction sees item order.
599fn 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    // The reduced measures join on the categories they share; a category
622    // only some of them range over broadcasts (the rows gain it).
623    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    // Parameters: literals fold; the rest join per row on the categories they
651    // share with the rows so far. Categories only a parameter has join in
652    // (broadcast): each of its cells sees every row (a lookup).
653    // ponytail: a lookup costs |table| × |parameter cells| rows; index the
654    // table by key if lookups over large tables ever matter.
655    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    // (group key, ((positions, item of the first reduced category), values))
686    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        // A list of results laid over the landing categories row-major: the
692        // k-th result at the k-th cell of their product (last fastest).
693        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, &params)) {
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, &params), 1isize));
714            }
715        }));
716    }
717    // One result per item of the (single) reduced category.
718    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, &params),
724                Some(w) => windows(func, &rs, &params, 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
740/// One result per row: the reduction over the rows whose position lies in
741/// the row's window (positions along the single reduced category).
742pub(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    // FIRST and LAST are the group's first and last items with a cell.
751    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
778/// A constant at the empty coordinate (broadcasts into any join).
779fn 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/// Broadcast-join two collections on `keys`, the categories both range over.
791/// Every pair that agrees on `keys` yields one row at the union coordinate
792/// carrying both values, so either side may have extra dimensions. This is the
793/// single alignment primitive for `Join`, `MapBinary` and two-arg `FuncCall`.
794#[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/// [`align`] for any left value type (n-ary calls carry a `Vec` of values).
807#[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
825/// The categories both plan nodes range over: their broadcast join key.
826pub(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
835/// If a plan node is a literal, return its `CellValue` (for folding a constant
836/// operand into a `MapBinary` map rather than materializing a collection).
837pub(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    // An error operand propagates (the left one first).
846    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    // Ordering compares like values: numbers, text (by code point), dates,
866    // booleans (FALSE < TRUE).
867    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    // Dates: ± days, and the difference of two dates in days.
874    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
926/// Truthiness for logical ops: `Bool` directly, a nonzero `Num`, a non-empty
927/// `Text`; errors are falsy.
928fn 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
938/// Project a coordinate key down to a subset of category ids (proven in
939/// [`improv_verified::keys::project`]).
940pub(crate) fn project(k: &CoordKey, keep: &[u32]) -> CoordKey {
941    improv_verified::keys::project(k, keep)
942}
943
944/// Merge two sorted coordinate keys (right's extra dims added to left; shared
945/// dims agree, they were the join key). A proven merge:
946/// [`improv_verified::keys::union_keys`].
947pub(crate) fn union_keys(a: &CoordKey, b: &CoordKey) -> CoordKey {
948    improv_verified::keys::union_keys(a, b)
949}
950
951/// Compute the derived measures transitively needed by `targets`, in
952/// dependency order (a measure comes after every derived measure it
953/// references). Rejects cycles.
954pub(crate) fn derived_build_order<'m>(
955    model: &'m Model,
956    targets: &[MeasureId],
957) -> Result<Vec<(MeasureId, &'m Formula)>, EngineError> {
958    // Derived measures are the graph's nodes. A measure's references to itself
959    // are a recurrence (`Bal = PREV(Bal, Time, 1, Open) + Flow`), checked by
960    // the compiler, not a cycle; refs to inputs or unknown measures (which the
961    // compiler reports) are no edges.
962    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    // Proven: the reachable nodes, each once, after their dependencies; an
990    // error exactly when a cycle is reachable.
991    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
1000/// Compile every derived measure `targets` transitively needs, in build order.
1001pub(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
1012/// Build each plan's collection, collapsed to one value per key, in order (so
1013/// a later plan can reference an earlier one). A plan that fails to build, such
1014/// as a ref to an input measure with no collection, is left out.
1015pub(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
1042/// Run `model` to completion and return computed values for the requested
1043/// derived measures: `measure -> (coordinate-key -> value)`.
1044///
1045/// Numeric input + derived measures. Supports **multi-layer** derivation: a
1046/// derived measure may reference other derived measures; they are built in
1047/// topological order so each layer feeds the next. Cyclic dependencies are
1048/// rejected.
1049pub fn evaluate(
1050    model: &Model,
1051    targets: &[MeasureId],
1052) -> Result<HashMap<MeasureId, HashMap<CoordKey, CellValue>>, EngineError> {
1053    // Compile every derived measure transitively needed by `targets`, in
1054    // dependency (topological) order.
1055    let plans = compile_plans(model, targets)?;
1056    // A FIXPOINT is solved host-side, round by round, by the live engine.
1057    // (It feeds only the inputs of their measure's type, as edits do.)
1058    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    // Gather input cells to feed, grouped by measure. Every model value type
1067    // maps into the DD lane: Number, Boolean, Text, Enum, DateTime (as
1068    // CellValue::Date millis), and Error.
1069    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    // A session per input measure, with or without cells: an empty measure
1084    // is an empty collection (a lazy IF may never need it), not a missing one.
1085    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        // Feed all input cells at time 0, advance, and run to completion.
1122        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        // Closing the sessions (on return) lets `execute_directly` run the
1129        // dataflow to completion before it returns.
1130        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    /// Two numeric inputs A (`MeasureId(100)`) and B (`MeasureId(101)`) and a
1151    /// derived `R = A op B` (or `B op A` when `!a_left`) at `MeasureId(102)`.
1152    /// Category `c` has items `10*c + 0..3`; cells are `(item ids, value)` in
1153    /// the order of the measure's categories.
1154    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    /// Evaluate `R` (`MeasureId(102)`) to a `CoordKey -> f64` map.
1211    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    /// Price[Time,Product] and Discount[Product]: (cats, cells) pairs.
1223    #[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    /// `Price op Discount` (larger on the left) and `Discount op Price`
1242    /// (larger on the right), exact values for every arithmetic operator.
1243    #[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    /// Run hand-built plan `plan` as derived `MeasureId(102)` over `cells`.
1275    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    /// Both operands carry an extra dim the other lacks:
1312    /// `A[T,P] * B[P,R]` lands on `[T,P,R]` in the dataflow builder, whichever
1313    /// side is on the left and whether or not a `Join` feeds the map; the
1314    /// compiler accepts it (they share P), and refuses disjoint operands.
1315    #[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        // The compiler accepts the shared-category join too (a matrix
1380        // product's first step) and agrees with the hand-built plans.
1381        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        // Disjoint categories are still refused.
1389        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    /// `session::Engine` (incremental) agrees with `evaluate` on broadcasts,
1406    /// both at build time and after edits to either operand.
1407    #[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                // Edit the smaller operand and add a cell to the larger one.
1427                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    /// All coordinates over `cats`, two items per category (`10c`, `10c+1`).
1453    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        /// `evaluate(A op B)` equals a naive reference: iterate the union's
1469        /// Cartesian coordinates, look up each broadcast operand by projecting,
1470        /// and apply `op` where both exist. Non-broadcastable dims must error.
1471        #[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            // Refused only when neither is a subset and they share nothing.
1489            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    // The canonical Time x Product revenue model with known results.
1517    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        // Price[Product] (input)
1528        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        // Quantity[Time,Product] (input)
1537        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        // Revenue[Time,Product] = Price[Product] * Quantity[Time,Product]
1546        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        // Price
1561        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        // Quantity
1572        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    /// Add derived `name = text` over `cats` to `m` as measure `id`.
1596    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    /// The customer's case (P1-2): Person has four items, Authored a cell
1617    /// for two, Vol for all four. COALESCE and FILL give all four cells;
1618    /// IFNA and an IF on ISNUMBER keep Authored's two.
1619    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        // Three arguments, the first with no cell anywhere but p3's Vol.
1670        define(
1671            &mut m,
1672            204,
1673            vec![person],
1674            "D = COALESCE(Authored, IF(Vol > 25, Vol), -1)",
1675        );
1676        // A per-Person default broadcast under Person × Year: Mail's cells
1677        // only (Vol adds no Year).
1678        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        // Error-only arguments take the others' type.
1722        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        // FILL's default must not range beyond x.
1728        assert!(ty("X = FILL(Authored, Mail)").is_err());
1729        // Categories must nest.
1730        let e = ty("X = COALESCE(SUM(Mail OVER Person), Authored)");
1731        assert!(
1732            matches!(e, Err(crate::compiler::CompileError::DimensionMismatch(_))),
1733            "{e:?}"
1734        );
1735        // FILL needs the model's categories.
1736        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        // Scalar: the first argument with a value.
1740        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    /// The edit path: a cell appearing or going away in an earlier argument
1749    /// moves the winner, without a rebuild.
1750    #[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        // A new Person item gets its filled cell. Items are fixed per engine
1767        // build, so this takes a new Engine (a structural rebuild).
1768        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    /// COUNTUNIQUE and RANK.DENSE live (P1-4, P3-1): deleting one of two
1778    /// rows that share a key leaves the distinct count as it was, deleting
1779    /// the other lowers it, and the dense ranks follow; the live engine and
1780    /// a fresh evaluation agree after each edit.
1781    #[test]
1782    fn countunique_and_dense_rank_follow_deletes() {
1783        let person = CategoryId(1);
1784        let mut m = people();
1785        // Vol: 10, 20, 30, 40; make p2 and p3 share the key 20.
1786        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        // Delete p2: 20 is still there (p3).
1802        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        // Delete p3: now 20 is gone.
1809        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        // Insert a duplicate of 40: unchanged count, a shared rank.
1813        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        // Random batches of sets and clears (values 0..4, so keys repeat):
1818        // the count is count(DISTINCT) of the cells after every batch.
1819        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    /// Two roles of Person, Src and Dst (items a, b, c; Dst also has z, no
1850    /// Src namesake), and Iter.
1851    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        // Dst lists its items in another order: matching is by name.
1861        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        // a→b and b→a reciprocal, a→c one way, c→a with weight 0.
1883        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        // Same values, over Src; z (no Src namesake) is dropped.
1910        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        // a→b with b→a: reciprocal. a→c has no c→a > 0; c→a has no a→c cell.
1923        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        // SWAP is symmetric in its categories.
1929        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        // The rules.
1936        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    /// Measures reduced together need only reduce the same categories; the
1964    /// rest broadcast: SUMPRODUCT(Rs, W OVER Src) with Rs over Src × Iter and
1965    /// W over Src × Dst is Σ_src Rs·W per Dst × Iter (a matrix-vector
1966    /// product, PageRank's step).
1967    #[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        // Each still reduces the OVER categories it has; with none reduced,
1994        // the measures must agree (no silent cross product).
1995        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        // The customer's lookup: X over Dst x Iter, the key over Dst alone.
2004        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        // XLOOKUP's return measure may range beyond its key: R over Dst × Iter
2019        // looked up by name along Dst, per Src.
2020        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    /// IF over dimensions: the test over Product, the branches over
2037    /// Time×Product. The then-branch's coordinates extend the test, an
2038    /// unselected branch's missing cell or error never matters, and
2039    /// IFERROR/CLEARZERO follow.
2040    #[test]
2041    fn if_family_over_dimensions() {
2042        let (time, product) = (CategoryId(1), CategoryId(2));
2043        let mut m = revenue_model();
2044        // Quantity for 2026/WidgetB is missing: no Revenue cell there.
2045        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        // WidgetA (price 10): else-branch 0 wherever Revenue exists.
2085        assert_eq!(at(200, 10, 20), Some(n(0.0)));
2086        assert_eq!(at(200, 11, 20), Some(n(0.0)));
2087        // WidgetB (price 20): Revenue, and no cell where Revenue is missing.
2088        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        // Ratio divides by zero for WidgetA; IFERROR replaces it.
2092        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        // The error branch is never selected for WidgetA, so it never shows.
2096        assert_eq!(at(203, 10, 20), Some(n(0.0)));
2097        assert_eq!(at(203, 10, 21), Some(n(100.0)));
2098        // A text IF over Product only.
2099        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        // CLEARERROR without an alternative drops the error cells.
2109        assert_eq!(at(205, 10, 20), None);
2110        assert_eq!(at(205, 10, 21), Some(n(100.0)));
2111    }
2112
2113    /// The P3 dimensional forms end to end: positions and names, neighbours,
2114    /// running windows, several OVER categories, ALL EXCEPT, WHERE, and the
2115    /// ordered reductions.
2116    #[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        // Quantity: 2025 A 100, B 50; 2026 A 120, B 80.
2182        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    /// A recurrence: a balance carried forward along Time, per Product,
2215    /// opening from a per-product measure; it updates incrementally.
2216    #[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        // Open[Product] = Price; Bal = PREV(Bal, Time, 1, Open) + Quantity.
2235        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        // WidgetA: price 10; 10+100 = 110, +120 = 230, +10 = 240.
2250        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        // WidgetB: price 20; 70, 150; no 2027 Quantity, so no 2027 balance.
2254        assert_eq!(at(11, 21), Some(CellValue::num(150.0)));
2255        assert_eq!(at(12, 21), None);
2256        // Incremental: changing 2025's Quantity re-runs WidgetA's chain.
2257        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        // Not a recurrence: a self-reference outside PREV, or along a
2266        // category the measure lacks.
2267        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    /// A recurrence across a gap (no cell for an item) restarts from the
2275    /// default; an error in the body flows on as that error; a recurrence
2276    /// with no default has no cells.
2277    #[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        // WidgetA: 2025 and 2027 only (2026 missing: a gap).
2284        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        // WidgetB 2025 Quantity is 50: 1 / 0 is #DIV/0!, carried forward.
2317        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    /// Windows anchored at LAST and ending at FIRST, an empty WHERE group,
2326    /// and TEXTJOIN of booleans.
2327    #[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    /// LET and LAMBDA expand at parse time; SCAN and REDUCE are folds
2366    /// along a category (recurrences under the hood).
2367    #[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        // Parse errors: LET without a body, an uncalled LAMBDA, a fold
2412        // without OVER, a LAMBDA with the wrong arity.
2413        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    /// Operands sharing a category join over it: the matrix product is
2429    /// `AB = A * B` then `SUM(AB OVER K)`.
2430    #[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        // A = [[1, 2], [3, 4]], B = [[5, 6], [7, 8]].
2453        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        // [[19, 22], [43, 50]].
2479        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    /// Named LAMBDAs: defined once, called by name, expanded when compiled
2486    /// (so redefining one changes every use); their errors.
2487    #[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        // Quantity 100, Price 10.
2512        assert_eq!(at(&m, 200), Some(CellValue::num(0.9)));
2513        assert_eq!(at(&m, 201), Some(CellValue::num(1.98)));
2514        // Redefine Margin: both uses follow.
2515        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        // A missing lambda is an error, not a crash.
2520        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        // Expansion: a cycle (only by hand-editing) and arity errors.
2541        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    /// `INTO R, C` lays a list over two categories, row-major: SORT's
2564    /// four values over a 2 × 2 grid, and LINEST's statistics as Excel's
2565    /// 5 × 2 block.
2566    #[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        // 50, 80, 100, 120 row-major over r0..r1 × c0..c1.
2601        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        // Widget A: y = 100, 120 over x = 1, 2: slope 20, intercept 80; r² 1.
2606        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        // Two x's (Over::Leading): Quantity on X and X² over Time per
2611        // product, 3 coefficients into one row; the options after the x's.
2612        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        // The GUI printer and the parser agree on the form.
2639        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    /// SEQUENCE and MAKEARRAY are positions; SELECTLIST is SELECT with a
2678    /// lookup over its own category; SORTBY takes several keys.
2679    #[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        // Price spread over Time × Product (the shape of Quantity); each Rank
2695        // item looks up the product whose price is 10 * its position.
2696        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        // Products by price descending: B then A (the second key never ties).
2710        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        // BYROW/BYCOL: one value per item of the other categories.
2732        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    /// Array results land along the INTO category: SORT and UNIQUE of each
2768    /// product's quantities over Time, into a Rank category.
2769    #[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        // INTO is required for a list result and refused for one value; it
2806        // must not be one of the measure's own categories.
2807        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    /// A parameter over a category the measure lacks broadcasts: each of its
2829    /// cells sees the whole table (the shape lookups need).
2830    #[test]
2831    fn parameters_beyond_the_table_broadcast() {
2832        let (time, product) = (CategoryId(1), CategoryId(2));
2833        let mut m = revenue_model();
2834        // Sep[Time] per year; J joins each product's quantities with it.
2835        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    /// A parameter that is a measure (one value per group) joins per row.
2856    #[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    /// The else-branch spans (the then-branch is a literal), the test is an
2874    /// error somewhere, and an aggregation meets an error in its group.
2875    #[test]
2876    fn if_else_spans_and_errors_flow() {
2877        let (time, product) = (CategoryId(1), CategoryId(2));
2878        let mut m = revenue_model();
2879        // Price - 10 is 0 for WidgetA: Ratio is #DIV/0! there.
2880        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        // WidgetA: the test is #DIV/0!, so each spanned cell is that error.
2898        assert_eq!(at(201, 10, 20), Some(CellValue::Err(err::DIV0)));
2899        assert_eq!(at(201, 11, 20), Some(CellValue::Err(err::DIV0)));
2900        // WidgetB: Ratio 0.1 > 0 selects the literal 0, at Revenue's cells.
2901        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        // No else-branch: a false test is no cell.
2905        assert_eq!(at(203, 10, 20), None);
2906        assert_eq!(at(203, 10, 21), Some(CellValue::num(1000.0)));
2907    }
2908
2909    /// n-ary scalar calls align every collection argument and fold literals;
2910    /// an aggregation of booleans and a product reduce typed values.
2911    #[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        // Revenue[2025,A] = 10*100 = 1000
2955        assert_eq!(
2956            rev.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
2957            Some(1000.0)
2958        );
2959        // Revenue[2025,B] = 20*50 = 1000
2960        assert_eq!(
2961            rev.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
2962            Some(1000.0)
2963        );
2964        // Revenue[2026,A] = 10*120 = 1200
2965        assert_eq!(
2966            rev.get(&key(&[(1, 11), (2, 20)])).and_then(|v| v.as_num()),
2967            Some(1200.0)
2968        );
2969        // Revenue[2026,B] = 20*80 = 1600
2970        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        // RevenueByProduct[Product] = SUM(Revenue OVER Time), where Revenue is
2980        // itself a derived measure -> two dataflow layers.
2981        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), // SUM
2995                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        // WidgetA: 1000 + 1200 = 2200 ; WidgetB: 1000 + 1600 = 2600
3003        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    // Scalar FuncCall evaluation over the revenue model: ABS (unary) and MIN2
3015    // (2-arg join path). Comparison-as-numeric is checked separately.
3016    #[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        // Profit[Time,Product] = Price[Product] - Quantity[Time,Product]
3031        // (negative everywhere: 10-100, 20-50, ...).
3032        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        // AbsProfit = ABS(Profit)
3043        model.add_measure(mk(
3044            111,
3045            "AbsProfit",
3046            vec![time, product],
3047            Expr::Call(FuncId(10), vec![refr(MeasureId(110))]),
3048        ));
3049        // MinPQ[Time,Product] = MIN2(Price[Product], Quantity[Time,Product])
3050        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        // AbsProfit = |Price - Quantity|.
3060        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        ); // |10-100|
3065        assert_eq!(
3066            ap.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
3067            Some(30.0)
3068        ); // |20-50|
3069        assert_eq!(
3070            ap.get(&key(&[(1, 11), (2, 20)])).and_then(|v| v.as_num()),
3071            Some(110.0)
3072        ); // |10-120|
3073        assert_eq!(
3074            ap.get(&key(&[(1, 11), (2, 21)])).and_then(|v| v.as_num()),
3075            Some(60.0)
3076        ); // |20-80|
3077
3078        // MinPQ = min(Price, Quantity) = Price everywhere (Price<Quantity).
3079        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        // Determinism: a second run yields identical results.
3090        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    // Comparison result encoded as 1.0/0.0 in the numeric lane:
3096    // Hot[Product] = Price > Threshold, an input Threshold[Product] = 15.
3097    #[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        // Threshold[Product] = 15 for both products (input).
3104        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        // Hot[Product] = Price > Threshold  (Price A=10 -> 0.0, B=20 -> 1.0).
3125        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        // Expensive[Product] = Price > 15  (a *literal* RHS, no Threshold
3155        // measure) — exercises literal folding + broadcast in MapBinary.
3156        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        // TaxRate = 0.08  (a bare literal RHS, no measure ref) evaluates to a
3202        // single scalar cell at the empty coordinate.
3203        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        // One cell, at the empty coordinate.
3217        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        // A -> B -> A cycle must error, not loop forever.
3224        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    /// A plan the compiler would never emit (an unknown function) still
3255    /// builds; its cells are `#NAME?`, and a ref to an absent measure is an
3256    /// error.
3257    #[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        // Errors propagate, the left one first; a non-number is #VALUE!.
3302        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        // An error in the group is the result: the first in item order.
3409        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        // A ref to an input measure with no cells has no collection: skipped.
3449        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        // An unknown target is ignored here (the compiler reports bad refs).
3459        assert!(evaluate(&model, &[MeasureId(999)]).unwrap().is_empty());
3460    }
3461}