1use std::collections::HashMap;
22
23use serde::Serialize;
24
25use crate::dag::{toposort, Dag};
26use crate::excel_error::ExcelError;
27use crate::finding::{LintFinding, Severity};
28use crate::formula::{BinOp, Expr, UnOp};
29use crate::resolve;
30
31use super::eval_bridge::{eval_leaf, from_json, percent, powf, to_json, CellEnv};
32use super::eval_value::EvalValue;
33use super::semantics;
34use super::value::CellValue;
35use super::{Cell, CellExpr};
36
37#[derive(Debug, Clone, Default, Serialize, schemars::JsonSchema)]
41pub struct EvalTrace {
42 pub cell: String,
44 pub formula: Option<String>,
46 pub resolved_refs: Vec<(String, CellValue)>,
48 pub dispatched_fn: Option<String>,
50 pub operand_values: Vec<CellValue>,
52 pub coercions: Vec<String>,
54 pub short_circuited: Option<ExcelError>,
56}
57
58impl EvalTrace {
59 fn new(cell: &str) -> Self {
61 EvalTrace {
62 cell: cell.to_string(),
63 ..Default::default()
64 }
65 }
66}
67
68#[derive(Debug, Clone, Default, Serialize, schemars::JsonSchema)]
71pub struct RunResult {
72 pub computed: HashMap<String, CellValue>,
74 pub traces: HashMap<String, EvalTrace>,
76}
77
78fn owning_sheet(key: &str) -> &str {
82 key.split_once('!').map_or("", |(s, _)| s)
83}
84
85pub fn run(
93 ir: &HashMap<String, Cell>,
94 dag: &Dag,
95 seed: &CellEnv,
96) -> Result<RunResult, Box<LintFinding>> {
97 let order = toposort(dag).map_err(|residual| {
98 let (sheet, cell) = residual
99 .first()
100 .and_then(|k| k.split_once('!'))
101 .map(|(s, a)| (s.to_string(), Some(a.to_string())))
102 .unwrap_or_default();
103 Box::new(LintFinding::new(
104 Severity::Error,
105 "dag/cycle",
106 sheet,
107 cell,
108 format!("dependency cycle through cells: {}", residual.join(" → ")),
109 "break the cycle by removing one of the circular references",
110 ))
111 })?;
112
113 let mut env = seed.clone();
114 let mut errs: HashMap<String, ExcelError> = HashMap::new();
115 let mut computed: HashMap<String, CellValue> = HashMap::new();
116 let mut traces: HashMap<String, EvalTrace> = HashMap::new();
117
118 for key in order {
119 match ir.get(&key) {
120 Some(Cell {
121 expr: CellExpr::Literal(v),
122 ..
123 }) => {
124 if env.get(&key).is_none() {
131 env = env.seed_cell(&key, v);
132 }
133 if let CellValue::Error(err) = v {
134 errs.insert(key.clone(), *err);
135 }
136 computed.insert(key.clone(), v.clone());
137 },
138 Some(Cell {
139 expr: CellExpr::Formula(e),
140 ..
141 }) => {
142 let mut trace = EvalTrace::new(&key);
143 let current_sheet = owning_sheet(&key);
146 let result = eval_expr(e, &env, &errs, current_sheet, &mut trace);
147 if let CellValue::Error(err) = &result {
148 errs.insert(key.clone(), *err);
149 trace.short_circuited.get_or_insert(*err);
150 }
151 if let Some(j) = to_json(&result) {
152 env = env.with_value(&key, j);
153 }
154 computed.insert(key.clone(), result);
155 traces.insert(key.clone(), trace);
156 },
157 None => {},
159 }
160 }
161
162 Ok(RunResult { computed, traces })
163}
164
165fn eval_expr(
169 e: &Expr,
170 env: &CellEnv,
171 errs: &HashMap<String, ExcelError>,
172 current_sheet: &str,
173 trace: &mut EvalTrace,
174) -> CellValue {
175 trace.formula.get_or_insert_with(|| format!("{e:?}"));
176 let mut ctx = Ctx {
177 env,
178 errs,
179 current_sheet,
180 trace,
181 };
182 match e {
183 Expr::Call { name, args } => {
184 eval_call(name, args, ctx.env, ctx.errs, ctx.current_sheet, ctx.trace)
185 },
186 Expr::BinaryOp { left, op, right } => eval_binary_op(e, left, *op, right, &mut ctx),
187 Expr::UnaryOp { op, operand } => eval_unary_op(e, *op, operand, &mut ctx),
188 other => {
189 record_refs(other, ctx.env, ctx.current_sheet, ctx.trace);
190 eval_leaf(other, ctx.env, ctx.errs)
191 },
192 }
193}
194
195fn eval_call(
199 name: &str,
200 args: &[Expr],
201 env: &CellEnv,
202 errs: &HashMap<String, ExcelError>,
203 current_sheet: &str,
204 trace: &mut EvalTrace,
205) -> CellValue {
206 let vals: Vec<EvalValue> = args
207 .iter()
208 .map(|a| materialize_arg(a, env, errs, current_sheet, trace))
209 .collect();
210 trace.dispatched_fn = Some(name.to_string());
211 for v in &vals {
212 record_operand_values(v, trace);
213 }
214 semantics::apply(name, &vals)
215}
216
217fn record_operand_values(v: &EvalValue, trace: &mut EvalTrace) {
220 match v {
221 EvalValue::Scalar(cv) => trace.operand_values.push(cv.clone()),
222 EvalValue::Range(rows) => {
223 for row in rows {
224 for cv in row {
225 trace.operand_values.push(cv.clone());
226 }
227 }
228 },
229 }
230}
231
232struct Ctx<'a> {
237 env: &'a CellEnv,
238 errs: &'a HashMap<String, ExcelError>,
239 current_sheet: &'a str,
240 trace: &'a mut EvalTrace,
241}
242
243impl Ctx<'_> {
244 fn eval(&mut self, e: &Expr) -> CellValue {
246 eval_expr(e, self.env, self.errs, self.current_sheet, self.trace)
247 }
248}
249
250fn eval_binary_op(e: &Expr, left: &Expr, op: BinOp, right: &Expr, ctx: &mut Ctx) -> CellValue {
256 if matches!(op, BinOp::Pow) {
257 let lv = ctx.eval(left);
258 let rv = ctx.eval(right);
259 return match (semantics::to_number(&lv), semantics::to_number(&rv)) {
260 (Ok(b), Ok(x)) => finite_or_num(powf(b, x)),
261 (Err(e), _) | (_, Err(e)) => CellValue::Error(e),
262 };
263 }
264 if is_leaf_lowerable(left) && is_leaf_lowerable(right) {
265 record_refs(e, ctx.env, ctx.current_sheet, ctx.trace);
266 return eval_leaf(e, ctx.env, ctx.errs);
267 }
268 let l = ctx.eval(left);
269 let r = ctx.eval(right);
270 let lowered = Expr::BinaryOp {
271 left: Box::new(scalar_to_leaf(&l)),
272 op,
273 right: Box::new(scalar_to_leaf(&r)),
274 };
275 eval_leaf(&lowered, ctx.env, ctx.errs)
276}
277
278fn eval_unary_op(e: &Expr, op: UnOp, operand: &Expr, ctx: &mut Ctx) -> CellValue {
284 if matches!(op, UnOp::Percent) {
285 let v = ctx.eval(operand);
286 return match semantics::to_number(&v) {
287 Ok(x) => finite_or_num(percent(x)),
288 Err(e) => CellValue::Error(e),
289 };
290 }
291 if is_leaf_lowerable(operand) {
292 record_refs(e, ctx.env, ctx.current_sheet, ctx.trace);
293 return eval_leaf(e, ctx.env, ctx.errs);
294 }
295 let v = ctx.eval(operand);
296 let lowered = Expr::UnaryOp {
297 op,
298 operand: Box::new(scalar_to_leaf(&v)),
299 };
300 eval_leaf(&lowered, ctx.env, ctx.errs)
301}
302
303fn finite_or_num(n: f64) -> CellValue {
307 if n.is_nan() {
308 CellValue::Error(ExcelError::DivZero)
309 } else if !n.is_finite() {
310 CellValue::Error(ExcelError::Num)
311 } else {
312 CellValue::Number(n)
313 }
314}
315
316fn is_leaf_lowerable(e: &Expr) -> bool {
319 match e {
320 Expr::Ref(_) | Expr::Number(_) | Expr::Str(_) | Expr::Bool(_) | Expr::ErrorLit(_) => true,
321 Expr::Range(_) | Expr::Name(_) | Expr::Call { .. } => false,
322 Expr::BinaryOp { left, op, right } => {
323 !matches!(op, BinOp::Pow) && is_leaf_lowerable(left) && is_leaf_lowerable(right)
324 },
325 Expr::UnaryOp { op, operand } => !matches!(op, UnOp::Percent) && is_leaf_lowerable(operand),
326 }
327}
328
329fn scalar_to_leaf(cv: &CellValue) -> Expr {
331 match cv {
332 CellValue::Number(n) => Expr::Number(*n),
333 CellValue::Text(s) => Expr::Str(s.clone()),
334 CellValue::Bool(b) => Expr::Bool(*b),
335 CellValue::Empty => Expr::Number(0.0), CellValue::Error(e) => Expr::ErrorLit(*e),
337 }
338}
339
340fn materialize_arg(
346 a: &Expr,
347 env: &CellEnv,
348 errs: &HashMap<String, ExcelError>,
349 current_sheet: &str,
350 trace: &mut EvalTrace,
351) -> EvalValue {
352 match a {
353 Expr::Range(range) => match resolve::expand_range(range, current_sheet) {
354 Ok((keys, shape)) => build_range(&keys, shape, env, errs, trace),
355 Err(_) => {
356 trace.short_circuited.get_or_insert(ExcelError::Ref);
357 EvalValue::Scalar(CellValue::Error(ExcelError::Ref))
358 },
359 },
360 Expr::Name(_) => EvalValue::Scalar(CellValue::Error(ExcelError::Name)),
361 scalar => EvalValue::Scalar(eval_expr(scalar, env, errs, current_sheet, trace)),
362 }
363}
364
365fn build_range(
371 keys: &[String],
372 shape: resolve::RangeShape,
373 env: &CellEnv,
374 errs: &HashMap<String, ExcelError>,
375 trace: &mut EvalTrace,
376) -> EvalValue {
377 let rows = shape.rows as usize;
378 let cols = shape.cols as usize;
379 let mut out: Vec<Vec<CellValue>> = Vec::with_capacity(rows);
380 for r in 0..rows {
381 let mut row_cells: Vec<CellValue> = Vec::with_capacity(cols);
382 for c in 0..cols {
383 let key = &keys[c * rows + r];
384 let cv = match env_lookup(env, key) {
385 Some(cv) => cv,
386 None => match errs.get(key) {
388 Some(e) => CellValue::Error(*e),
389 None => {
391 trace.short_circuited.get_or_insert(ExcelError::Ref);
392 CellValue::Error(ExcelError::Ref)
393 },
394 },
395 };
396 trace.resolved_refs.push((key.clone(), cv.clone()));
397 row_cells.push(cv);
398 }
399 out.push(row_cells);
400 }
401 EvalValue::Range(out)
402}
403
404fn collect_ref_keys(e: &Expr, current_sheet: &str, out: &mut Vec<String>) {
415 match e {
416 Expr::Ref(addr) => out.push(addr.clone()),
417 Expr::Range(range) => {
418 if let Ok((keys, _shape)) = resolve::expand_range(range, current_sheet) {
421 out.extend(keys);
422 }
423 },
424 Expr::BinaryOp { left, right, .. } => {
425 collect_ref_keys(left, current_sheet, out);
426 collect_ref_keys(right, current_sheet, out);
427 },
428 Expr::UnaryOp { operand, .. } => collect_ref_keys(operand, current_sheet, out),
429 Expr::Call { args, .. } => {
430 for a in args {
431 collect_ref_keys(a, current_sheet, out);
432 }
433 },
434 Expr::Number(_) | Expr::Str(_) | Expr::Bool(_) | Expr::Name(_) | Expr::ErrorLit(_) => {},
435 }
436}
437
438fn record_refs(e: &Expr, env: &CellEnv, current_sheet: &str, trace: &mut EvalTrace) {
441 let mut keys = Vec::new();
442 collect_ref_keys(e, current_sheet, &mut keys);
443 for key in keys {
444 let cv = env_lookup(env, &key).unwrap_or(CellValue::Empty);
445 trace.resolved_refs.push((key, cv));
446 }
447}
448
449pub fn build_dag(ir: &HashMap<String, Cell>) -> Dag {
459 let mut dag = Dag::new();
460 for (key, cell) in ir {
461 dag.add_node(key);
462 if let CellExpr::Formula(e) = &cell.expr {
463 let current_sheet = owning_sheet(key);
466 let mut deps = Vec::new();
467 collect_ref_keys(e, current_sheet, &mut deps);
468 for dep in deps {
469 dag.add_edge(key, &dep);
470 }
471 }
472 }
473 dag
474}
475
476fn env_lookup(env: &CellEnv, key: &str) -> Option<CellValue> {
479 env.get(key).map(from_json)
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::formula::BinOp;
486 use crate::range_ref::RangeRef;
487
488 fn lit(key: &str, n: f64) -> (String, Cell) {
489 (
490 key.to_string(),
491 Cell {
492 key: key.to_string(),
493 expr: CellExpr::Literal(CellValue::Number(n)),
494 },
495 )
496 }
497
498 fn formula(key: &str, e: Expr) -> (String, Cell) {
499 (
500 key.to_string(),
501 Cell {
502 key: key.to_string(),
503 expr: CellExpr::Formula(e),
504 },
505 )
506 }
507
508 fn dag_of(edges: &[(&str, &[&str])]) -> Dag {
509 let mut dag = Dag::new();
510 for (cell, deps) in edges {
511 dag.add_node(cell);
512 for d in *deps {
513 dag.add_edge(cell, d);
514 }
515 }
516 dag
517 }
518
519 fn range(sheet: &str, start: &str, end: &str) -> Expr {
520 Expr::Range(RangeRef {
521 sheet: sheet.to_string(),
522 start: start.to_string(),
523 end: end.to_string(),
524 })
525 }
526
527 fn call(name: &str, args: Vec<Expr>) -> Expr {
528 Expr::Call {
529 name: name.to_string(),
530 args,
531 }
532 }
533
534 #[test]
535 fn literal_is_seeded_and_readable_downstream() {
536 let ir: HashMap<String, Cell> = [
537 lit("S!A1", 3.0),
538 formula("S!B1", Expr::Ref("S!A1".to_string())),
539 ]
540 .into_iter()
541 .collect();
542 let dag = dag_of(&[("S!A1", &[]), ("S!B1", &["S!A1"])]);
543 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
544 assert_eq!(out.computed.get("S!B1"), Some(&CellValue::Number(3.0)));
545 }
546
547 #[test]
548 fn leaf_arithmetic_computes_via_pure_rust() {
549 let ir: HashMap<String, Cell> = [
550 lit("S!A1", 10.0),
551 lit("S!A2", 5.0),
552 formula(
553 "S!C1",
554 Expr::BinaryOp {
555 left: Box::new(Expr::Ref("S!A1".to_string())),
556 op: BinOp::Add,
557 right: Box::new(Expr::Ref("S!A2".to_string())),
558 },
559 ),
560 ]
561 .into_iter()
562 .collect();
563 let dag = dag_of(&[("S!A1", &[]), ("S!A2", &[]), ("S!C1", &["S!A1", "S!A2"])]);
564 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
565 assert_eq!(out.computed.get("S!C1"), Some(&CellValue::Number(15.0)));
566 }
567
568 #[test]
569 fn cycle_is_one_located_finding_not_a_panic() {
570 let ir: HashMap<String, Cell> = [
571 formula("S!A1", Expr::Ref("S!B1".to_string())),
572 formula("S!B1", Expr::Ref("S!A1".to_string())),
573 ]
574 .into_iter()
575 .collect();
576 let mut dag = Dag::new();
577 dag.add_edge("S!A1", "S!B1");
578 dag.add_edge("S!B1", "S!A1");
579 let err = run(&ir, &dag, &CellEnv::new()).expect_err("a cycle must be Err");
580 assert_eq!(err.rule, "dag/cycle");
581 assert_eq!(err.severity, Severity::Error);
582 }
583
584 #[test]
585 fn eval_trace_records_resolved_refs() {
586 let ir: HashMap<String, Cell> = [
587 lit("S!A1", 10.0),
588 lit("S!A2", 5.0),
589 formula(
590 "S!C1",
591 Expr::BinaryOp {
592 left: Box::new(Expr::Ref("S!A1".to_string())),
593 op: BinOp::Add,
594 right: Box::new(Expr::Ref("S!A2".to_string())),
595 },
596 ),
597 ]
598 .into_iter()
599 .collect();
600 let dag = dag_of(&[("S!A1", &[]), ("S!A2", &[]), ("S!C1", &["S!A1", "S!A2"])]);
601 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
602 let trace = out.traces.get("S!C1").expect("a trace for C1");
603 assert_eq!(
604 trace.resolved_refs,
605 vec![
606 ("S!A1".to_string(), CellValue::Number(10.0)),
607 ("S!A2".to_string(), CellValue::Number(5.0)),
608 ]
609 );
610 }
611
612 #[test]
613 fn error_cell_short_circuits_downstream() {
614 let ir: HashMap<String, Cell> = [
615 (
616 "S!A1".to_string(),
617 Cell {
618 key: "S!A1".to_string(),
619 expr: CellExpr::Literal(CellValue::Error(ExcelError::Ref)),
620 },
621 ),
622 formula(
623 "S!B1",
624 Expr::BinaryOp {
625 left: Box::new(Expr::Ref("S!A1".to_string())),
626 op: BinOp::Add,
627 right: Box::new(Expr::Number(1.0)),
628 },
629 ),
630 ]
631 .into_iter()
632 .collect();
633 let dag = dag_of(&[("S!A1", &[]), ("S!B1", &["S!A1"])]);
634 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
635 assert_eq!(
636 out.computed.get("S!B1"),
637 Some(&CellValue::Error(ExcelError::Ref))
638 );
639 }
640
641 #[test]
642 fn sum_range_1d_column_major() {
643 let ir: HashMap<String, Cell> = [
644 lit("S!B2", 10.0),
645 lit("S!B3", 20.0),
646 lit("S!B4", 30.0),
647 formula("S!C1", call("SUM", vec![range("S", "B2", "B4")])),
648 ]
649 .into_iter()
650 .collect();
651 let dag = dag_of(&[
652 ("S!B2", &[]),
653 ("S!B3", &[]),
654 ("S!B4", &[]),
655 ("S!C1", &["S!B2", "S!B3", "S!B4"]),
656 ]);
657 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
658 assert_eq!(out.computed.get("S!C1"), Some(&CellValue::Number(60.0)));
659 }
660
661 #[test]
662 fn sum_over_range_member_with_computed_error_propagates_that_error() {
663 let ir: HashMap<String, Cell> = [
670 lit("S!B2", 10.0),
671 formula("S!B3", Expr::ErrorLit(ExcelError::DivZero)),
672 lit("S!B4", 30.0),
673 formula("S!C1", call("SUM", vec![range("S", "B2", "B4")])),
674 ]
675 .into_iter()
676 .collect();
677 let dag = dag_of(&[
678 ("S!B2", &[]),
679 ("S!B3", &[]),
680 ("S!B4", &[]),
681 ("S!C1", &["S!B2", "S!B3", "S!B4"]),
682 ]);
683 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
684 assert_eq!(
685 out.computed.get("S!C1"),
686 Some(&CellValue::Error(ExcelError::DivZero)),
687 "the member's actual error propagates, not #REF!"
688 );
689 let trace = out.traces.get("S!C1").expect("a trace for C1");
691 assert!(
692 trace
693 .resolved_refs
694 .iter()
695 .any(|(k, v)| k == "S!B3" && *v == CellValue::Error(ExcelError::DivZero)),
696 "resolved_refs records S!B3 as #DIV/0!, got {:?}",
697 trace.resolved_refs
698 );
699 }
700
701 #[test]
702 fn nested_call_in_binary_op() {
703 let ir: HashMap<String, Cell> = [
704 lit("S!A1", 1.0),
705 lit("S!A2", 2.0),
706 lit("S!A3", 3.0),
707 lit("S!B1", 4.567),
708 formula(
709 "S!C1",
710 Expr::BinaryOp {
711 left: Box::new(call("SUM", vec![range("S", "A1", "A3")])),
712 op: BinOp::Add,
713 right: Box::new(call(
714 "ROUND",
715 vec![Expr::Ref("S!B1".to_string()), Expr::Number(2.0)],
716 )),
717 },
718 ),
719 ]
720 .into_iter()
721 .collect();
722 let dag = dag_of(&[
723 ("S!A1", &[]),
724 ("S!A2", &[]),
725 ("S!A3", &[]),
726 ("S!B1", &[]),
727 ("S!C1", &["S!A1", "S!A2", "S!A3", "S!B1"]),
728 ]);
729 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
730 match out.computed.get("S!C1") {
731 Some(CellValue::Number(n)) => assert!((n - 10.57).abs() < 1e-9, "got {n}"),
732 other => panic!("expected Number(10.57), got {other:?}"),
733 }
734 }
735
736 #[test]
737 fn pow_and_percent_are_not_in_the_evaluator() {
738 let ir: HashMap<String, Cell> = [
739 formula(
740 "S!C1",
741 Expr::BinaryOp {
742 left: Box::new(Expr::Number(2.0)),
743 op: BinOp::Pow,
744 right: Box::new(Expr::Number(3.0)),
745 },
746 ),
747 formula(
748 "S!D1",
749 Expr::UnaryOp {
750 op: UnOp::Percent,
751 operand: Box::new(Expr::Number(50.0)),
752 },
753 ),
754 ]
755 .into_iter()
756 .collect();
757 let dag = dag_of(&[("S!C1", &[]), ("S!D1", &[])]);
758 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
759 assert_eq!(out.computed.get("S!C1"), Some(&CellValue::Number(8.0)));
760 assert_eq!(out.computed.get("S!D1"), Some(&CellValue::Number(0.5)));
761 }
762
763 #[test]
764 fn build_dag_includes_range_member_edges() {
765 let ir: HashMap<String, Cell> = [
768 lit("S!B2", 10.0),
769 lit("S!B3", 20.0),
770 lit("S!B4", 30.0),
771 formula("S!C1", call("SUM", vec![range("S", "B2", "B4")])),
772 ]
773 .into_iter()
774 .collect();
775 let dag = build_dag(&ir);
776 let mut deps = dag.dependencies_of("S!C1").to_vec();
777 deps.sort();
778 assert_eq!(
779 deps,
780 vec!["S!B2".to_string(), "S!B3".to_string(), "S!B4".to_string()],
781 "every range member is a DAG edge (not dropped)"
782 );
783 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
785 assert_eq!(out.computed.get("S!C1"), Some(&CellValue::Number(60.0)));
786 }
787
788 #[test]
789 fn unqualified_range_defaults_to_the_owning_cells_sheet() {
790 let ir: HashMap<String, Cell> = [
795 lit("S!B2", 10.0),
796 lit("S!B3", 20.0),
797 lit("S!B4", 30.0),
798 formula("S!C1", call("SUM", vec![range("", "B2", "B4")])),
799 ]
800 .into_iter()
801 .collect();
802 let dag = build_dag(&ir);
804 let mut deps = dag.dependencies_of("S!C1").to_vec();
805 deps.sort();
806 assert_eq!(
807 deps,
808 vec!["S!B2".to_string(), "S!B3".to_string(), "S!B4".to_string()],
809 "unqualified range edges are sheet-qualified, not phantom \"!B2\" nodes"
810 );
811 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
813 assert_eq!(out.computed.get("S!C1"), Some(&CellValue::Number(60.0)));
814 }
815
816 #[test]
817 fn build_dag_ref_edges_drive_a_correct_run() {
818 let ir: HashMap<String, Cell> = [
819 lit("S!A1", 3.0),
820 formula(
821 "S!B1",
822 Expr::BinaryOp {
823 left: Box::new(Expr::Ref("S!A1".to_string())),
824 op: BinOp::Add,
825 right: Box::new(Expr::Number(1.0)),
826 },
827 ),
828 ]
829 .into_iter()
830 .collect();
831 let dag = build_dag(&ir);
832 assert_eq!(dag.dependencies_of("S!B1"), &["S!A1".to_string()]);
833 let out = run(&ir, &dag, &CellEnv::new()).expect("no cycle");
834 assert_eq!(out.computed.get("S!B1"), Some(&CellValue::Number(4.0)));
835 }
836
837 #[test]
838 fn coil_band_ceiling_reconciles_700() {
839 let ir: HashMap<String, Cell> = [
841 formula(
842 "5_Quantities!C8",
843 call(
844 "CEILING",
845 vec![
846 Expr::BinaryOp {
847 left: Box::new(Expr::Ref("5_Quantities!C6".to_string())),
848 op: BinOp::Mul,
849 right: Box::new(Expr::Ref("2_Constants!C17".to_string())),
850 },
851 Expr::Ref("2_Constants!C18".to_string()),
852 ],
853 ),
854 ),
855 lit("2_Constants!C17", 1.05),
856 lit("2_Constants!C18", 50.0),
857 ]
858 .into_iter()
859 .collect();
860 let mut dag = Dag::new();
861 dag.add_node("5_Quantities!C6");
862 dag.add_node("2_Constants!C17");
863 dag.add_node("2_Constants!C18");
864 dag.add_edge("5_Quantities!C8", "5_Quantities!C6");
865 dag.add_edge("5_Quantities!C8", "2_Constants!C17");
866 dag.add_edge("5_Quantities!C8", "2_Constants!C18");
867 let seed = CellEnv::new().seed_cell("5_Quantities!C6", &CellValue::Number(666.0));
868 let out = run(&ir, &dag, &seed).expect("no cycle");
869 assert_eq!(
870 out.computed.get("5_Quantities!C8"),
871 Some(&CellValue::Number(700.0))
872 );
873 }
874}