Skip to main content

improv_engine/
session.rs

1//! Live incremental edit API.
2//!
3//! `evaluate()` runs the whole model once. `Engine` instead builds the
4//! differential-dataflow graph **once** and keeps it alive on a dedicated
5//! worker thread, so subsequent input-cell edits propagate as *deltas* —
6//! only affected coordinates (and their downstream measures) recompute. This
7//! is the "edits → deltas → recompute without full rebuild" API the engine
8//! steering calls for (see AGENT_ENGINE_STEERING §6.2).
9//!
10//! Scope: the measure *structure* (which measures exist, their formulas and
11//! dimensions) is fixed at `Engine::new` time. Editing **input cell values**
12//! is incremental. Adding/removing measures or changing a formula requires a
13//! new `Engine` (a structural rebuild) — callers rebuild on structural change,
14//! which is cheap relative to a data-edit-heavy session.
15//!
16//! Threading: differential dataflow's worker and `InputSession`s are not
17//! `Send`-friendly to hold across calls, so the worker lives on its own thread
18//! and communicates over channels. `Engine` is the `Send` handle.
19
20use crate::plan::{PlanNode, PlanNodeKind};
21use crate::{encode_coord, CellValue, CoordKey};
22use improv_core_model::{MeasureId, MeasureKind, Model, Value, ValueType};
23use std::collections::{HashMap, HashSet};
24use std::sync::mpsc::{Receiver, Sender};
25use std::sync::{Arc, Mutex};
26use std::thread::JoinHandle;
27
28use crate::dataflow::{build_derived, compile_plans, EngineError};
29use differential_dataflow::input::InputSession;
30use timely::dataflow::operators::probe::Handle as ProbeHandle;
31
32/// A single input-cell edit: set `measure[coord]` to `value` (or remove it when
33/// `value` is `None`).
34#[derive(Debug, Clone)]
35pub struct Edit {
36    pub measure: MeasureId,
37    pub coord: CoordKey,
38    pub value: Option<CellValue>,
39}
40
41/// The engine cell an input of type `ty` holding `val` contributes: `None`
42/// for a value of another type (text in a number measure, an error), which is
43/// no cell. One rule for the seed and for callers' edits, so an edit and a
44/// rebuild always agree.
45pub fn input_cell(ty: ValueType, val: &Value) -> Option<CellValue> {
46    let typed = matches!(
47        (ty, val),
48        (ValueType::Number, Value::Number(_))
49            | (ValueType::Boolean, Value::Boolean(_))
50            | (ValueType::Text, Value::Text(_))
51            | (ValueType::DateTime, Value::DateTime(_))
52    );
53    CellValue::from_model_value(val).filter(|_| typed)
54}
55
56/// A snapshot of a computed derived measure after an edit round.
57pub type MeasureValues = HashMap<CoordKey, CellValue>;
58
59/// Every tracked derived measure's values after an edit round.
60///
61/// Copy-on-write: cloning a `Snapshot` is a reference-count bump, and the
62/// engine shares unchanged measures between successive snapshots. An edit
63/// round copies the outer map (one `Arc` per measure) and only the measures
64/// whose cells changed, and only if a caller still holds the previous
65/// snapshot; drop the old one first and an edit costs just its delta.
66// ponytail: a changed measure is copied whole while the previous snapshot is
67// alive, so one huge measure still costs O(its cells) per edit for callers
68// that keep the old snapshot; a persistent map (`im`) per measure lifts that.
69pub type Snapshot = Arc<HashMap<MeasureId, Arc<MeasureValues>>>;
70
71enum Cmd {
72    /// Apply a batch of edits, advance time, and return the new full snapshot
73    /// of every tracked derived measure.
74    Edit(Vec<Edit>, Sender<Snapshot>),
75    Shutdown,
76}
77
78/// A live, incremental evaluation handle over a fixed model structure.
79pub struct Engine {
80    tx: Sender<Cmd>,
81    worker: Option<JoinHandle<()>>,
82    /// The derived measures this engine tracks (its `evaluate` targets).
83    targets: Vec<MeasureId>,
84    /// Current known value of each input cell, so `set` can emit the correct
85    /// retract+assert delta for a cardinality-one cell.
86    current: HashMap<(MeasureId, CoordKey), CellValue>,
87    /// The compiled plans (a FIXPOINT as its first-iterate and step parts),
88    /// to tell whether a model with more items still compiles to the same
89    /// graph ([`Engine::serves`]).
90    plans: Vec<(MeasureId, PlanNode)>,
91}
92
93impl Engine {
94    /// Build a live engine for `model`, tracking `targets` (derived measures).
95    /// Seeds the graph with the model's current input cells and returns a handle
96    /// plus the initial snapshot.
97    pub fn new(model: &Model, targets: &[MeasureId]) -> Result<(Engine, Snapshot), EngineError> {
98        Self::build(model, targets, true)
99    }
100
101    /// [`Engine::new`]; with `host_steps` false every FIXPOINT step is a
102    /// dataflow round (the path a body the host evaluator does not cover
103    /// takes), so tests can hold the two paths to the same answer.
104    fn build(
105        model: &Model,
106        targets: &[MeasureId],
107        host_steps: bool,
108    ) -> Result<(Engine, Snapshot), EngineError> {
109        // Compile plans up front (outside the worker), so a compile error is
110        // reported synchronously rather than on the worker thread.
111        let plans = compile_plans(model, targets)?;
112        let (plans, mut fixes) = split_fixpoints(model, plans)?;
113        if !host_steps {
114            fixes.iter_mut().for_each(|f| f.host = None);
115        }
116
117        // Which input measures need sessions? Every input measure referenced by
118        // any tracked plan, plus any that already has cells. We over-approximate
119        // with "all input measures in the model" — extra empty sessions are
120        // cheap and keep later edits to any input measure valid.
121        let mut input_ids: Vec<MeasureId> = model
122            .measures
123            .iter()
124            .filter(|(_, m)| matches!(m.kind, MeasureKind::Input))
125            .map(|(id, _)| *id)
126            .collect();
127        // A FIXPOINT's iterate and its solution ride on sessions of their own.
128        input_ids.extend(fixes.iter().flat_map(|f| [f.r, f.id]));
129
130        // Seed edits: the model's current input cells of their measure's
131        // type (a mistyped value, e.g. text in a number measure, is no cell).
132        let mut seed: Vec<(MeasureId, CoordKey, CellValue)> = Vec::new();
133        let mut current: HashMap<(MeasureId, CoordKey), CellValue> = HashMap::new();
134        for ((mid, coord), val) in &model.inputs {
135            let ty = model.measures.get(mid).map(|m| m.value_type);
136            if let Some(cv) = ty.and_then(|t| input_cell(t, val)) {
137                let key = encode_coord(coord);
138                seed.push((*mid, key.clone(), cv.clone()));
139                current.insert((*mid, key), cv);
140            }
141        }
142
143        let (cmd_tx, cmd_rx) = std::sync::mpsc::channel::<Cmd>();
144        let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
145        let targets_owned = targets.to_vec();
146        let plans_owned = plans.clone();
147        let seed_owned = seed;
148
149        let worker = std::thread::spawn(move || {
150            worker_loop(input_ids, plans_owned, fixes, seed_owned, cmd_rx, ready_tx);
151        });
152
153        // Wait for the worker to build the graph and apply the seed.
154        ready_rx
155            .recv()
156            .map_err(|_| EngineError::Unsupported("engine worker failed to start".into()))?;
157
158        let mut engine = Engine {
159            tx: cmd_tx,
160            worker: Some(worker),
161            targets: targets_owned,
162            current,
163            plans,
164        };
165
166        // Fetch the initial snapshot with an empty edit round.
167        let snapshot = engine.apply(Vec::new())?;
168        Ok((engine, snapshot))
169    }
170
171    /// Set an input cell to `value` and return the recomputed snapshot of all
172    /// tracked derived measures. Incremental: only affected coordinates flow.
173    pub fn set(
174        &mut self,
175        measure: MeasureId,
176        coord: CoordKey,
177        value: f64,
178    ) -> Result<Snapshot, EngineError> {
179        self.apply(vec![Edit {
180            measure,
181            coord,
182            value: Some(CellValue::num(value)),
183        }])
184    }
185
186    /// Remove an input cell and return the recomputed snapshot.
187    pub fn clear(&mut self, measure: MeasureId, coord: CoordKey) -> Result<Snapshot, EngineError> {
188        self.apply(vec![Edit {
189            measure,
190            coord,
191            value: None,
192        }])
193    }
194
195    /// Apply a batch of edits atomically (one time step) and return the new
196    /// snapshot of all tracked derived measures. An edit's coordinate may list
197    /// its categories in any order: it is sorted into the engine's key form,
198    /// which the key kernels (`improv_verified::keys`) require.
199    pub fn apply(&mut self, mut edits: Vec<Edit>) -> Result<Snapshot, EngineError> {
200        for e in &mut edits {
201            e.coord.sort_unstable();
202        }
203        // Update our shadow of current input values so retract deltas are exact.
204        for e in &edits {
205            let k = (e.measure, e.coord.clone());
206            match &e.value {
207                Some(v) => {
208                    self.current.insert(k, v.clone());
209                }
210                None => {
211                    self.current.remove(&k);
212                }
213            }
214        }
215        let (reply_tx, reply_rx) = std::sync::mpsc::channel();
216        self.tx
217            .send(Cmd::Edit(edits, reply_tx))
218            .map_err(|_| EngineError::Unsupported("engine worker is gone".into()))?;
219        reply_rx
220            .recv()
221            .map_err(|_| EngineError::Unsupported("engine worker dropped the reply".into()))
222    }
223
224    /// The derived measures this engine tracks.
225    pub fn targets(&self) -> &[MeasureId] {
226        &self.targets
227    }
228
229    /// Whether this engine computes `model` as a new engine would, so a
230    /// model that differs from the one it was built on only in input cells
231    /// and in new items can be fed to it as edits. False when a plan reads
232    /// the item list of a category that gained items: `POSITION`, `@ITEM`,
233    /// `@ITEMCOUNT`, `PREV`/`NEXT`, recurrences, windows, per-item and
234    /// `INTO` reductions, `FILL` (its grid), `RELABEL`/`SWAP` (their item
235    /// maps), and every
236    /// reduction other than `SUM` … `XOR`, `COUNT`, `COUNTA` and
237    /// `COUNTUNIQUE` (which read only their rows' values: a new item
238    /// reaches them as data, sorted last as an unknown item sorts). New
239    /// items must have ids above every item of their category and come
240    /// after them in its item order (append-only minting does both).
241    pub fn serves(&self, model: &Model) -> Result<bool, EngineError> {
242        let (now, _) = split_fixpoints(model, compile_plans(model, &self.targets)?)?;
243        Ok(now.len() == self.plans.len()
244            && now
245                .iter()
246                .zip(&self.plans)
247                .all(|((a, p), (b, q))| a == b && item_blind(p) == item_blind(q)))
248    }
249}
250
251/// `p` without the item order of the reductions that read only their rows'
252/// values (see [`Engine::serves`]).
253fn item_blind(p: &PlanNode) -> PlanNode {
254    let mut p = p.clone();
255    blind(&mut p);
256    p
257}
258
259fn blind(p: &mut PlanNode) {
260    use crate::plan::PlanNodeKind as K;
261    match &mut p.kind {
262        K::MapUnary(_, a) => blind(a),
263        K::MapBinary(_, a, b)
264        | K::Join {
265            left: a, right: b, ..
266        } => {
267            blind(a);
268            blind(b);
269        }
270        K::Aggregate {
271            inputs,
272            params,
273            pred,
274            func,
275            order,
276            window,
277            per_item,
278            into,
279            ..
280        } => {
281            inputs.iter_mut().chain(params.iter_mut()).for_each(blind);
282            if let Some(x) = pred {
283                blind(x);
284            }
285            // SUM … XOR, COUNT, COUNTA, COUNTUNIQUE: values only, no
286            // positions or capacity.
287            let values_only = matches!(func.0, 1..=9 | 1005 | 1006 | 1100);
288            if values_only && window.is_none() && !*per_item && into.is_empty() {
289                order.clear();
290            }
291        }
292        K::Recurrence { refs, .. } => refs.iter_mut().for_each(|(_, x)| blind(x)),
293        K::Shift { input, default, .. } => {
294            blind(input);
295            if let Some(d) = default {
296                blind(d);
297            }
298        }
299        K::FuncCall { args, .. } => args.iter_mut().for_each(blind),
300        // COALESCE's domain is its arguments' cells; FILL's grid lists items.
301        K::Coalesce { args, .. } => args.iter_mut().for_each(blind),
302        K::Remap { input, .. } => blind(input),
303        K::If {
304            test,
305            then,
306            otherwise,
307            ..
308        } => {
309            blind(test);
310            blind(then);
311            if let Some(o) = otherwise {
312                blind(o);
313            }
314        }
315        // Leaves, and nodes whose item lists always count.
316        _ => {}
317    }
318}
319
320impl Drop for Engine {
321    fn drop(&mut self) {
322        let _ = self.tx.send(Cmd::Shutdown);
323        if let Some(h) = self.worker.take() {
324            let _ = h.join();
325        }
326    }
327}
328
329/// The first engine-internal measure id of a FIXPOINT's parts (three per
330/// FIXPOINT), between the LAMBDA placeholders and the recurrence slots.
331const FIX_BASE: u32 = u32::MAX - 4000;
332/// How many FIXPOINT measures one engine solves.
333const FIX_LIMIT: u32 = 900;
334
335/// A FIXPOINT measure, solved host-side: the dataflow computes its first
336/// iterate (`init`); the worker iterates, and publishes the result on
337/// session `id`, the measure's own, which the measures that read it see as
338/// an input. A step is `host`, the body's plan evaluated in memory
339/// ([`crate::hosteval`]), when it covers the body; else the dataflow's
340/// `body`, reading the iterate from session `r`.
341struct Fix {
342    id: MeasureId,
343    r: MeasureId,
344    init: MeasureId,
345    body: MeasureId,
346    max_iter: u32,
347    tolerance: f64,
348    host: Option<PlanNode>,
349    /// The measures the body reads, but r.
350    reads: Vec<MeasureId>,
351}
352
353/// Replace each FIXPOINT plan by its first-iterate and step plans, in place
354/// (so the build order holds), and describe it.
355fn split_fixpoints(model: &Model, plans: Plans) -> Result<(Plans, Vec<Fix>), EngineError> {
356    let ctx = crate::compiler::CompileContext::for_model(model);
357    let (mut out, mut fixes) = (Vec::new(), Vec::new());
358    for (mid, plan) in plans {
359        let PlanNodeKind::Fixpoint {
360            init,
361            body,
362            max_iter,
363            tolerance,
364        } = &plan.kind
365        else {
366            out.push((mid, plan));
367            continue;
368        };
369        let n = fixes.len() as u32;
370        if n == FIX_LIMIT {
371            return Err(EngineError::Unsupported(format!(
372                "a model may have at most {FIX_LIMIT} FIXPOINT measures"
373            )));
374        }
375        let part = |j: u32| MeasureId(FIX_BASE + 3 * n + j);
376        let (r, i, b) = (part(0), part(1), part(2));
377        let (ip, bp, _) = crate::compiler::fixpoint_parts(&ctx, mid, init, body, r)?;
378        let host = crate::hosteval::covers(&bp, r, &|_| true).then(|| bp.clone());
379        let mut reads = HashSet::new();
380        crate::hosteval::reads(&bp, &mut reads);
381        reads.remove(&r);
382        out.push((i, ip));
383        out.push((b, bp));
384        fixes.push(Fix {
385            id: mid,
386            r,
387            init: i,
388            body: b,
389            max_iter: *max_iter,
390            tolerance: *tolerance,
391            host,
392            reads: reads.into_iter().collect(),
393        });
394    }
395    Ok((out, fixes))
396}
397
398type Sessions = HashMap<MeasureId, InputSession<u64, (CoordKey, CellValue), isize>>;
399type Plans = Vec<(MeasureId, PlanNode)>;
400
401/// What a FIXPOINT's sessions hold: the iterate last fed to `r`, and the
402/// published result (and whether it converged).
403#[derive(Default)]
404struct FixState {
405    r: MeasureValues,
406    result: Arc<MeasureValues>,
407    converged: bool,
408    /// The host-side step, kept between solves for its row layouts.
409    host: Option<crate::hosteval::Host>,
410}
411
412/// Turn session `s`'s cells from `old` into `new`, as deltas.
413fn replace(
414    s: &mut InputSession<u64, (CoordKey, CellValue), isize>,
415    old: &MeasureValues,
416    new: &MeasureValues,
417) {
418    for (k, v) in old {
419        if new.get(k) != Some(v) {
420            s.remove((k.clone(), v.clone()));
421        }
422    }
423    for (k, v) in new {
424        if old.get(k) != Some(v) {
425            s.insert((k.clone(), v.clone()));
426        }
427    }
428}
429
430/// Σ|a − b| over the cells, summed in key order (so a run decides the same
431/// way every time). A cell only one side has, or a non-number that differs,
432/// is an infinite change.
433fn distance(a: &MeasureValues, b: &MeasureValues) -> f64 {
434    if b.keys().any(|k| !a.contains_key(k)) {
435        return f64::INFINITY;
436    }
437    let mut d: Vec<(&CoordKey, f64)> = a
438        .iter()
439        .map(|(k, x)| {
440            let y = b.get(k);
441            let step = match (x.as_num(), y.and_then(CellValue::as_num)) {
442                _ if y == Some(x) => 0.0,
443                (Some(x), Some(y)) => (x - y).abs(),
444                _ => f64::INFINITY,
445            };
446            (k, step)
447        })
448        .collect();
449    d.sort_by(|p, q| p.0.cmp(q.0));
450    d.iter().map(|(_, x)| x).sum()
451}
452
453/// The worker's view of the model for a FIXPOINT solve: the input cells per
454/// measure, and the measures changed since the last solve.
455struct World<'w> {
456    inputs: &'w HashMap<MeasureId, Arc<MeasureValues>>,
457    dirty: &'w Mutex<HashSet<MeasureId>>,
458}
459
460/// Solve every FIXPOINT, in build order (one may read another), and publish
461/// each result on its measure's session. A round is one step: the body's
462/// plan evaluated host-side at the iterate (with what it reads but r fixed
463/// for the solve), or, for a body the host evaluator does not cover, one
464/// engine round: feed the iterate, read the step. `warm` (after an edit):
465/// restart from the previous result if it converged, on the cells the first
466/// iterate has; if that does not converge within max_iter rounds, start over
467/// from the first iterate, so a result cut off at max_iter is always a full
468/// recompute's. An edit that leaves a FIXPOINT's first iterate and what its
469/// step reads as they were leaves its result as it was, unsolved.
470// shortcut: a host round still reads every edge (every PageRank cell moves
471// every round, so a delta step would touch them all anyway), though only to
472// refill laid-out rows: about 4 ms at 100,000 edges, 75 ms at 1,000,000. A
473// sparse step (delta-PageRank) would pay only once few cells still move.
474#[allow(clippy::too_many_arguments, reason = "the worker's state, passed down")]
475fn solve_fixpoints(
476    fixes: &[Fix],
477    state: &mut [FixState],
478    warm: bool,
479    sessions: &mut Sessions,
480    t: &mut u64,
481    run: &mut dyn FnMut(u64, &mut Sessions),
482    snapshot: &Mutex<Snapshot>,
483    first: &mut HashMap<MeasureId, Arc<MeasureValues>>,
484    world: &World<'_>,
485) {
486    let get = |id: MeasureId| {
487        snapshot
488            .lock()
489            .unwrap()
490            .get(&id)
491            .cloned()
492            .unwrap_or_default()
493    };
494    for (f, st) in fixes.iter().zip(state.iter_mut()) {
495        let init = get(f.init);
496        // The step's output is body(the last iterate fed), which is the
497        // result, as long as nothing the FIXPOINT reads has changed.
498        let same = |st: &FixState| match f.host {
499            Some(_) => {
500                let dirty = world.dirty.lock().unwrap();
501                !f.reads.iter().any(|m| dirty.contains(m))
502            }
503            None => *get(f.body) == *st.result,
504        };
505        if warm && first.get(&f.id) == Some(&init) && same(st) {
506            continue;
507        }
508        let mut host = st.host.take();
509        if let Some(plan) = &f.host {
510            let snap = snapshot.lock().unwrap();
511            let env = f.reads.iter().filter_map(|m| {
512                let vals = world.inputs.get(m).or_else(|| snap.get(m))?;
513                Some((*m, Arc::clone(vals)))
514            });
515            let h = host.get_or_insert_with(|| crate::hosteval::Host::new(plan.clone(), f.r));
516            h.refresh(env.collect());
517        }
518        let mut solve = |start: MeasureValues, st: &mut FixState| {
519            let mut x = start;
520            for _ in 0..f.max_iter {
521                let y = match host.as_mut() {
522                    Some(h) => h.step(&x),
523                    None => {
524                        let s = sessions
525                            .get_mut(&f.r)
526                            .expect("a FIXPOINT's iterate session");
527                        replace(s, &st.r, &x);
528                        st.r.clone_from(&x);
529                        *t += 1;
530                        run(*t, sessions);
531                        (*get(f.body)).clone()
532                    }
533                };
534                let d = distance(&x, &y);
535                x = y;
536                if d <= f.tolerance {
537                    return (x, true);
538                }
539            }
540            (x, false)
541        };
542        let mut out = None;
543        if warm && st.converged {
544            let start = init
545                .iter()
546                .map(|(k, v)| (k.clone(), st.result.get(k).unwrap_or(v).clone()))
547                .collect();
548            out = Some(solve(start, st)).filter(|(_, ok)| *ok);
549        }
550        let (x, converged) = out.unwrap_or_else(|| solve((*init).clone(), st));
551        st.host = host;
552        if x != *st.result {
553            world.dirty.lock().unwrap().insert(f.id);
554        }
555        let s = sessions
556            .get_mut(&f.id)
557            .expect("a FIXPOINT's result session");
558        replace(s, &st.result, &x);
559        st.result = Arc::new(x);
560        st.converged = converged;
561        first.insert(f.id, init);
562        *t += 1;
563        run(*t, sessions);
564        let mut snap = snapshot.lock().unwrap();
565        Arc::make_mut(&mut snap).insert(f.id, Arc::clone(&st.result));
566    }
567}
568
569/// The worker thread: builds the dataflow once, applies the seed, then serves
570/// edit rounds. Each round advances time by 1 and reports the changed outputs;
571/// we keep a running snapshot per measure so we can return a full picture.
572fn worker_loop(
573    input_ids: Vec<MeasureId>,
574    plans: Vec<(MeasureId, PlanNode)>,
575    fixes: Vec<Fix>,
576    seed: Vec<(MeasureId, CoordKey, CellValue)>,
577    cmd_rx: Receiver<Cmd>,
578    ready_tx: Sender<()>,
579) {
580    // The accumulated snapshot, shared with the dataflow's inspect closure.
581    // `Arc<Mutex<..>>` (not Rc/RefCell) so the timely closure is Send + Sync.
582    // The snapshot itself is copy-on-write (see [`Snapshot`]): replies hand
583    // out an `Arc` clone, and `Arc::make_mut` below copies only what a caller
584    // still shares and this round changes.
585    let snapshot: Arc<Mutex<Snapshot>> = Arc::new(Mutex::new(Snapshot::default()));
586    // The measures whose cells changed since the last edit round began.
587    let dirty: Arc<Mutex<HashSet<MeasureId>>> = Arc::default();
588    // The command receiver is `!Sync`; wrap it so the (Sync-bounded) timely
589    // closure can hold it, and take it out once inside (single worker).
590    let cmd_rx = Arc::new(Mutex::new(Some(cmd_rx)));
591    let ready_tx = Arc::new(Mutex::new(Some(ready_tx)));
592
593    timely::execute::execute_directly(move |worker| {
594        let cmd_rx = cmd_rx
595            .lock()
596            .unwrap()
597            .take()
598            .expect("worker_loop runs a single timely worker");
599        let ready_tx = ready_tx
600            .lock()
601            .unwrap()
602            .take()
603            .expect("worker_loop runs a single timely worker");
604        let mut sessions: Sessions = HashMap::new();
605        let snap = snapshot.clone();
606        let mut probe = ProbeHandle::new();
607        // A FIXPOINT's parts are the engine's own: not edited, not reported.
608        let internal: HashSet<MeasureId> = fixes
609            .iter()
610            .flat_map(|f| [f.id, f.r, f.init, f.body])
611            .collect();
612        let mut fix_state: Vec<FixState> = fixes.iter().map(|_| FixState::default()).collect();
613        let mut first = HashMap::new();
614        // A body the host evaluates is no dataflow of its own.
615        let mut fixes = fixes;
616        let mut plans = plans;
617        plans.retain(|(m, _)| !fixes.iter().any(|f| f.body == *m && f.host.is_some()));
618        let mark = dirty.clone();
619
620        let built: HashSet<MeasureId> = worker.dataflow::<u64, _, _>(|scope| {
621            let mut input_colls = HashMap::new();
622            for id in &input_ids {
623                let mut s = InputSession::new();
624                let coll = s.to_collection(scope);
625                sessions.insert(*id, s);
626                input_colls.insert(*id, coll);
627            }
628
629            let mut built = HashSet::new();
630            for (mid, reduced) in build_derived(scope, &plans, &input_colls) {
631                built.insert(mid);
632                let snap = snap.clone();
633                let mark = mark.clone();
634                reduced
635                    .inspect(move |((k, val), _t, diff)| {
636                        // Maintain the running snapshot from the delta
637                        // stream: a +1 sets the value, a -1 removes it. A
638                        // changed cell's `+new` may arrive before its `-old`
639                        // (updates are ordered by value), so a retraction
640                        // only removes the value it retracts.
641                        mark.lock().unwrap().insert(mid);
642                        let mut s = snap.lock().unwrap();
643                        let m = Arc::make_mut(Arc::make_mut(&mut s).entry(mid).or_default());
644                        if *diff > 0 {
645                            m.insert(k.clone(), val.clone());
646                        } else if *diff < 0 && m.get(k) == Some(val) {
647                            m.remove(k);
648                        }
649                    })
650                    .probe_with(&mut probe);
651            }
652            built
653        });
654        // A body reading a measure with no collection would not build as a
655        // dataflow either: it takes the dataflow path, with no step cells.
656        for f in &mut fixes {
657            let has = |m: &MeasureId| sessions.contains_key(m) || built.contains(m);
658            if !f.reads.iter().all(has) {
659                f.host = None;
660            }
661        }
662
663        // Step the worker until the output frontier has passed `t`. (Advancing
664        // an InputSession sets its time synchronously, so we cannot gate on the
665        // session time; we gate on the dataflow probe instead.)
666        let mut run_to = |t: u64, sessions: &mut Sessions| {
667            for s in sessions.values_mut() {
668                s.advance_to(t);
669                s.flush();
670            }
671            while probe.less_than(&t) {
672                worker.step();
673            }
674        };
675
676        // Apply the seed at time 0, then advance to 1 and run. We keep each
677        // input cell's value so a "set" is a proper retract(old)+assert(new)
678        // on the cardinality-one cell, and a FIXPOINT step reads them.
679        let mut inputs: HashMap<MeasureId, Arc<MeasureValues>> = HashMap::new();
680        for (mid, coord, v) in &seed {
681            if let Some(session) = sessions.get_mut(mid) {
682                session.insert((coord.clone(), v.clone()));
683                Arc::make_mut(inputs.entry(*mid).or_default()).insert(coord.clone(), v.clone());
684            }
685        }
686        let mut t: u64 = 1;
687        run_to(t, &mut sessions);
688        solve_fixpoints(
689            &fixes,
690            &mut fix_state,
691            false,
692            &mut sessions,
693            &mut t,
694            &mut run_to,
695            &snapshot,
696            &mut first,
697            &World {
698                inputs: &inputs,
699                dirty: &dirty,
700            },
701        );
702        // What a reply shows: every measure but a FIXPOINT's parts.
703        let reply_snapshot = |snapshot: &Mutex<Snapshot>| {
704            let s = snapshot.lock().unwrap();
705            if fixes.is_empty() {
706                return Arc::clone(&s);
707            }
708            let mut m = (**s).clone();
709            for f in &fixes {
710                m.remove(&f.init);
711                m.remove(&f.body);
712            }
713            Arc::new(m)
714        };
715        // Signal that startup is complete.
716        let _ = ready_tx.send(());
717
718        // Serve edit rounds.
719        while let Ok(cmd) = cmd_rx.recv() {
720            match cmd {
721                Cmd::Edit(edits, reply) => {
722                    dirty.lock().unwrap().clear();
723                    for e in edits.iter().filter(|e| !internal.contains(&e.measure)) {
724                        if let Some(session) = sessions.get_mut(&e.measure) {
725                            let cells = Arc::make_mut(inputs.entry(e.measure).or_default());
726                            if let Some(old_val) = cells.remove(&e.coord) {
727                                session.remove((e.coord.clone(), old_val));
728                            }
729                            if let Some(cv) = &e.value {
730                                session.insert((e.coord.clone(), cv.clone()));
731                                cells.insert(e.coord.clone(), cv.clone());
732                            }
733                            dirty.lock().unwrap().insert(e.measure);
734                        }
735                    }
736                    t += 1;
737                    run_to(t, &mut sessions);
738                    solve_fixpoints(
739                        &fixes,
740                        &mut fix_state,
741                        true,
742                        &mut sessions,
743                        &mut t,
744                        &mut run_to,
745                        &snapshot,
746                        &mut first,
747                        &World {
748                            inputs: &inputs,
749                            dirty: &dirty,
750                        },
751                    );
752                    let _ = reply.send(reply_snapshot(&snapshot));
753                }
754                Cmd::Shutdown => break,
755            }
756        }
757    });
758}
759
760#[cfg(test)]
761#[cfg_attr(coverage_nightly, coverage(off))]
762mod tests {
763    use super::*;
764    use improv_core_model::{
765        BinaryOp, CategoryId, Coordinate, DimensionSpec, Expr, Formula, ItemId, Measure, Name,
766        Value, ValueType,
767    };
768
769    fn revenue_model() -> Model {
770        let mut m = Model::new();
771        let (time, product) = (CategoryId(1), CategoryId(2));
772        m.add_category(time, "Time");
773        m.add_category(product, "Product");
774        m.add_item(ItemId(10), time, "2025");
775        m.add_item(ItemId(20), product, "WidgetA");
776        m.add_item(ItemId(21), product, "WidgetB");
777        m.add_measure(Measure {
778            id: MeasureId(100),
779            name: Name("Price".into()),
780            value_type: ValueType::Number,
781            categories: vec![product],
782            kind: MeasureKind::Input,
783            description: None,
784        });
785        m.add_measure(Measure {
786            id: MeasureId(101),
787            name: Name("Quantity".into()),
788            value_type: ValueType::Number,
789            categories: vec![time, product],
790            kind: MeasureKind::Input,
791            description: None,
792        });
793        m.add_measure(Measure {
794            id: MeasureId(102),
795            name: Name("Revenue".into()),
796            value_type: ValueType::Number,
797            categories: vec![time, product],
798            kind: MeasureKind::Derived(Formula::new(Expr::BinaryOp(
799                BinaryOp::Mul,
800                Box::new(Expr::Ref(MeasureId(100), DimensionSpec::default())),
801                Box::new(Expr::Ref(MeasureId(101), DimensionSpec::default())),
802            ))),
803            description: None,
804        });
805        let c = |p: &[(CategoryId, ItemId)]| Coordinate::from_pairs(p.iter().copied());
806        m.set_input(
807            MeasureId(100),
808            c(&[(product, ItemId(20))]),
809            Value::Number(10.0),
810        );
811        m.set_input(
812            MeasureId(100),
813            c(&[(product, ItemId(21))]),
814            Value::Number(20.0),
815        );
816        m.set_input(
817            MeasureId(101),
818            c(&[(time, ItemId(10)), (product, ItemId(20))]),
819            Value::Number(100.0),
820        );
821        m.set_input(
822            MeasureId(101),
823            c(&[(time, ItemId(10)), (product, ItemId(21))]),
824            Value::Number(50.0),
825        );
826        m
827    }
828
829    /// A new item reaches a value-only reduction as data: the engine serves
830    /// the grown model and computes what a new engine computes. One that a
831    /// plan enumerates (`POSITION`, `PREV`) does not.
832    #[test]
833    fn new_items_are_served_unless_a_plan_reads_their_category() {
834        use improv_core_model::parse_definition;
835        let (time, product) = (CategoryId(1), CategoryId(2));
836        let define = |m: &mut Model, id: u32, text: &str| {
837            let improv_core_model::Definition::Formula(f) = parse_definition(m, text).unwrap()
838            else {
839                panic!("{text}")
840            };
841            let name = f.target.0.clone();
842            let (value_type, categories) =
843                crate::compiler::derived_shape(m, MeasureId(id), &name, &f.formula).unwrap();
844            m.add_measure(Measure {
845                id: MeasureId(id),
846                name: Name(name),
847                value_type,
848                categories,
849                kind: MeasureKind::Derived(f.formula),
850                description: None,
851            });
852        };
853        let mut m = revenue_model();
854        define(&mut m, 103, "Total = SUM(Revenue OVER Product)");
855        define(&mut m, 104, "Count = COUNT(Quantity OVER Time, Product)");
856        let targets = [MeasureId(102), MeasureId(103), MeasureId(104)];
857        let (mut e, _) = Engine::new(&m, &targets).unwrap();
858        assert!(e.serves(&m).unwrap());
859        let mut grown = m.clone();
860        grown.add_item(ItemId(22), product, "WidgetC");
861        grown.add_item(ItemId(11), time, "2026");
862        assert!(e.serves(&grown).unwrap());
863        let c = |p: &[(CategoryId, ItemId)]| Coordinate::from_pairs(p.iter().copied());
864        let edits = [
865            (MeasureId(100), c(&[(product, ItemId(22))]), 5.0),
866            (
867                MeasureId(101),
868                c(&[(time, ItemId(10)), (product, ItemId(22))]),
869                7.0,
870            ),
871            (
872                MeasureId(101),
873                c(&[(time, ItemId(11)), (product, ItemId(22))]),
874                3.0,
875            ),
876        ];
877        for (mid, coord, v) in &edits {
878            grown.set_input(*mid, coord.clone(), Value::Number(*v));
879        }
880        let snap = e
881            .apply(
882                edits
883                    .iter()
884                    .map(|(mid, coord, v)| Edit {
885                        measure: *mid,
886                        coord: encode_coord(coord),
887                        value: Some(CellValue::num(*v)),
888                    })
889                    .collect(),
890            )
891            .unwrap();
892        let fresh = crate::dataflow::evaluate(&grown, &targets).unwrap();
893        for t in targets {
894            assert_eq!(
895                snap.get(&t).map(|m| (**m).clone()),
896                fresh.get(&t).cloned(),
897                "{t:?}"
898            );
899        }
900        for text in ["Pos = POSITION(Time)", "Last = PREV(Quantity, Time)"] {
901            let mut m = revenue_model();
902            define(&mut m, 105, text);
903            let (e, _) = Engine::new(&m, &[MeasureId(105)]).unwrap();
904            let mut grown = m.clone();
905            grown.add_item(ItemId(11), time, "2026");
906            assert!(!e.serves(&grown).unwrap(), "{text}");
907            grown = m.clone();
908            grown.add_item(ItemId(22), product, "WidgetC");
909            assert!(
910                e.serves(&grown).unwrap(),
911                "{text} over Time ignores Product"
912            );
913        }
914        // FILL fills every item combination: a new item of either category
915        // is a new grid row, so neither is served.
916        let mut m = revenue_model();
917        define(&mut m, 105, "Filled = FILL(Quantity, 0)");
918        let (e, _) = Engine::new(&m, &[MeasureId(105)]).unwrap();
919        for (id, cat) in [(11, time), (22, product)] {
920            let mut grown = m.clone();
921            grown.add_item(ItemId(id), cat, "new");
922            assert!(!e.serves(&grown).unwrap(), "FILL with a new item {id}");
923        }
924        // A FIXPOINT is served when its parts are: here its step reads
925        // @ITEMCOUNT(Product), so a new product is not served.
926        let mut m = revenue_model();
927        define(
928            &mut m,
929            105,
930            "Fix = FIXPOINT(Price, LAMBDA(r, r / @ITEMCOUNT(Product) + 1))",
931        );
932        let (e, _) = Engine::new(&m, &[MeasureId(105)]).unwrap();
933        let mut grown = m.clone();
934        grown.add_item(ItemId(11), time, "2026");
935        assert!(e.serves(&grown).unwrap());
936        grown.add_item(ItemId(22), product, "WidgetC");
937        assert!(!e.serves(&grown).unwrap());
938        // COALESCE (no grid) and COUNTUNIQUE read only cells.
939        let mut m = revenue_model();
940        define(&mut m, 105, "Either = COALESCE(Quantity, Price)");
941        define(&mut m, 106, "Kinds = COUNTUNIQUE(Quantity OVER Product)");
942        let (e, _) = Engine::new(&m, &[MeasureId(105), MeasureId(106)]).unwrap();
943        let mut grown = m.clone();
944        grown.add_item(ItemId(22), product, "WidgetC");
945        grown.add_item(ItemId(11), time, "2026");
946        assert!(e.serves(&grown).unwrap());
947    }
948
949    /// Copy-on-write: an edit replaces only the changed measure's `Arc`;
950    /// an untouched measure is the same allocation in both snapshots, and an
951    /// old snapshot keeps its values.
952    #[test]
953    fn snapshot_shares_unchanged_measures() {
954        let mut model = revenue_model();
955        model.add_measure(Measure {
956            id: MeasureId(103),
957            name: Name("Double".into()),
958            value_type: ValueType::Number,
959            categories: vec![CategoryId(1), CategoryId(2)],
960            kind: MeasureKind::Derived(Formula::new(Expr::BinaryOp(
961                BinaryOp::Mul,
962                Box::new(Expr::Ref(MeasureId(101), DimensionSpec::default())),
963                Box::new(Expr::Literal(Value::Number(2.0))),
964            ))),
965            description: None,
966        });
967        let (mut engine, before) =
968            Engine::new(&model, &[MeasureId(102), MeasureId(103)]).expect("engine");
969        // Price feeds Revenue only.
970        let after = engine
971            .set(MeasureId(100), key(&[(2, 20)]), 15.0)
972            .expect("set");
973        assert!(Arc::ptr_eq(
974            &before[&MeasureId(103)],
975            &after[&MeasureId(103)]
976        ));
977        assert!(!Arc::ptr_eq(
978            &before[&MeasureId(102)],
979            &after[&MeasureId(102)]
980        ));
981        let rev = |s: &Snapshot| s[&MeasureId(102)][&key(&[(1, 10), (2, 20)])].as_num();
982        assert_eq!(rev(&before), Some(1000.0), "old snapshot unchanged");
983        assert_eq!(rev(&after), Some(1500.0));
984    }
985
986    /// Typed inputs seed and edit like numbers: a text input feeding a
987    /// derived copy follows a text edit.
988    #[test]
989    fn typed_inputs_seed_and_edit() {
990        let mut model = revenue_model();
991        model.add_measure(Measure {
992            id: MeasureId(103),
993            name: Name("Kind".into()),
994            value_type: ValueType::Text,
995            categories: vec![CategoryId(2)],
996            kind: MeasureKind::Input,
997            description: None,
998        });
999        model.add_measure(Measure {
1000            id: MeasureId(104),
1001            name: Name("Label".into()),
1002            value_type: ValueType::Text,
1003            categories: vec![CategoryId(2)],
1004            kind: MeasureKind::Derived(Formula::new(Expr::Ref(
1005                MeasureId(103),
1006                DimensionSpec::default(),
1007            ))),
1008            description: None,
1009        });
1010        model.set_input(
1011            MeasureId(103),
1012            Coordinate::from_pairs([(CategoryId(2), ItemId(20))]),
1013            Value::Text("a".into()),
1014        );
1015        // A number in the text measure is mistyped: no cell.
1016        model.set_input(
1017            MeasureId(103),
1018            Coordinate::from_pairs([(CategoryId(2), ItemId(21))]),
1019            Value::Number(1.0),
1020        );
1021        let (mut engine, before) = Engine::new(&model, &[MeasureId(104)]).expect("engine");
1022        assert_eq!(before[&MeasureId(104)].len(), 1);
1023        let label = |s: &Snapshot| s[&MeasureId(104)].get(&key(&[(2, 20)])).cloned();
1024        assert_eq!(label(&before), Some(CellValue::Text("a".into())));
1025        let edit = |value| Edit {
1026            measure: MeasureId(103),
1027            coord: key(&[(2, 20)]),
1028            value,
1029        };
1030        let after = engine
1031            .apply(vec![edit(Some(CellValue::Text("b".into())))])
1032            .expect("edit");
1033        assert_eq!(label(&after), Some(CellValue::Text("b".into())));
1034        let cleared = engine.apply(vec![edit(None)]).expect("clear");
1035        assert_eq!(label(&cleared), None);
1036    }
1037
1038    fn key(pairs: &[(u32, u32)]) -> CoordKey {
1039        let mut k: Vec<(u32, u32)> = pairs.to_vec();
1040        k.sort();
1041        k
1042    }
1043
1044    #[test]
1045    fn initial_snapshot_matches_evaluate() {
1046        let model = revenue_model();
1047        let (_engine, snap) = Engine::new(&model, &[MeasureId(102)]).expect("engine");
1048        let rev = snap.get(&MeasureId(102)).expect("revenue");
1049        assert_eq!(
1050            rev.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
1051            Some(1000.0)
1052        ); // 10*100
1053        assert_eq!(
1054            rev.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
1055            Some(1000.0)
1056        ); // 20*50
1057        assert_eq!(rev.len(), 2);
1058    }
1059
1060    #[test]
1061    fn edit_updates_only_affected_cell() {
1062        let model = revenue_model();
1063        let (mut engine, _snap) = Engine::new(&model, &[MeasureId(102)]).expect("engine");
1064
1065        // Change Quantity[2025,WidgetA] 100 -> 120: Revenue there 1000 -> 1200,
1066        // the other cell (WidgetB) is unchanged.
1067        let snap = engine
1068            .set(MeasureId(101), key(&[(1, 10), (2, 20)]), 120.0)
1069            .expect("edit");
1070        let rev = snap.get(&MeasureId(102)).expect("revenue");
1071        assert_eq!(
1072            rev.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
1073            Some(1200.0)
1074        );
1075        assert_eq!(
1076            rev.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
1077            Some(1000.0)
1078        );
1079    }
1080
1081    /// IF switches branches incrementally: raising Price past the test's
1082    /// threshold retracts the else-cells and asserts the then-cells.
1083    #[test]
1084    fn if_switches_branch_incrementally() {
1085        let mut model = revenue_model();
1086        let f =
1087            improv_core_model::parse_formula(&model, "Pick = IF(Price > 15, Revenue, -1)").unwrap();
1088        model.add_measure(Measure {
1089            id: MeasureId(103),
1090            name: f.target,
1091            value_type: ValueType::Number,
1092            categories: vec![CategoryId(1), CategoryId(2)],
1093            kind: MeasureKind::Derived(f.formula),
1094            description: None,
1095        });
1096        let (mut engine, snap) = Engine::new(&model, &[MeasureId(103)]).expect("engine");
1097        let pick = |s: &Snapshot, t, p| s[&MeasureId(103)].get(&key(&[(1, t), (2, p)])).cloned();
1098        assert_eq!(pick(&snap, 10, 20), Some(CellValue::num(-1.0)));
1099        let snap = engine
1100            .set(MeasureId(100), key(&[(2, 20)]), 16.0)
1101            .expect("edit");
1102        assert_eq!(pick(&snap, 10, 20), Some(CellValue::num(1600.0)));
1103        assert_eq!(pick(&snap, 10, 21), Some(CellValue::num(1000.0)));
1104        assert_eq!(snap[&MeasureId(103)].len(), 2, "no stale else-cell");
1105        let snap = engine
1106            .set(MeasureId(100), key(&[(2, 20)]), 1.0)
1107            .expect("edit");
1108        assert_eq!(pick(&snap, 10, 20), Some(CellValue::num(-1.0)));
1109    }
1110
1111    #[test]
1112    fn edit_price_broadcasts() {
1113        let model = revenue_model();
1114        let (mut engine, _snap) = Engine::new(&model, &[MeasureId(102)]).expect("engine");
1115        // Price[WidgetA] 10 -> 15 broadcasts to Revenue[*, WidgetA].
1116        let snap = engine
1117            .set(MeasureId(100), key(&[(2, 20)]), 15.0)
1118            .expect("edit");
1119        let rev = snap.get(&MeasureId(102)).expect("revenue");
1120        assert_eq!(
1121            rev.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
1122            Some(1500.0)
1123        ); // 15*100
1124    }
1125
1126    #[test]
1127    fn edit_that_lowers_a_value_keeps_the_cell() {
1128        let model = revenue_model();
1129        let (mut engine, _snap) = Engine::new(&model, &[MeasureId(102)]).expect("engine");
1130        // Price[WidgetA] 10 -> 5: Revenue[2025,A] 1000 -> 500. The `+500`
1131        // update sorts before the `-1000` one; the cell must survive.
1132        let snap = engine
1133            .set(MeasureId(100), key(&[(2, 20)]), 5.0)
1134            .expect("edit");
1135        let rev = snap.get(&MeasureId(102)).expect("revenue");
1136        assert_eq!(
1137            rev.get(&key(&[(1, 10), (2, 20)])).and_then(|v| v.as_num()),
1138            Some(500.0)
1139        );
1140        assert_eq!(rev.len(), 2);
1141    }
1142
1143    #[test]
1144    fn adding_a_cell_creates_a_result() {
1145        let model = revenue_model();
1146        let (mut engine, _snap) = Engine::new(&model, &[MeasureId(102)]).expect("engine");
1147        // Add Quantity[2025, WidgetB] was already present; add a NEW coordinate
1148        // isn't possible without the item, but re-setting an existing cell and
1149        // clearing works. Clear Quantity[2025,WidgetA] -> Revenue there vanishes.
1150        let snap = engine
1151            .clear(MeasureId(101), key(&[(1, 10), (2, 20)]))
1152            .expect("clear");
1153        let rev = snap.get(&MeasureId(102)).expect("revenue");
1154        assert_eq!(rev.get(&key(&[(1, 10), (2, 20)])), None);
1155        // The other cell survives.
1156        assert_eq!(
1157            rev.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
1158            Some(1000.0)
1159        );
1160    }
1161
1162    #[test]
1163    fn edits_to_untracked_measures_and_new_cells() {
1164        let mut model = revenue_model();
1165        // A non-numeric input and an orphan cell (no such measure) aren't seeded.
1166        model.set_input(
1167            MeasureId(100),
1168            Coordinate::from_pairs([(CategoryId(2), ItemId(22))]),
1169            Value::Text("n/a".into()),
1170        );
1171        model.set_input(MeasureId(999), Coordinate::new(), Value::Number(1.0));
1172        let (mut engine, snap) = Engine::new(&model, &[MeasureId(102)]).expect("engine");
1173        assert_eq!(engine.targets(), [MeasureId(102)]);
1174        assert_eq!(snap[&MeasureId(102)].len(), 2);
1175        // An edit to a measure with no input session changes nothing.
1176        let s = engine.set(MeasureId(999), key(&[]), 5.0).expect("set");
1177        assert_eq!(s[&MeasureId(102)], snap[&MeasureId(102)]);
1178        // A brand-new Price cell (no previous value) has no Quantity yet...
1179        let s = engine
1180            .set(MeasureId(100), key(&[(2, 22)]), 7.0)
1181            .expect("set");
1182        assert_eq!(s[&MeasureId(102)].len(), 2);
1183        // ...until a matching Quantity arrives.
1184        let s = engine
1185            .set(MeasureId(101), key(&[(1, 10), (2, 22)]), 3.0)
1186            .expect("set");
1187        assert_eq!(
1188            s[&MeasureId(102)]
1189                .get(&key(&[(1, 10), (2, 22)]))
1190                .and_then(|v| v.as_num()),
1191            Some(21.0)
1192        );
1193    }
1194
1195    /// Define derived measure `id` from `text`, with the engine's shape.
1196    fn define_text(m: &mut Model, id: u32, text: &str) {
1197        let f = improv_core_model::parse_formula(m, text).unwrap_or_else(|e| panic!("{e}"));
1198        let (value_type, categories) =
1199            crate::compiler::derived_shape(m, MeasureId(id), &f.target.0, &f.formula).unwrap();
1200        m.add_measure(Measure {
1201            id: MeasureId(id),
1202            name: f.target,
1203            value_type,
1204            categories,
1205            kind: MeasureKind::Derived(f.formula),
1206            description: None,
1207        });
1208    }
1209
1210    /// One role pair Src/Dst of n people, inputs X[Src] = 1, 2, … and a
1211    /// pseudo-random weighted graph W[Src, Dst] (person 0 dangling).
1212    fn graph_model(n: u32) -> Model {
1213        let mut m = Model::new();
1214        let (s, d) = (CategoryId(1), CategoryId(2));
1215        m.add_category(s, "Src");
1216        m.add_category(d, "Dst");
1217        for i in 0..n {
1218            m.add_item(ItemId(100 + i), s, format!("p{i}"));
1219            m.add_item(ItemId(200 + i), d, format!("p{i}"));
1220        }
1221        for (id, name, cats) in [(10, "X", vec![s]), (11, "W", vec![s, d])] {
1222            m.add_measure(Measure {
1223                id: MeasureId(id),
1224                name: Name(name.into()),
1225                value_type: ValueType::Number,
1226                categories: cats,
1227                kind: MeasureKind::Input,
1228                description: None,
1229            });
1230        }
1231        let mut seed = 7u64;
1232        for i in 0..n {
1233            let at = Coordinate::from_pairs([(s, ItemId(100 + i))]);
1234            m.set_input(MeasureId(10), at, Value::Number(f64::from(i) + 1.0));
1235            for j in 0..n {
1236                seed = seed
1237                    .wrapping_mul(6_364_136_223_846_793_005)
1238                    .wrapping_add(1_442_695_040_888_963_407);
1239                if i > 0 && i != j && (seed >> 33).is_multiple_of(3) {
1240                    let at = Coordinate::from_pairs([(s, ItemId(100 + i)), (d, ItemId(200 + j))]);
1241                    let w = 1.0 + ((seed >> 40) % 9) as f64;
1242                    m.set_input(MeasureId(11), at, Value::Number(w));
1243                }
1244            }
1245        }
1246        m
1247    }
1248
1249    /// The fixpoint suite's FIXPOINTs, solved with host steps and with
1250    /// dataflow steps: the same snapshot bit for bit, fresh and after an
1251    /// edit and a clear. A body with an uncovered node (PREV is a shift)
1252    /// solves on the dataflow either way, to the hand-computed answer.
1253    #[test]
1254    fn host_and_dataflow_steps_agree_bit_for_bit() {
1255        let mut m = graph_model(12);
1256        for (id, text) in [
1257            (300, "Out = SUM(W OVER Dst)"),
1258            (301, "M = W / Out"),
1259            (302, "Dangling = FILL(0 * Out, 1)"),
1260            (
1261                303,
1262                "PR = FIXPOINT(SEQUENCE(Src, 1 / @ITEMCOUNT(Src), 0), \
1263                 LAMBDA(r, RELABEL((1 - 0.85) / @ITEMCOUNT(Src) \
1264                 + 0.85 * (FILL(SUMPRODUCT(r, M OVER Src), 0) \
1265                 + SUMPRODUCT(r, Dangling OVER Src) / @ITEMCOUNT(Src)), Dst, Src)), \
1266                 200, 0.0000000000001)",
1267            ),
1268            (304, "Two = FIXPOINT(X, LAMBDA(r, r / 2 + X))"),
1269            (305, "Cut = FIXPOINT(X, LAMBDA(r, r / 2 + X), 3)"),
1270            (306, "Grow = FIXPOINT(X, LAMBDA(r, r * 2), 40, 0)"),
1271            (307, "Zero = FIXPOINT(X, LAMBDA(r, r * 2), 0)"),
1272            (308, "Half = Two / 2"),
1273            (309, "Again = FIXPOINT(Two, LAMBDA(r, (r + Half) / 2))"),
1274            (310, "Root = FIXPOINT(1, LAMBDA(r, (r + 2 / r) / 2))"),
1275            (311, "Wide = FIXPOINT(0, LAMBDA(r, r / 2 + X))"),
1276            (
1277                312,
1278                "Back = FIXPOINT(X, LAMBDA(r, PREV(r, Src, 1, 0) / 2 + X))",
1279            ),
1280        ] {
1281            define_text(&mut m, id, text);
1282        }
1283        let ids: Vec<MeasureId> = (300..=312).map(MeasureId).collect();
1284        let (_, fixes) = split_fixpoints(&m, compile_plans(&m, &ids).unwrap()).unwrap();
1285        let hosted: Vec<u32> = fixes
1286            .iter()
1287            .filter(|f| f.host.is_some())
1288            .map(|f| f.id.0)
1289            .collect();
1290        assert_eq!(hosted, [303, 304, 305, 306, 307, 309, 310, 311]);
1291        let (mut host, a) = Engine::build(&m, &ids, true).unwrap();
1292        let (mut dd, b) = Engine::build(&m, &ids, false).unwrap();
1293        assert_eq!(a, b);
1294        let back = |s: &Snapshot| {
1295            (0..12)
1296                .map(|i| s[&MeasureId(312)][&vec![(1, 100 + i)]].as_num().unwrap())
1297                .collect::<Vec<_>>()
1298        };
1299        // r_i = r_(i-1) / 2 + X_i, from r_0 = X_0 = 1: exact in 12 rounds.
1300        let mut want = vec![0.0; 12];
1301        for i in 0..12 {
1302            want[i] = f64::from(i as u32 + 1) + if i > 0 { want[i - 1] / 2.0 } else { 0.0 };
1303        }
1304        assert_eq!(back(&a), want);
1305        let pr: f64 = a[&MeasureId(303)]
1306            .values()
1307            .filter_map(CellValue::as_num)
1308            .sum();
1309        assert!((pr - 1.0).abs() < 1e-12);
1310        for e in [
1311            (MeasureId(11), vec![(1, 103), (2, 200)], Some(30.0)),
1312            (MeasureId(10), vec![(1, 105)], Some(-4.0)),
1313            (MeasureId(10), vec![(1, 102)], None),
1314        ] {
1315            let edit = vec![Edit {
1316                measure: e.0,
1317                coord: e.1.clone(),
1318                value: e.2.map(CellValue::num),
1319            }];
1320            let (a, b) = (host.apply(edit.clone()).unwrap(), dd.apply(edit).unwrap());
1321            assert_eq!(a, b, "after {e:?}");
1322        }
1323    }
1324}