1use 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#[derive(Debug, Clone)]
35pub struct Edit {
36 pub measure: MeasureId,
37 pub coord: CoordKey,
38 pub value: Option<CellValue>,
39}
40
41pub 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
56pub type MeasureValues = HashMap<CoordKey, CellValue>;
58
59pub type Snapshot = Arc<HashMap<MeasureId, Arc<MeasureValues>>>;
70
71enum Cmd {
72 Edit(Vec<Edit>, Sender<Snapshot>),
75 Shutdown,
76}
77
78pub struct Engine {
80 tx: Sender<Cmd>,
81 worker: Option<JoinHandle<()>>,
82 targets: Vec<MeasureId>,
84 current: HashMap<(MeasureId, CoordKey), CellValue>,
87 plans: Vec<(MeasureId, PlanNode)>,
91}
92
93impl Engine {
94 pub fn new(model: &Model, targets: &[MeasureId]) -> Result<(Engine, Snapshot), EngineError> {
98 Self::build(model, targets, true)
99 }
100
101 fn build(
105 model: &Model,
106 targets: &[MeasureId],
107 host_steps: bool,
108 ) -> Result<(Engine, Snapshot), EngineError> {
109 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 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 input_ids.extend(fixes.iter().flat_map(|f| [f.r, f.id]));
129
130 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 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 let snapshot = engine.apply(Vec::new())?;
168 Ok((engine, snapshot))
169 }
170
171 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 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 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 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 pub fn targets(&self) -> &[MeasureId] {
226 &self.targets
227 }
228
229 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
251fn 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 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 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 _ => {}
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
329const FIX_BASE: u32 = u32::MAX - 4000;
332const FIX_LIMIT: u32 = 900;
334
335struct 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 reads: Vec<MeasureId>,
351}
352
353fn 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#[derive(Default)]
404struct FixState {
405 r: MeasureValues,
406 result: Arc<MeasureValues>,
407 converged: bool,
408 host: Option<crate::hosteval::Host>,
410}
411
412fn 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
430fn 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
453struct World<'w> {
456 inputs: &'w HashMap<MeasureId, Arc<MeasureValues>>,
457 dirty: &'w Mutex<HashSet<MeasureId>>,
458}
459
460#[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 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
569fn 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 let snapshot: Arc<Mutex<Snapshot>> = Arc::new(Mutex::new(Snapshot::default()));
586 let dirty: Arc<Mutex<HashSet<MeasureId>>> = Arc::default();
588 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 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 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 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 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 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 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 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 let _ = ready_tx.send(());
717
718 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 #[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 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 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 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 #[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 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 #[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 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 ); assert_eq!(
1054 rev.get(&key(&[(1, 10), (2, 21)])).and_then(|v| v.as_num()),
1055 Some(1000.0)
1056 ); 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 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 #[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 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 ); }
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 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 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 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 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 let s = engine.set(MeasureId(999), key(&[]), 5.0).expect("set");
1177 assert_eq!(s[&MeasureId(102)], snap[&MeasureId(102)]);
1178 let s = engine
1180 .set(MeasureId(100), key(&[(2, 22)]), 7.0)
1181 .expect("set");
1182 assert_eq!(s[&MeasureId(102)].len(), 2);
1183 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 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 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 #[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 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}