1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Regression test for a cross-sheet evaluation-order bug.
//!
//! When a workbook-level defined name spans multiple cells on Sheet B and
//! each cell of that range carries a formula that references a *formula*
//! cell on Sheet A, the engine should evaluate all cells correctly. Prior
//! to the fix, the first cell of the multi-cell defined name on Sheet B
//! evaluated to ``None``/``0`` while subsequent cells evaluated correctly
//! — the source cell on Sheet A was not yet materialized when the first
//! Sheet B cell was visited.
//!
//! Surfaced from supermod CalculatedSchedules (#1967) where a multi-period
//! rollup metric defined on the "Model" sheet referenced per-period totals
//! on a "Schedule" sheet; the first time period silently returned None.
use crate::engine::named_range::{NameScope, NamedDefinition};
use crate::engine::{Engine, EvalConfig};
use crate::reference::{CellRef, Coord, RangeRef};
use crate::test_workbook::TestWorkbook;
use formualizer_common::LiteralValue;
use formualizer_parse::parser::parse;
fn build_repro_engine() -> Engine<TestWorkbook> {
let mut engine = Engine::new(TestWorkbook::default(), EvalConfig::default());
// ── Sheet "Schedule" ─────────────────────────────────────────────
engine.graph.add_sheet("Schedule").unwrap();
for r in 2..=5u32 {
engine
.set_cell_value("Schedule", r, 1, LiteralValue::Number(r as f64 * 100.0))
.unwrap();
engine
.set_cell_formula("Schedule", r, 3, parse(format!("=$A${r}")).unwrap())
.unwrap();
}
engine
.set_cell_formula("Schedule", 6, 3, parse("=SUM(C2:C5)").unwrap())
.unwrap();
// ── Sheet "Model" with a multi-cell workbook-level defined name ──
engine.graph.add_sheet("Model").unwrap();
let model_sheet = engine.graph.sheet_id("Model").unwrap();
let metric_start = CellRef::new(model_sheet, Coord::new(7, 4, true, true));
let metric_end = CellRef::new(model_sheet, Coord::new(7, 6, true, true));
engine
.define_name(
"METRIC",
NamedDefinition::Range(RangeRef::new(metric_start, metric_end)),
NameScope::Workbook,
)
.unwrap();
engine
.set_cell_formula("Model", 7, 4, parse("='Schedule'!C6").unwrap())
.unwrap();
engine
.set_cell_formula("Model", 7, 5, parse("='Schedule'!C6").unwrap())
.unwrap();
engine
.set_cell_formula("Model", 7, 6, parse("='Schedule'!C6").unwrap())
.unwrap();
engine
}
#[test]
fn first_cell_of_cross_sheet_dn_range_resolves_via_evaluate_all() {
let mut engine = build_repro_engine();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Model", 7, 4),
Some(LiteralValue::Number(1400.0)),
"Model!D7 (first cell of METRIC range) must resolve to the cross-sheet \
formula-cell value"
);
assert_eq!(
engine.get_cell_value("Model", 7, 5),
Some(LiteralValue::Number(1400.0))
);
assert_eq!(
engine.get_cell_value("Model", 7, 6),
Some(LiteralValue::Number(1400.0))
);
}
#[test]
fn first_cell_of_cross_sheet_dn_range_resolves_via_evaluate_cell() {
// Demand-driven per-cell evaluation (the path used by the supermod
// ``FormualizerModel.calculate()`` adapter, which iterates a range
// calling ``evaluate_cell`` per slot). This is where the surfaced
// bug actually appears: the first cell's cross-sheet ref returns
// the wrong value because its source isn't materialized yet.
let mut engine = build_repro_engine();
let v_d7 = engine.evaluate_cell("Model", 7, 4).unwrap();
let v_e7 = engine.evaluate_cell("Model", 7, 5).unwrap();
let v_f7 = engine.evaluate_cell("Model", 7, 6).unwrap();
assert_eq!(
v_d7,
Some(LiteralValue::Number(1400.0)),
"Model!D7 (first cell of METRIC range) via evaluate_cell — got {:?}",
v_d7
);
assert_eq!(v_e7, Some(LiteralValue::Number(1400.0)));
assert_eq!(v_f7, Some(LiteralValue::Number(1400.0)));
}
#[test]
fn cross_sheet_evaluate_cell_drains_all_staged_sheets() {
// ``defer_graph_building`` mode is what the xlsx loader uses
// (``Workbook::interactive()``). Formulas land in a staging map and
// are promoted to the graph on first evaluate. Before the fix,
// ``evaluate_cell`` only promoted the *target* sheet's staged
// formulas — so a target cell whose formula referenced another
// sheet would read the still-unevaluated source and silently
// return ``None``. This test mirrors the xlsx-load path by staging
// formulas explicitly, then evaluating a single cross-sheet target
// cell. The fix in ``evaluate_cell`` calls ``build_graph_all`` so
// every staged sheet is materialized before the target's formula
// tries to read its dependencies.
let cfg = EvalConfig {
defer_graph_building: true,
..Default::default()
};
let mut engine = Engine::new(TestWorkbook::default(), cfg);
engine.graph.add_sheet("Schedule").unwrap();
engine.graph.add_sheet("Model").unwrap();
for r in 2..=5u32 {
engine
.set_cell_value("Schedule", r, 1, LiteralValue::Number(r as f64 * 100.0))
.unwrap();
}
// Stage formulas (the xlsx-load path uses ``stage_formula_text``).
for r in 2..=5u32 {
engine.stage_formula_text("Schedule", r, 3, format!("=$A${r}"));
}
engine.stage_formula_text("Schedule", 6, 3, "=SUM(C2:C5)".to_string());
engine.stage_formula_text("Model", 7, 4, "='Schedule'!C6".to_string());
// Target a cell on Model. Before the fix, only ``Model``'s staged
// formulas were promoted — so ``Schedule!C6`` remained unevaluated
// and the target read ``None``. After the fix, all staged sheets
// are promoted and the cross-sheet ref resolves cleanly.
let v = engine.evaluate_cell("Model", 7, 4).unwrap();
assert_eq!(
v,
Some(LiteralValue::Number(1400.0)),
"evaluate_cell must drain all staged sheets so cross-sheet refs \
to formula cells resolve; got {:?}",
v
);
}