truecalc_workbook/recalc.rs
1//! The recalc engine (plan item 3.3, issue #535): the layer that makes a
2//! [`Workbook`] actually recompute.
3//!
4//! A workbook stores formulas verbatim with their last evaluated result
5//! ([`Value::Empty`] until first recalc, P3.4). Recalc walks the dependency
6//! graph (P3.2), evaluates every formula cell in dependency order through a
7//! grid-backed [`Resolver`] (the core P1.3 seam), and writes each new result
8//! back into the grid — returning the ordered list of [`Change`]s it made.
9//!
10//! # Two modes, one result
11//!
12//! - [`Workbook::recalc`] is a **full** recalc: it evaluates every formula
13//! cell in topological order.
14//! - [`Workbook::recalc_incremental`] is an **incremental** recalc: given the
15//! cells an edit touched, it recomputes only their transitive dependents
16//! (plus all volatile cells, which are always dirty — scope ADR Decision 3),
17//! reusing the stored results of everything outside that closure.
18//!
19//! Both produce the same grid for the same workbook + context: incremental
20//! recalc is full recalc restricted to the dirty closure, and the property
21//! `recalc_incremental(edits) ≡ recalc()` is asserted by the test suite (the
22//! issue's acceptance criterion).
23//!
24//! # Determinism and `RecalcContext`
25//!
26//! Recalc takes an explicit [`RecalcContext`] (scope ADR Decision 3): the same
27//! workbook + same context produces a byte-identical grid. The context pins the
28//! volatile date functions (`NOW`/`TODAY`) to a fixed instant via core's
29//! `evaluate_with_resolver_at` `now_serial` hook, with the UTC→local serial
30//! conversion done against a **vendored** IANA timezone database (`chrono-tz`),
31//! never the host clock or OS tz tables. See [`RecalcContext`] for the RNG
32//! caveat.
33//!
34//! # Cycles
35//!
36//! A formula cell on a dependency cycle (and any cell the cycle taints) cannot
37//! be evaluated in order; recalc assigns it the Sheets circular-dependency
38//! error without looping forever. Cycle membership comes from the graph's
39//! Tarjan SCC pass ([`DependencyGraph::cycle_cells`]); see [`CIRCULAR_ERROR`].
40
41use std::collections::{BTreeMap, BTreeSet, VecDeque};
42
43use chrono::{NaiveDate, TimeZone, Timelike, Utc};
44use chrono_tz::Tz;
45use icu_casemap::CaseMapperBorrowed;
46use truecalc_core::eval::EvalHook;
47use truecalc_core::{Engine, EngineFlavor, ErrorKind, Ref, Resolver, Value as CoreValue};
48
49use crate::address::Address;
50use crate::casefold::simple_fold;
51use crate::cell::Cell;
52use crate::depgraph::{CellRef, DependencyGraph, Precedent, RangeRef};
53use crate::spill::{spill_rect, SpillRect, BLOCKED_SPILL_ERROR};
54use crate::value::Value;
55use crate::workbook::Workbook;
56
57/// The error a cell on (or downstream of) a circular dependency takes.
58///
59/// Google Sheets reports a circular dependency as `#REF!` (surfaced in the UI
60/// as "Circular dependency detected"). A dedicated workbook-level cycle
61/// fixture is not yet in the repo (the P3.6 set covers cross-sheet, named
62/// ranges, and date-type), so this exact code is **not** fixture-pinned here;
63/// the in-repo cycle tests assert the engine's behavior (a cycle is detected,
64/// every cell on it takes this error, and recalc terminates), and the code is
65/// re-verified once a `cycles` fixture lands (issue note).
66pub const CIRCULAR_ERROR: &str = "#REF!";
67
68/// The deterministic context a recalc evaluates against (scope ADR Decision 3).
69///
70/// Same workbook + same `RecalcContext` ⇒ byte-identical recomputed grid. The
71/// context is an **input to recalc**, never part of the workbook value or its
72/// JSON (value-object ADR): two recalcs with different contexts legitimately
73/// differ, and the property tests compare like-context runs only.
74///
75/// # Volatile pinning
76///
77/// - **`NOW()` / `TODAY()`** are pinned: [`timestamp_ms`](Self::timestamp_ms)
78/// (a UTC instant) is converted to a local spreadsheet serial against the
79/// **vendored** [`timezone`](Self::timezone) (`chrono-tz`, not the host tz
80/// database), and that serial is passed to core's
81/// `evaluate_with_resolver_at`. The conversion is the determinism envelope:
82/// same instant + same timezone + same truecalc version ⇒ same serial.
83/// - **`RAND()` / `RANDBETWEEN()` / `RANDARRAY()`** carry a
84/// [`rng_seed`](Self::rng_seed) and a per-cell key helper ([`Self::rng_key`])
85/// implementing the ADR's `prf(seed, sheet_index, row, col, draw_index)`
86/// scheme. **Caveat:** core's RNG functions presently read the system clock
87/// directly and take no per-cell key (`crates/core/.../math/rand`), so the
88/// workbook layer cannot yet inject this seed into them — full PRF-keyed RNG
89/// determinism requires a core change and is tracked for P4. `rng_seed` is
90/// carried now so the API is stable; recalc therefore guarantees determinism
91/// for non-RNG workbooks (which is every P3.6 fixture).
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct RecalcContext {
94 /// The evaluation instant, in milliseconds since the Unix epoch (UTC).
95 /// `NOW()`/`TODAY()` derive from this.
96 timestamp_ms: i64,
97 /// The IANA timezone the instant is rendered into a local serial against,
98 /// from the vendored `chrono-tz` snapshot.
99 timezone: Tz,
100 /// Keys the deterministic per-cell RNG draws (ADR `prf(...)`); see the
101 /// type-level caveat about core support.
102 rng_seed: u64,
103}
104
105impl RecalcContext {
106 /// Builds a context from a UTC instant (Unix milliseconds), an IANA
107 /// timezone id (e.g. `"Etc/GMT"`, `"America/New_York"`), and an RNG seed.
108 ///
109 /// Returns `None` if `tz` is not a known IANA id in the vendored database.
110 pub fn new(timestamp_ms: i64, tz: &str, rng_seed: u64) -> Option<Self> {
111 let timezone: Tz = tz.parse().ok()?;
112 Some(Self {
113 timestamp_ms,
114 timezone,
115 rng_seed,
116 })
117 }
118
119 /// The UTC instant this context pins volatile time to (Unix milliseconds).
120 pub fn timestamp_ms(&self) -> i64 {
121 self.timestamp_ms
122 }
123
124 /// The vendored IANA timezone the instant is localized against.
125 pub fn timezone(&self) -> Tz {
126 self.timezone
127 }
128
129 /// The RNG seed keying deterministic per-cell draws.
130 pub fn rng_seed(&self) -> u64 {
131 self.rng_seed
132 }
133
134 /// The local spreadsheet serial datetime this context pins `NOW()`/`TODAY()`
135 /// to: the UTC `timestamp_ms` rendered into `timezone`, expressed as days
136 /// since the 1899-12-30 epoch (integer part) plus time-of-day (fraction) —
137 /// the `now_serial` core's `evaluate_at` family consumes.
138 ///
139 /// Returns `None` only if the instant is unrepresentable (e.g. out of
140 /// `chrono`'s range), which cannot happen for any realistic timestamp.
141 pub fn now_serial(&self) -> Option<f64> {
142 let utc = Utc.timestamp_millis_opt(self.timestamp_ms).single()?;
143 let local = utc.with_timezone(&self.timezone).naive_local();
144 let epoch = NaiveDate::from_ymd_opt(1899, 12, 30)?;
145 let days = local.date().signed_duration_since(epoch).num_days() as f64;
146 let secs = local.time().num_seconds_from_midnight() as f64;
147 Some(days + secs / 86_400.0)
148 }
149
150 /// The pinned "now" as an absolute UTC instant in nanoseconds, for the
151 /// zone-aware `TZNOW`. Derived from the same `timestamp_ms` as
152 /// [`now_serial`](Self::now_serial), so `NOW()` and `TZNOW()` share one
153 /// deterministic clock.
154 pub fn now_utc_nanos(&self) -> Option<i64> {
155 self.timestamp_ms.checked_mul(1_000_000)
156 }
157
158 /// The ADR's per-draw RNG key `prf(rng_seed, sheet_index, row, col,
159 /// draw_index)`, a deterministic, order-independent mixing of the cell
160 /// identity into the seed.
161 ///
162 /// Exposed (and unit-tested) so the keying scheme is fixed and ready for
163 /// the core integration that will consume it; see the type-level caveat.
164 pub fn rng_key(&self, sheet_index: u32, row: u32, col: u32, draw_index: u32) -> u64 {
165 // SplitMix64-style finalizer chained over the identity tuple — pure,
166 // order-independent, and identical across surfaces.
167 let mut h = self.rng_seed;
168 for part in [
169 sheet_index as u64,
170 row as u64,
171 col as u64,
172 draw_index as u64,
173 ] {
174 h = mix64(h ^ mix64(part));
175 }
176 h
177 }
178}
179
180/// One cell whose evaluated value a recalc changed.
181///
182/// Returned (in deterministic order) by [`Workbook::recalc`] and
183/// [`Workbook::recalc_incremental`]: the "change events" of v1, delivered as a
184/// value rather than via a callback (value-object ADR). Ordering is pinned —
185/// by sheet **tab index**, then row, then column (scope ADR Decision 3) — so
186/// the change list is reproducible.
187#[derive(Debug, Clone, PartialEq)]
188pub struct Change {
189 /// The sheet's name (its authored casing).
190 pub sheet: String,
191 /// The recomputed cell's address.
192 pub addr: Address,
193 /// The cell's value before this recalc (the stored result).
194 pub old: Value,
195 /// The cell's value after this recalc.
196 pub new: Value,
197}
198
199impl Workbook {
200 /// Recomputes **every** formula cell in dependency order against `ctx`,
201 /// writing each new result back into the grid and returning the ordered
202 /// list of cells whose value changed.
203 ///
204 /// Formula cells are evaluated in topological order (precedents first), so
205 /// each reads its inputs already current. Cells on a dependency cycle —
206 /// and any cell that cannot be ordered because it (transitively) reads one
207 /// — take the circular-dependency error ([`CIRCULAR_ERROR`]); recalc always
208 /// terminates. Volatile functions are pinned by `ctx` (scope ADR
209 /// Decision 3).
210 ///
211 /// Changes are returned sorted by (sheet tab index, row, column).
212 pub fn recalc(&mut self, ctx: &RecalcContext) -> Vec<Change> {
213 let graph = DependencyGraph::build(self);
214 // Evaluate every formula cell; ordering and cycle handling are shared
215 // with the incremental path.
216 let to_eval: BTreeSet<CellRef> = graph.formula_cells().cloned().collect();
217 self.recompute(&graph, ctx, to_eval)
218 }
219
220 /// Recomputes only the formula cells affected by an edit and returns the
221 /// ordered changes.
222 ///
223 /// `edited` lists the cells a mutation touched (the cell written, or — for
224 /// a named-range retarget — the name's old and new target cells; callers
225 /// pass whatever changed). The recalc closure is the transitive
226 /// [`direct_dependents`](DependencyGraph::direct_dependents_of) of those
227 /// cells, **plus** every volatile formula cell (always dirty, scope ADR
228 /// Decision 3). Everything outside the closure keeps its stored result.
229 ///
230 /// The result is identical to the subset of [`recalc`](Self::recalc)'s
231 /// output for the same edits — the `incremental ≡ full` guarantee.
232 pub fn recalc_incremental(
233 &mut self,
234 ctx: &RecalcContext,
235 edited: &[(String, Address)],
236 ) -> Vec<Change> {
237 let graph = DependencyGraph::build(self);
238 let folder = CaseMapperBorrowed::new();
239
240 // Seed the dirty frontier with the dependents of each edited cell.
241 let mut dirty: BTreeSet<CellRef> = BTreeSet::new();
242 let mut frontier: VecDeque<CellRef> = VecDeque::new();
243 for (sheet, addr) in edited {
244 let folded = simple_fold(&folder, sheet);
245 let seed = CellRef {
246 sheet: folded,
247 addr: *addr,
248 };
249 // The edited cell itself recomputes only if it is a formula; its
250 // dependents always do.
251 if graph.is_formula(&seed) && dirty.insert(seed.clone()) {
252 frontier.push_back(seed.clone());
253 }
254 for dep in graph.direct_dependents_of(&seed) {
255 if dirty.insert(dep.clone()) {
256 frontier.push_back(dep);
257 }
258 }
259 }
260 // Transitive closure over the formula-cell dependents.
261 while let Some(cell) = frontier.pop_front() {
262 for dep in graph.direct_dependents_of(&cell) {
263 if dirty.insert(dep.clone()) {
264 frontier.push_back(dep);
265 }
266 }
267 }
268 // Volatile cells are always dirty (scope ADR Decision 3).
269 for cell in graph.formula_cells() {
270 if self.is_volatile(cell) {
271 dirty.insert(cell.clone());
272 }
273 }
274
275 // Spill-occupancy seeding (issue #591). A cell's spill footprint or
276 // blocked status can change without the dependency graph carrying an
277 // edge that would dirty the cells depending on that change, because a
278 // spilled cell is not a formula node (P3.2) and a *blocked* anchor
279 // stores an error rather than an array that reads its blocker. Two
280 // concrete violations of `incremental ≡ full` (P3.3) follow:
281 //
282 // - **Shrink / replace-with-scalar.** Setting a former array anchor to
283 // a scalar vacates its old footprint, but `set` has already discarded
284 // the prior array, so the widen loop's `before = anchor_rectangles()`
285 // no longer sees the old rectangle and never dirties the readers of
286 // the vacated cells (e.g. `D1 = =B1+1` after `A1` stops spilling onto
287 // `B1`).
288 // - **Unblock.** Clearing or overwriting the cell that blocks a spill
289 // must let the anchor re-expand, but a blocked anchor has no edge to
290 // its blocker, so clearing the blocker never re-dirties the anchor.
291 //
292 // Seeding the dirty set with every spill-occupancy-sensitive cell makes
293 // the closure independent of which edit triggered the recalc, so the
294 // result matches a full recalc despite the lost pre-edit footprint.
295 // Over-seeding is safe: a re-evaluated cell whose value is unchanged
296 // emits no change event (`diff_against_snapshot`), so `incremental ≡
297 // full` is preserved while the minimal-closure guarantee still holds for
298 // ordinary (non-spill) edits, which seed nothing here.
299 self.seed_spill_sensitive(&graph, &mut dirty);
300
301 // A cell that reads a *spilled* cell has no dependency-graph edge to its
302 // spilling anchor (a spilled cell is not a formula node, P3.2), so the
303 // closure above can miss a spilled-cell reader when an anchor's spill
304 // footprint changes. We widen the dirty set to those readers and re-run
305 // until it stabilizes, so an incremental recalc reproduces the full one
306 // (`incremental ≡ full`, P3.3) even across spills (§5).
307 //
308 // To return change events with correct *pre-operation* `old` values
309 // despite the multiple internal recomputes, snapshot every formula
310 // cell's value first, then recompute over the (growing) dirty set until
311 // no anchor's spill footprint changes, and finally diff the resulting
312 // grid against the snapshot. The loop is bounded by the formula-cell
313 // count (each pass strictly grows the dirty set or stops).
314 let pre = self.snapshot_formula_values(&graph);
315 let max_widen = graph.formula_cells().count().saturating_add(2).max(1);
316 for _ in 0..max_widen {
317 let before = self.anchor_rectangles();
318 self.recompute(&graph, ctx, dirty.clone());
319 let after = self.anchor_rectangles();
320
321 let mut added = false;
322 for (sheet, addr) in changed_rectangle_cells(&before, &after) {
323 let spilled_ref = CellRef { sheet, addr };
324 for dep in graph.direct_dependents_of(&spilled_ref) {
325 if dirty.insert(dep) {
326 added = true;
327 }
328 }
329 }
330 if !added {
331 break;
332 }
333 }
334 self.diff_against_snapshot(pre)
335 }
336
337 /// Explains one cell's value against the **currently stored grid** (issue
338 /// #743): evaluates `addr`'s formula once through `hook`, resolving every
339 /// precedent read to its **stored** value (the same grid-backed
340 /// [`Resolver`] semantics `recalc` uses), and returns the value — provably
341 /// the same value `recalc`/`recalc_incremental` would write for this cell,
342 /// provided the grid is already current for its precedents.
343 ///
344 /// This is a point-in-time explain, not a recalc: unlike
345 /// [`Workbook::recalc`], `trace_cell` does **not** recompute anything
346 /// transitively — a precedent's value is whatever is already on the grid
347 /// (or, for a cell inside another anchor's placed spill, the
348 /// reconstructed spilled element — schema spec §5). If the grid is stale
349 /// relative to unapplied edits, `trace_cell` faithfully explains the
350 /// *stale* value; call `recalc` or `recalc_incremental` first if the
351 /// caller needs a fresh grid.
352 ///
353 /// Two pieces of `recalc`'s behavior can't be reproduced from the target
354 /// cell in isolation, so `trace_cell` matches them explicitly rather than
355 /// diverging (an on-demand, single-cell call — a user clicking a cell —
356 /// can afford this; see the two call sites below):
357 ///
358 /// - **Spill occupancy** (schema spec §5): an array result is only stored
359 /// if its target rectangle is free on the current grid; otherwise
360 /// `recalc` stores [`BLOCKED_SPILL_ERROR`] instead, exactly like
361 /// [`Workbook::place_spill`] applies for a real recompute.
362 /// - **Dependency cycles**: `recalc` never evaluates a cycle member's
363 /// formula at all — it short-circuits straight to
364 /// [`CIRCULAR_ERROR`] (see [`DependencyGraph::cycle_cells`] and
365 /// `recompute`). Evaluating the formula anyway would diverge whenever it
366 /// *catches* the error (e.g. `IFERROR`), since its precedents' stored
367 /// values already carry the propagated error but `recalc` never gave the
368 /// formula the chance to run.
369 ///
370 /// `addr` need not be a formula cell: a literal (or empty, or spilled
371 /// non-anchor) cell has no expression to trace, so this returns its
372 /// resolved value directly without invoking `hook` — `hook` observes no
373 /// events in that case, by design (there is nothing to walk). Passing a
374 /// hook is optional in the sense that evaluating with `hook = None`'s
375 /// counterpart, [`Engine::evaluate_with_resolver_at_keyed`], produces this
376 /// same value: `trace_cell` adds observation, it does not change what gets
377 /// computed.
378 pub fn trace_cell(
379 &self,
380 sheet: &str,
381 addr: Address,
382 ctx: &RecalcContext,
383 hook: &mut dyn EvalHook,
384 ) -> Value {
385 let folder = CaseMapperBorrowed::new();
386 let own_sheet = simple_fold(&folder, sheet);
387 let cell_ref = CellRef {
388 sheet: own_sheet.clone(),
389 addr,
390 };
391
392 // A cycle member never gets its formula evaluated by `recalc` — it is
393 // skipped in every pass of `recompute` and then unconditionally
394 // assigned `CIRCULAR_ERROR`, regardless of what the formula itself
395 // might do with its (already error-tainted) precedents. Match that
396 // before evaluating anything. Building the graph is an on-demand,
397 // single-cell, interactive call (a user clicking a cell), so
398 // correctness beats avoiding the graph walk here.
399 let graph = DependencyGraph::build(self);
400 if graph.cycle_cells().contains(&cell_ref) {
401 return Value::Error(CIRCULAR_ERROR.to_owned());
402 }
403
404 // No per-pass recompute state: every precedent read falls straight
405 // through to the stored grid (see `GridResolver::cell_value`'s
406 // fallback chain), which is exactly "explain given the current grid".
407 let empty_values: BTreeMap<CellRef, Value> = BTreeMap::new();
408 let empty_spills: BTreeMap<CellRef, SpillRect> = BTreeMap::new();
409 let empty_cells: BTreeSet<CellRef> = BTreeSet::new();
410 let mut resolver = GridResolver {
411 workbook: self,
412 own_sheet: &own_sheet,
413 new_values: &empty_values,
414 spills: &empty_spills,
415 prev_values: &empty_values,
416 prev_spills: &empty_spills,
417 cycle: &empty_cells,
418 recomputed: &empty_cells,
419 };
420
421 let Some(formula) = self.cell_at(&cell_ref).and_then(Cell::formula) else {
422 // Not a formula: nothing to trace. Resolve the cell's own value
423 // through the same fallback chain a precedent read would use, so
424 // e.g. a spilled (non-anchor) cell still resolves correctly.
425 return core_to_workbook(resolver.cell_value(&own_sheet, addr));
426 };
427 let formula = formula.to_owned();
428
429 let engine = match self.engine() {
430 EngineFlavor::Sheets => Engine::sheets(),
431 EngineFlavor::Excel => Engine::excel(),
432 };
433 let sheet_index = self
434 .sheets()
435 .iter()
436 .position(|ws| simple_fold(&folder, ws.name()) == own_sheet)
437 .unwrap_or(0) as u32;
438 let rng_cell = Some((ctx.rng_seed(), sheet_index, addr.row, addr.column));
439
440 let core = engine.evaluate_with_resolver_at_keyed_hooked(
441 &formula,
442 &mut resolver,
443 ctx.now_serial(),
444 ctx.now_utc_nanos(),
445 rng_cell,
446 Some(hook),
447 );
448 let raw = core_to_workbook(core);
449
450 // Match `eval_formula_cell`'s spill placement: an array result is
451 // only stored if its target rectangle is free on the *current*
452 // stored grid (`place_spill`/`spill_blocked` read `self.cell_at`
453 // directly, so passing fresh, empty per-pass maps here reads exactly
454 // that — no real spill state is mutated).
455 self.place_spill(&cell_ref, raw, &empty_values, &mut BTreeMap::new())
456 }
457
458 /// Shared evaluation core: evaluates `to_eval` (a set of formula cells) in
459 /// dependency order through a grid-backed resolver, applies cycle errors,
460 /// writes results back, and returns the changes in pinned order.
461 fn recompute(
462 &mut self,
463 graph: &DependencyGraph,
464 ctx: &RecalcContext,
465 to_eval: BTreeSet<CellRef>,
466 ) -> Vec<Change> {
467 let now_serial = ctx.now_serial();
468 let now_utc_nanos = ctx.now_utc_nanos();
469 let rng_seed = ctx.rng_seed();
470
471 // Cells on a cycle short-circuit to the circular error; the rest are
472 // evaluated in topological order. `topological_order` returns the full
473 // order when acyclic, else the cycle set; we always have the cycle set
474 // available via `cycle_cells` for the tainted-downstream case.
475 let cycle = graph.cycle_cells();
476 let order = match graph.topological_order() {
477 Ok(order) => order,
478 Err(_) => {
479 // The graph has a cycle. Build a best-effort order over the
480 // acyclic remainder by stripping cycle nodes, so cells that do
481 // not touch the cycle still evaluate; cycle-tainted cells fall
482 // out as the error below.
483 graph.acyclic_order_excluding(&cycle)
484 }
485 };
486
487 // Evaluate in order, resolving array spills as we go (plan item 3.5,
488 // schema spec §5). `new_values` holds each formula's result — a spilling
489 // anchor stores its full `array` (its serialized form, §6); a blocked
490 // anchor stores the Sheets blocked-spill error and no array. `spills`
491 // records the rectangle each *successfully placed* anchor occupies, so
492 // (a) a later anchor competing for one of its cells blocks, and (b) the
493 // resolver returns spilled values to cells that read them (spilled cells
494 // participate in recalc as precedents, §5).
495 //
496 // A cell that *reads* a spilled cell has no dependency-graph edge to the
497 // spilling anchor (a spilled cell is not a formula node, P3.2), so the
498 // topological order does not guarantee the anchor is evaluated first. We
499 // therefore iterate the pass to a fixpoint: each pass re-evaluates every
500 // `to_eval` cell against the prior pass's spills, so a reader that ran
501 // before its anchor in one pass sees the spilled value in the next. The
502 // grid is finite and spill geometry is monotone (an anchor's array
503 // depends only on its own non-spilled precedents), so this converges; we
504 // cap the iteration count at the node count as a hard safety bound.
505 //
506 // Seed the "previous pass" state from the stored grid so an *incremental*
507 // recalc — whose `to_eval` is only the dirty closure — still resolves a
508 // read of a cell spilled by an anchor that is **not** dirty this pass:
509 // that anchor's array is already on the grid, so its spill rectangle is
510 // available as a fallback even though it is never re-placed this recalc.
511 // A full recalc re-places every anchor, overriding the seed.
512 let (mut new_values, mut spills) = self.seed_spills_from_grid();
513 let max_passes = order.len().saturating_add(2).max(1);
514 for _ in 0..max_passes {
515 let mut next_values: BTreeMap<CellRef, Value> = BTreeMap::new();
516 let mut next_spills: BTreeMap<CellRef, SpillRect> = BTreeMap::new();
517 for cell in &order {
518 if cycle.contains(cell) {
519 continue; // handled in the cycle pass below
520 }
521 if !to_eval.contains(cell) {
522 continue;
523 }
524 // Evaluate against this pass's values/spills placed so far, with
525 // the *previous* pass's values/spills as a fallback. The
526 // fallback is what lets a reader that comes *before* its spill
527 // anchor in the order still see the spilled value: the anchor
528 // placed its spill in the previous pass, so the reader resolves
529 // it from `prev_*` even though `next_*` has not reached the
530 // anchor yet this pass.
531 let raw = self.eval_formula_cell(
532 cell,
533 now_serial,
534 now_utc_nanos,
535 rng_seed,
536 &next_values,
537 &next_spills,
538 &new_values,
539 &spills,
540 &cycle,
541 &to_eval,
542 );
543 // Resolve array results into a placed spill or a blocked-spill
544 // error; a placed spill records its rectangle so later anchors
545 // and readers see it. Occupancy is judged against authored cells
546 // and the spills placed so far this pass.
547 let stored = self.place_spill(cell, raw, &next_values, &mut next_spills);
548 next_values.insert(cell.clone(), stored);
549 }
550 let converged = next_values == new_values && next_spills == spills;
551 new_values = next_values;
552 spills = next_spills;
553 if converged {
554 break;
555 }
556 }
557 // Cycle cells (and downstream cells the order could not place) take the
558 // circular error.
559 for cell in &to_eval {
560 if !new_values.contains_key(cell) {
561 new_values.insert(cell.clone(), Value::Error(CIRCULAR_ERROR.to_owned()));
562 }
563 }
564
565 self.apply_changes(new_values)
566 }
567
568 /// Evaluates a single formula cell through a resolver that reads the *new*
569 /// values computed so far this recalc, falling back to the stored grid for
570 /// everything else.
571 #[allow(clippy::too_many_arguments)]
572 fn eval_formula_cell(
573 &self,
574 cell: &CellRef,
575 now_serial: Option<f64>,
576 now_utc_nanos: Option<i64>,
577 rng_seed: u64,
578 new_values: &BTreeMap<CellRef, Value>,
579 spills: &BTreeMap<CellRef, SpillRect>,
580 prev_values: &BTreeMap<CellRef, Value>,
581 prev_spills: &BTreeMap<CellRef, SpillRect>,
582 cycle: &BTreeSet<CellRef>,
583 recomputed: &BTreeSet<CellRef>,
584 ) -> Value {
585 let formula = match self.cell_at(cell).and_then(Cell::formula) {
586 Some(f) => f.to_owned(),
587 None => return Value::Empty,
588 };
589 let engine = match self.engine() {
590 EngineFlavor::Sheets => Engine::sheets(),
591 EngineFlavor::Excel => Engine::excel(),
592 };
593 let folder = CaseMapperBorrowed::new();
594 let sheet_index = self
595 .sheets()
596 .iter()
597 .position(|ws| simple_fold(&folder, ws.name()) == cell.sheet)
598 .unwrap_or(0) as u32;
599 let rng_cell = Some((rng_seed, sheet_index, cell.addr.row, cell.addr.column));
600 let mut resolver = GridResolver {
601 workbook: self,
602 own_sheet: &cell.sheet,
603 new_values,
604 spills,
605 prev_values,
606 prev_spills,
607 cycle,
608 recomputed,
609 };
610 let core = engine.evaluate_with_resolver_at_keyed(
611 &formula,
612 &mut resolver,
613 now_serial,
614 now_utc_nanos,
615 rng_cell,
616 );
617 core_to_workbook(core)
618 }
619
620 /// Turns a freshly evaluated formula result into its **stored** value,
621 /// applying Sheets spill semantics (plan item 3.5, schema spec §5).
622 ///
623 /// A non-array result is stored verbatim. An array result is a spill anchor:
624 /// it occupies the `m × n` rectangle anchored at `cell`. If every non-anchor
625 /// cell of that rectangle is free — not authored, and not already claimed by
626 /// an earlier anchor's placed spill (`placed`) — and the rectangle stays in
627 /// the sheet's address bounds, the spill is *placed*: its rectangle is
628 /// recorded in `placed` and the anchor stores the full array (its serialized
629 /// form, §6; the spilled cells are reconstructed, never serialized). If any
630 /// target is occupied or the rectangle is out of bounds, the spill is
631 /// **blocked**: the anchor takes the Sheets blocked-spill error
632 /// ([`BLOCKED_SPILL_ERROR`]) and stores no array (§5, §12).
633 fn place_spill(
634 &self,
635 cell: &CellRef,
636 value: Value,
637 new_values: &BTreeMap<CellRef, Value>,
638 placed: &mut BTreeMap<CellRef, SpillRect>,
639 ) -> Value {
640 let Value::Array(ref rows) = value else {
641 return value; // scalar result: stored as-is
642 };
643 let nrows = rows.len();
644 let ncols = rows.first().map_or(0, Vec::len);
645 // `core_array_to_workbook` guarantees a rectangular, ≥ 2-cell array.
646 let Some(rect) = spill_rect(cell.addr, nrows, ncols) else {
647 // Out-of-bounds rectangle is blocked (§5).
648 return Value::Error(BLOCKED_SPILL_ERROR.to_owned());
649 };
650 if self.spill_blocked(cell, &rect, new_values, placed) {
651 return Value::Error(BLOCKED_SPILL_ERROR.to_owned());
652 }
653 placed.insert(cell.clone(), rect);
654 value
655 }
656
657 /// Whether the spill `rect` anchored at `cell` is blocked: any non-anchor
658 /// cell of the rectangle is authored on that sheet, is itself an evaluated
659 /// formula in this recalc (`new_values`), or already lies in an earlier
660 /// anchor's placed spill (`placed`). Schema spec §5.
661 fn spill_blocked(
662 &self,
663 cell: &CellRef,
664 rect: &SpillRect,
665 new_values: &BTreeMap<CellRef, Value>,
666 placed: &BTreeMap<CellRef, SpillRect>,
667 ) -> bool {
668 for addr in rect.spilled_cells() {
669 let target = CellRef {
670 sheet: cell.sheet.clone(),
671 addr,
672 };
673 // An authored cell in the way (literal or formula).
674 if self.cell_at(&target).is_some() {
675 return true;
676 }
677 // A formula cell evaluated this recalc that is not itself authored
678 // in the grid cannot exist, but a formula reader could be in
679 // `new_values`; treat any computed cell here as occupied for safety.
680 if new_values.contains_key(&target) {
681 return true;
682 }
683 // A cell already claimed by an earlier anchor's spill.
684 if placed
685 .values()
686 .any(|r| r.anchor != cell.addr && r.contains(addr))
687 {
688 return true;
689 }
690 }
691 false
692 }
693
694 /// Builds the spill state implied by the **stored** grid: every authored
695 /// cell whose stored value is an `array` is a spill anchor occupying its
696 /// reconstructed rectangle (schema spec §5). Returns the anchor → array map
697 /// and the anchor → rectangle map, used to seed an incremental recalc so a
698 /// read of a spilled cell whose anchor is not dirty this pass still resolves
699 /// (the anchor placed the spill in a prior recalc). An out-of-bounds stored
700 /// array — which a valid document never contains (`from_json` rejects it,
701 /// validate.rs §5) — is skipped.
702 fn seed_spills_from_grid(&self) -> (BTreeMap<CellRef, Value>, BTreeMap<CellRef, SpillRect>) {
703 let folder = CaseMapperBorrowed::new();
704 let mut values: BTreeMap<CellRef, Value> = BTreeMap::new();
705 let mut spills: BTreeMap<CellRef, SpillRect> = BTreeMap::new();
706 for sheet in self.sheets() {
707 let folded = simple_fold(&folder, sheet.name());
708 for (addr, cell) in sheet.iter() {
709 let Value::Array(rows) = cell.value() else {
710 continue;
711 };
712 let nrows = rows.len();
713 let ncols = rows.first().map_or(0, Vec::len);
714 if let Some(rect) = spill_rect(addr, nrows, ncols) {
715 let key = CellRef {
716 sheet: folded.clone(),
717 addr,
718 };
719 values.insert(key.clone(), cell.value().clone());
720 spills.insert(key, rect);
721 }
722 }
723 }
724 (values, spills)
725 }
726
727 /// Writes the recomputed values back, emitting a [`Change`] for each cell
728 /// whose value actually changed, in pinned (sheet index, row, column) order.
729 fn apply_changes(&mut self, new_values: BTreeMap<CellRef, Value>) -> Vec<Change> {
730 let folder = CaseMapperBorrowed::new();
731 // Resolve folded sheet names to tab index + authored name once.
732 let mut changes: Vec<(usize, Change)> = Vec::new();
733 for (cell, new) in new_values {
734 let Some(idx) = self.sheet_index_folded(&folder, &cell.sheet) else {
735 continue; // sheet vanished (cannot happen mid-recalc)
736 };
737 let sheet_name = self.sheets()[idx].name().to_owned();
738 let old = self.sheets()[idx]
739 .get(cell.addr)
740 .map(|c| c.value().clone())
741 .unwrap_or(Value::Empty);
742 if old == new {
743 continue;
744 }
745 // Preserve the formula text; only the stored value updates.
746 let formula = self.sheets()[idx]
747 .get(cell.addr)
748 .and_then(|c| c.formula())
749 .map(str::to_owned);
750 if let Some(formula) = formula {
751 self.sheets_mut()[idx].set(cell.addr, Cell::with_formula(formula, new.clone()));
752 }
753 changes.push((
754 idx,
755 Change {
756 sheet: sheet_name,
757 addr: cell.addr,
758 old,
759 new,
760 },
761 ));
762 }
763 // Pin order: sheet tab index, then row, then column.
764 changes.sort_by(|a, b| {
765 a.0.cmp(&b.0)
766 .then(a.1.addr.row.cmp(&b.1.addr.row))
767 .then(a.1.addr.column.cmp(&b.1.addr.column))
768 });
769 changes.into_iter().map(|(_, c)| c).collect()
770 }
771
772 /// Whether `cell`'s formula calls any volatile function (`NOW`, `TODAY`,
773 /// `RAND`, `RANDBETWEEN`, `RANDARRAY` — core's `VOLATILE_FUNCTIONS`).
774 /// A volatile cell is always dirty in incremental recalc.
775 fn is_volatile(&self, cell: &CellRef) -> bool {
776 let Some(formula) = self.cell_at(cell).and_then(Cell::formula) else {
777 return false;
778 };
779 let upper = formula.to_ascii_uppercase();
780 truecalc_core::Registry::VOLATILE_FUNCTIONS
781 .iter()
782 .any(|name| contains_call(&upper, name))
783 }
784
785 /// Every formula cell's current stored value, keyed by [`CellRef`]. The
786 /// pre-operation snapshot an incremental recalc diffs its final grid against
787 /// to emit change events with correct `old` values despite internal
788 /// re-recomputes (spill widening).
789 fn snapshot_formula_values(&self, graph: &DependencyGraph) -> BTreeMap<CellRef, Value> {
790 let mut snap = BTreeMap::new();
791 for cell in graph.formula_cells() {
792 let value = self
793 .cell_at(cell)
794 .map(|c| c.value().clone())
795 .unwrap_or(Value::Empty);
796 snap.insert(cell.clone(), value);
797 }
798 snap
799 }
800
801 /// Adds every spill-occupancy-sensitive formula cell to `dirty` (issue
802 /// #591), so an incremental recalc reproduces a full recalc across any spill
803 /// footprint or blocked-status change even though the dependency graph
804 /// carries no edge for those transitions and `set` discarded the pre-edit
805 /// footprint.
806 ///
807 /// A cell is seeded when it is, or reads something that can become or cease
808 /// being, a spill:
809 ///
810 /// 1. **Every array anchor** (a formula cell whose stored value is an
811 /// array) — re-placed so a footprint that should shrink or grow does so,
812 /// and so a write into its region re-blocks it.
813 /// 2. **Every blocked-spill anchor** (a formula cell whose stored value is
814 /// the blocked-spill error) — re-attempted so clearing/overwriting its
815 /// blocker lets it re-expand (the unblock case).
816 /// 3. **Every reader of a non-authored single cell** — that precedent is
817 /// empty or spilled today and may flip either way, e.g. `D1 = =B1+1`
818 /// whose `B1` was spilled by a now-shrunk anchor (the vacated-reader
819 /// case), or a reader of a cell a spill is about to grow onto.
820 /// 4. **Every reader of a range that overlaps a current spill rectangle** —
821 /// a range aggregation whose window includes spilled cells, so a change
822 /// to that spill (grow/shrink/block) re-aggregates.
823 ///
824 /// The blocked-spill error string equals [`BLOCKED_SPILL_ERROR`]; a cell
825 /// merely *holding* that error that is not actually a former/blocked spill
826 /// anchor is harmless to re-evaluate (it recomputes to the same value).
827 fn seed_spill_sensitive(&self, graph: &DependencyGraph, dirty: &mut BTreeSet<CellRef>) {
828 let rects = self.anchor_rectangles();
829 for cell in graph.formula_cells() {
830 // (1)/(2): the cell itself is (or held) a spill.
831 let is_spill_cell = match self.cell_at(cell).map(Cell::value) {
832 Some(Value::Array(_)) => true,
833 Some(Value::Error(code)) | Some(Value::ErrorMsg(code, _)) => {
834 code == BLOCKED_SPILL_ERROR
835 }
836 _ => false,
837 };
838 let mut seed = is_spill_cell;
839 // (3)/(4): the cell reads a spill-sensitive precedent.
840 if !seed {
841 if let Some(precedents) = graph.precedents_of(cell) {
842 seed = precedents
843 .iter()
844 .any(|p| self.precedent_is_spill_sensitive(p, &rects));
845 }
846 }
847 if seed {
848 dirty.insert(cell.clone());
849 }
850 }
851 }
852
853 /// Whether a single precedent reads a cell that is, or could become, a
854 /// spilled cell (issue #591): a non-authored single-cell target (empty or
855 /// spilled today), or a range overlapping a current spill rectangle.
856 fn precedent_is_spill_sensitive(
857 &self,
858 precedent: &Precedent,
859 rects: &BTreeMap<CellRef, SpillRect>,
860 ) -> bool {
861 match precedent {
862 // A single-cell precedent that is not authored is empty or spilled
863 // today, and may flip either way (grow/shrink/block/unblock).
864 Precedent::Cell(c) => self.cell_at(c).is_none(),
865 // A range precedent is spill-sensitive if it overlaps a current
866 // spill rectangle (a spill could grow/shrink/block within it) *or*
867 // if it contains any non-authored cell — which catches a cell a
868 // spill *used to* cover but no longer does (the lost pre-edit
869 // footprint of a shrink/collapse), since that cell is now empty.
870 Precedent::Range(r) => {
871 rects
872 .iter()
873 .any(|(anchor, rect)| anchor.sheet == r.sheet && rect_overlaps_range(rect, r))
874 || self.range_has_unauthored_cell(r)
875 }
876 // A name resolves to a cell or range; treat it conservatively as
877 // spill-sensitive so a name pointing at a spilled cell still seeds
878 // its reader. Names are rare and this only widens the dirty set.
879 Precedent::Name(_) => true,
880 Precedent::Unresolved(_) => false,
881 }
882 }
883
884 /// Whether the range `r` contains at least one cell that is **not** an
885 /// authored cell (empty or spilled). Computed by comparing the range's area
886 /// to the number of authored cells inside it — so the cost is bounded by the
887 /// sheet's populated-cell count, never the range area (issue #591).
888 fn range_has_unauthored_cell(&self, r: &RangeRef) -> bool {
889 let folder = CaseMapperBorrowed::new();
890 let Some(sheet) = self
891 .sheets()
892 .iter()
893 .find(|s| simple_fold(&folder, s.name()) == r.sheet)
894 else {
895 // The range targets a missing sheet; nothing authored, so it is
896 // (vacuously) all-unauthored — seed conservatively.
897 return true;
898 };
899 let rows = (r.end.row - r.start.row + 1) as u64;
900 let cols = (r.end.column - r.start.column + 1) as u64;
901 let area = rows.saturating_mul(cols);
902 let authored_inside = sheet
903 .iter()
904 .filter(|(addr, _)| {
905 addr.row >= r.start.row
906 && addr.row <= r.end.row
907 && addr.column >= r.start.column
908 && addr.column <= r.end.column
909 })
910 .count() as u64;
911 authored_inside < area
912 }
913
914 /// Every spill rectangle currently on the stored grid (anchor → rectangle),
915 /// derived from authored cells whose stored value is an array (schema spec
916 /// §5). Used to detect when an incremental recompute changed a spill
917 /// footprint so the affected readers can be dirtied.
918 fn anchor_rectangles(&self) -> BTreeMap<CellRef, SpillRect> {
919 let folder = CaseMapperBorrowed::new();
920 let mut rects = BTreeMap::new();
921 for sheet in self.sheets() {
922 let folded = simple_fold(&folder, sheet.name());
923 for (addr, cell) in sheet.iter() {
924 let Value::Array(rows) = cell.value() else {
925 continue;
926 };
927 let nrows = rows.len();
928 let ncols = rows.first().map_or(0, Vec::len);
929 if let Some(rect) = spill_rect(addr, nrows, ncols) {
930 rects.insert(
931 CellRef {
932 sheet: folded.clone(),
933 addr,
934 },
935 rect,
936 );
937 }
938 }
939 }
940 rects
941 }
942
943 /// Emits the change list for an incremental recalc by diffing the final grid
944 /// against the pre-operation `snapshot`: one [`Change`] per formula cell
945 /// whose value differs, in the pinned (sheet tab index, row, column) order.
946 fn diff_against_snapshot(&self, snapshot: BTreeMap<CellRef, Value>) -> Vec<Change> {
947 let folder = CaseMapperBorrowed::new();
948 let mut changes: Vec<(usize, Change)> = Vec::new();
949 for (cell, old) in snapshot {
950 let Some(idx) = self.sheet_index_folded(&folder, &cell.sheet) else {
951 continue;
952 };
953 let new = self.sheets()[idx]
954 .get(cell.addr)
955 .map(|c| c.value().clone())
956 .unwrap_or(Value::Empty);
957 if old == new {
958 continue;
959 }
960 changes.push((
961 idx,
962 Change {
963 sheet: self.sheets()[idx].name().to_owned(),
964 addr: cell.addr,
965 old,
966 new,
967 },
968 ));
969 }
970 changes.sort_by(|a, b| {
971 a.0.cmp(&b.0)
972 .then(a.1.addr.row.cmp(&b.1.addr.row))
973 .then(a.1.addr.column.cmp(&b.1.addr.column))
974 });
975 changes.into_iter().map(|(_, c)| c).collect()
976 }
977
978 /// The cell at a [`CellRef`] (folded sheet + address), or `None`.
979 fn cell_at(&self, cell: &CellRef) -> Option<&Cell> {
980 let folder = CaseMapperBorrowed::new();
981 let idx = self.sheet_index_folded(&folder, &cell.sheet)?;
982 self.sheets()[idx].get(cell.addr)
983 }
984
985 /// Tab index of the sheet whose folded name equals `folded`.
986 fn sheet_index_folded(
987 &self,
988 folder: &CaseMapperBorrowed<'static>,
989 folded: &str,
990 ) -> Option<usize> {
991 self.sheets()
992 .iter()
993 .position(|s| simple_fold(folder, s.name()) == folded)
994 }
995}
996
997/// A [`Resolver`] backed by the workbook grid, reading the values computed so
998/// far this recalc before falling back to the stored grid.
999struct GridResolver<'a> {
1000 workbook: &'a Workbook,
1001 own_sheet: &'a str,
1002 new_values: &'a BTreeMap<CellRef, Value>,
1003 /// Spills placed so far **this pass** (anchor → rectangle): a read of a cell
1004 /// inside one of these rectangles resolves to the spilled array element
1005 /// (schema spec §5 — spilled cells participate as precedents).
1006 spills: &'a BTreeMap<CellRef, SpillRect>,
1007 /// The **previous** pass's values, used as a fallback so a reader ordered
1008 /// before its spill anchor still sees the spilled value (the anchor placed
1009 /// it last pass). Empty on the first pass.
1010 prev_values: &'a BTreeMap<CellRef, Value>,
1011 /// The previous pass's spills (same fallback role as `prev_values`).
1012 prev_spills: &'a BTreeMap<CellRef, SpillRect>,
1013 cycle: &'a BTreeSet<CellRef>,
1014 /// The formula cells being recomputed this recalc (`to_eval`). The stored
1015 /// grid still holds these anchors' *pre-recalc* arrays until `apply_changes`
1016 /// runs, so the `grid_spilled_value` fallback must ignore an anchor in this
1017 /// set: its authoritative spill state for this recalc is the per-pass
1018 /// `spills`/`prev_spills`, not the stale grid (issue #591 — otherwise a
1019 /// reader of a cell an anchor *stops* spilling onto, e.g. when the anchor
1020 /// blocks or shrinks, would resolve the vacated cell from the obsolete
1021 /// stored array).
1022 recomputed: &'a BTreeSet<CellRef>,
1023}
1024
1025impl GridResolver<'_> {
1026 /// The current value of a resolved cell: this recalc's fresh value if it
1027 /// was already computed, else the stored grid value, else empty. A cell on
1028 /// a cycle resolves to the circular error (so a cell that *reads* a cycle
1029 /// inherits the taint).
1030 fn cell_value(&self, sheet_folded: &str, addr: Address) -> CoreValue {
1031 let key = CellRef {
1032 sheet: sheet_folded.to_owned(),
1033 addr,
1034 };
1035 if self.cycle.contains(&key) {
1036 return CoreValue::Error(ErrorKind::Ref);
1037 }
1038 if let Some(v) = self.new_values.get(&key) {
1039 return workbook_to_core(v);
1040 }
1041 let folder = CaseMapperBorrowed::new();
1042 if let Some(c) = self
1043 .workbook
1044 .sheets()
1045 .iter()
1046 .find(|s| simple_fold(&folder, s.name()) == sheet_folded)
1047 .and_then(|s| s.get(addr))
1048 {
1049 return workbook_to_core(c.value());
1050 }
1051 // Not authored and not freshly computed: it may be a spilled cell of an
1052 // anchor placed this pass — or, if the anchor is ordered *after* this
1053 // reader, of the previous pass (schema spec §5). Resolve through the
1054 // spill, preferring this pass's placement.
1055 if let Some(v) = self.spilled_value(sheet_folded, addr, self.spills, self.new_values) {
1056 return workbook_to_core(&v);
1057 }
1058 if let Some(v) = self.spilled_value(sheet_folded, addr, self.prev_spills, self.prev_values)
1059 {
1060 return workbook_to_core(&v);
1061 }
1062 // A cell whose value the previous pass computed but this pass has not
1063 // reached yet (a reader's plain-cell precedent ordered after it).
1064 if let Some(v) = self.prev_values.get(&key) {
1065 return workbook_to_core(v);
1066 }
1067 // Final fallback (matters for *incremental* recalc): the cell may be
1068 // spilled by an anchor that is not dirty this recalc, so it never enters
1069 // the per-pass maps. Its array is on the stored grid; reconstruct the
1070 // element directly (schema spec §5).
1071 if let Some(v) = self.grid_spilled_value(sheet_folded, addr) {
1072 return workbook_to_core(&v);
1073 }
1074 CoreValue::Empty
1075 }
1076
1077 /// The value spilled to `addr` on `sheet_folded` per the **stored grid**:
1078 /// scans authored anchors whose stored value is an array and reconstructs
1079 /// the element (schema spec §5). Used as the incremental-recalc fallback for
1080 /// spills whose anchor is not re-evaluated this pass.
1081 fn grid_spilled_value(&self, sheet_folded: &str, addr: Address) -> Option<Value> {
1082 let folder = CaseMapperBorrowed::new();
1083 let sheet = self
1084 .workbook
1085 .sheets()
1086 .iter()
1087 .find(|s| simple_fold(&folder, s.name()) == sheet_folded)?;
1088 for (anchor_addr, cell) in sheet.iter() {
1089 if anchor_addr == addr {
1090 continue;
1091 }
1092 // An anchor being recomputed this recalc has its current spill state
1093 // in the per-pass maps; its stored array is stale until
1094 // `apply_changes`, so never resolve through it here (issue #591).
1095 let anchor_key = CellRef {
1096 sheet: sheet_folded.to_owned(),
1097 addr: anchor_addr,
1098 };
1099 if self.recomputed.contains(&anchor_key) {
1100 continue;
1101 }
1102 let Value::Array(rows) = cell.value() else {
1103 continue;
1104 };
1105 let nrows = rows.len();
1106 let ncols = rows.first().map_or(0, Vec::len);
1107 let Some(rect) = crate::spill::spill_rect(anchor_addr, nrows, ncols) else {
1108 continue;
1109 };
1110 if let Some((i, j)) = rect.offset_of(addr) {
1111 return rows.get(i).and_then(|r| r.get(j)).cloned();
1112 }
1113 }
1114 None
1115 }
1116
1117 /// The value spilled to `addr` on `sheet_folded` per a given `spills` map
1118 /// and its backing `values`: the `[i][j]` element of the anchor's stored
1119 /// array (schema spec §5). `None` if `addr` is not a non-anchor cell of any
1120 /// spill in `spills`.
1121 fn spilled_value(
1122 &self,
1123 sheet_folded: &str,
1124 addr: Address,
1125 spills: &BTreeMap<CellRef, SpillRect>,
1126 values: &BTreeMap<CellRef, Value>,
1127 ) -> Option<Value> {
1128 for (anchor, rect) in spills {
1129 if anchor.sheet != sheet_folded {
1130 continue;
1131 }
1132 if anchor.addr == addr {
1133 continue; // the anchor itself is in `values`
1134 }
1135 let Some((i, j)) = rect.offset_of(addr) else {
1136 continue;
1137 };
1138 if let Some(Value::Array(rows)) = values.get(anchor) {
1139 return rows.get(i).and_then(|r| r.get(j)).cloned();
1140 }
1141 }
1142 None
1143 }
1144
1145 /// Resolves the folded target sheet name for a `Ref`'s optional sheet
1146 /// qualifier, or `None` if the named sheet does not exist.
1147 fn target_sheet(&self, sheet: &Option<String>) -> Option<String> {
1148 let folder = CaseMapperBorrowed::new();
1149 match sheet {
1150 None => Some(self.own_sheet.to_owned()),
1151 Some(name) => self
1152 .workbook
1153 .sheet(name)
1154 .map(|s| simple_fold(&folder, s.name())),
1155 }
1156 }
1157}
1158
1159impl Resolver for GridResolver<'_> {
1160 fn resolve(&mut self, r: &Ref) -> CoreValue {
1161 match r {
1162 Ref::Cell { sheet, addr } => {
1163 let Some(target) = self.target_sheet(sheet) else {
1164 return CoreValue::Error(ErrorKind::Ref);
1165 };
1166 match Address::new(addr.row, addr.col) {
1167 Some(a) => self.cell_value(&target, a),
1168 None => CoreValue::Error(ErrorKind::Ref),
1169 }
1170 }
1171 Ref::Range { sheet, start, end } => {
1172 let Some(target) = self.target_sheet(sheet) else {
1173 return CoreValue::Error(ErrorKind::Ref);
1174 };
1175 self.resolve_range(&target, start, end)
1176 }
1177 Ref::Name(name) => {
1178 // Resolve the name to its canonical ref, then resolve that.
1179 let folder = CaseMapperBorrowed::new();
1180 let folded = simple_fold(&folder, name);
1181 let target = self
1182 .workbook
1183 .names()
1184 .iter()
1185 .find(|nr| simple_fold(&folder, &nr.name) == folded);
1186 match target {
1187 None => CoreValue::Error(ErrorKind::Name),
1188 // Re-parse the name's canonical `Sheet!A1` ref so a name
1189 // pointing at a cell or a range resolves identically to a
1190 // literal ref of the same shape.
1191 Some(nr) => self.resolve_name_ref(&nr.r#ref),
1192 }
1193 }
1194 }
1195 }
1196}
1197
1198impl GridResolver<'_> {
1199 /// Materializes a range as a core `Value::Array` of its cells in row-major
1200 /// reading order — the shape the P1.3 [`Resolver`] contract specifies
1201 /// ("a range -> a Value::Array of the cells in reading order") and the shape
1202 /// core's aggregations (SUM/AVERAGE/COUNT/SUMIF) and shape functions
1203 /// consume.
1204 ///
1205 /// A single-column, multi-row range (a *vertical* range) is materialized
1206 /// as a nested `Array` of one-element row `Array`s — core's Nx1 column
1207 /// shape (see `to_2d`/`from_2d` in the array functions) — so elementwise
1208 /// operations over it (e.g. `=A1:A3*2`) spill down like Google Sheets,
1209 /// instead of losing their column orientation to a flat row. Every other
1210 /// shape (a single row, a single cell, or a genuine 2-D block) keeps the
1211 /// existing flat row-major array, unchanged. The own/target sheet has
1212 /// already been resolved.
1213 fn resolve_range(
1214 &self,
1215 sheet_folded: &str,
1216 start: &truecalc_core::CellAddr,
1217 end: &truecalc_core::CellAddr,
1218 ) -> CoreValue {
1219 let (r0, r1) = (start.row.min(end.row), start.row.max(end.row));
1220 let (c0, c1) = (start.col.min(end.col), start.col.max(end.col));
1221 let is_vertical = r1 > r0 && c0 == c1;
1222 let mut cells: Vec<CoreValue> = Vec::new();
1223 for r in r0..=r1 {
1224 for c in c0..=c1 {
1225 match Address::new(r, c) {
1226 Some(a) => {
1227 let v = self.cell_value(sheet_folded, a);
1228 // A spill anchor stores the full array; its individual
1229 // elements are visited when the range iteration reaches
1230 // the spilled positions (which resolve via spilled_value).
1231 // Use only the [0][0] element here to avoid double-counting.
1232 let scalar = match v {
1233 CoreValue::Array(ref rows) => match rows.first() {
1234 Some(CoreValue::Array(ref cols)) => {
1235 cols.first().cloned().unwrap_or(CoreValue::Empty)
1236 }
1237 Some(other) => other.clone(),
1238 None => CoreValue::Empty,
1239 },
1240 other => other,
1241 };
1242 cells.push(if is_vertical {
1243 CoreValue::Array(vec![scalar])
1244 } else {
1245 scalar
1246 });
1247 }
1248 None => cells.push(if is_vertical {
1249 CoreValue::Array(vec![CoreValue::Error(ErrorKind::Ref)])
1250 } else {
1251 CoreValue::Error(ErrorKind::Ref)
1252 }),
1253 }
1254 }
1255 }
1256 CoreValue::Array(cells)
1257 }
1258
1259 /// Resolves a named range's canonical `ref` string (`Sheet!A1` /
1260 /// `Sheet!A1:B2`) the same way a literal reference resolves.
1261 fn resolve_name_ref(&mut self, r: &str) -> CoreValue {
1262 let engine = match self.workbook.engine() {
1263 EngineFlavor::Sheets => Engine::sheets(),
1264 EngineFlavor::Excel => Engine::excel(),
1265 };
1266 // The ref string parses as a one-reference formula; extract and resolve.
1267 let formula = format!("={r}");
1268 match engine.parse(&formula) {
1269 Ok(expr) => {
1270 let refs = truecalc_core::extract_refs(&expr);
1271 match refs.first() {
1272 Some(first) => self.resolve(first),
1273 None => CoreValue::Error(ErrorKind::Ref),
1274 }
1275 }
1276 Err(_) => CoreValue::Error(ErrorKind::Ref),
1277 }
1278 }
1279}
1280
1281/// Maps a core evaluated [`CoreValue`] to the workbook [`Value`] (schema §6).
1282/// Core arrays (flat or nested rows) become a rectangular 2-D workbook array;
1283/// a 1×1 array collapses to its scalar (schema §6).
1284fn core_to_workbook(v: CoreValue) -> Value {
1285 match v {
1286 CoreValue::Number(n) => Value::Number(n),
1287 CoreValue::Text(s) => Value::Text(s),
1288 CoreValue::Bool(b) => Value::Boolean(b),
1289 CoreValue::Error(e) => Value::Error(e.to_string()),
1290 CoreValue::ErrorMsg(e, m) => Value::ErrorMsg(e.to_string(), m),
1291 CoreValue::Empty => Value::Empty,
1292 CoreValue::Date(n) => Value::Date(n),
1293 CoreValue::Zoned(z) => Value::Zoned(z),
1294 CoreValue::Sparkline(spec) => Value::Sparkline(spec),
1295 CoreValue::Array(elems) => core_array_to_workbook(elems),
1296 }
1297}
1298
1299/// Normalizes a core array (which may be flat scalars or nested rows) into the
1300/// workbook's row-major 2-D shape, collapsing a 1×1 array to its scalar.
1301fn core_array_to_workbook(elems: Vec<CoreValue>) -> Value {
1302 if elems.is_empty() {
1303 // An empty array has no scalar form; surface as #REF! (a degenerate
1304 // spill the P3.5 engine will own). Kept minimal here.
1305 return Value::Error("#REF!".to_owned());
1306 }
1307 let nested = elems.iter().all(|e| matches!(e, CoreValue::Array(_)));
1308 let rows: Vec<Vec<Value>> = if nested {
1309 elems
1310 .into_iter()
1311 .map(|row| match row {
1312 CoreValue::Array(cells) => cells.into_iter().map(core_to_workbook).collect(),
1313 other => vec![core_to_workbook(other)],
1314 })
1315 .collect()
1316 } else {
1317 vec![elems.into_iter().map(core_to_workbook).collect()]
1318 };
1319 if rows.len() == 1 && rows[0].len() == 1 {
1320 return rows.into_iter().next().unwrap().into_iter().next().unwrap();
1321 }
1322 Value::Array(rows)
1323}
1324
1325/// Maps a workbook [`Value`] back to a core [`CoreValue`] for feeding a stored
1326/// cell value into evaluation through the resolver.
1327fn workbook_to_core(v: &Value) -> CoreValue {
1328 match v {
1329 Value::Number(n) => CoreValue::Number(*n),
1330 Value::Text(s) => CoreValue::Text(s.clone()),
1331 Value::Boolean(b) => CoreValue::Bool(*b),
1332 Value::Error(code) | Value::ErrorMsg(code, _) => {
1333 CoreValue::Error(error_kind_from_code(code))
1334 }
1335 Value::Empty => CoreValue::Empty,
1336 Value::Date(n) => CoreValue::Date(*n),
1337 Value::Zoned(z) => CoreValue::Zoned(z.clone()),
1338 Value::Sparkline(spec) => CoreValue::Sparkline(spec.clone()),
1339 Value::Array(rows) => CoreValue::Array(
1340 rows.iter()
1341 .map(|row| CoreValue::Array(row.iter().map(workbook_to_core).collect()))
1342 .collect(),
1343 ),
1344 }
1345}
1346
1347/// Parses a Sheets error code string back to a core [`ErrorKind`]; an unknown
1348/// code maps to `#REF!` (the most conservative reference error).
1349fn error_kind_from_code(code: &str) -> ErrorKind {
1350 match code {
1351 "#DIV/0!" => ErrorKind::DivByZero,
1352 "#VALUE!" => ErrorKind::Value,
1353 "#REF!" => ErrorKind::Ref,
1354 "#NAME?" => ErrorKind::Name,
1355 "#NUM!" => ErrorKind::Num,
1356 "#N/A" => ErrorKind::NA,
1357 "#NULL!" => ErrorKind::Null,
1358 _ => ErrorKind::Ref,
1359 }
1360}
1361
1362/// Whether `upper` (an upper-cased formula) calls the function `name`, i.e.
1363/// `name` appears followed by `(` (ignoring spaces). Avoids matching a name
1364/// that is merely a substring of a longer identifier.
1365fn contains_call(upper: &str, name: &str) -> bool {
1366 let bytes = upper.as_bytes();
1367 let nb = name.as_bytes();
1368 let mut i = 0;
1369 while let Some(pos) = find_from(bytes, nb, i) {
1370 // Preceding char must not be an identifier char.
1371 let before_ok = pos == 0 || !is_ident_byte(bytes[pos - 1]);
1372 // Following non-space char must be '('.
1373 let mut j = pos + nb.len();
1374 while j < bytes.len() && bytes[j] == b' ' {
1375 j += 1;
1376 }
1377 let after_ok = j < bytes.len() && bytes[j] == b'(';
1378 if before_ok && after_ok {
1379 return true;
1380 }
1381 i = pos + 1;
1382 }
1383 false
1384}
1385
1386fn find_from(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
1387 if needle.is_empty() || from + needle.len() > haystack.len() {
1388 return None;
1389 }
1390 (from..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
1391}
1392
1393fn is_ident_byte(b: u8) -> bool {
1394 b.is_ascii_alphanumeric() || b == b'_'
1395}
1396
1397/// The set of `(folded sheet, address)` cells whose spill coverage changed
1398/// between two anchor-rectangle maps: the union of all cells in any rectangle
1399/// that appeared, vanished, or resized (schema spec §5). Their readers may now
1400/// be stale and must be dirtied in an incremental recalc.
1401fn changed_rectangle_cells(
1402 before: &BTreeMap<CellRef, SpillRect>,
1403 after: &BTreeMap<CellRef, SpillRect>,
1404) -> BTreeSet<(String, Address)> {
1405 let mut out: BTreeSet<(String, Address)> = BTreeSet::new();
1406 let mut consider = |anchor: &CellRef, rect: &SpillRect| {
1407 // The anchor cell itself is a formula node with its own graph edges;
1408 // only the spilled cells need this spill-aware dirtying.
1409 for addr in rect.spilled_cells() {
1410 out.insert((anchor.sheet.clone(), addr));
1411 }
1412 };
1413 for (anchor, rect) in before {
1414 match after.get(anchor) {
1415 Some(same) if same == rect => {}
1416 _ => consider(anchor, rect),
1417 }
1418 }
1419 for (anchor, rect) in after {
1420 match before.get(anchor) {
1421 Some(same) if same == rect => {}
1422 _ => consider(anchor, rect),
1423 }
1424 }
1425 out
1426}
1427
1428/// Whether a spill rectangle and a range reference overlap (same sheet assumed
1429/// checked by the caller): their inclusive row/column extents intersect (issue
1430/// #591). Used to seed range aggregations that read spilled cells.
1431fn rect_overlaps_range(rect: &SpillRect, range: &RangeRef) -> bool {
1432 let rect_r0 = rect.anchor.row;
1433 let rect_r1 = rect.anchor.row + rect.rows - 1;
1434 let rect_c0 = rect.anchor.column;
1435 let rect_c1 = rect.anchor.column + rect.cols - 1;
1436 rect_r0 <= range.end.row
1437 && rect_r1 >= range.start.row
1438 && rect_c0 <= range.end.column
1439 && rect_c1 >= range.start.column
1440}
1441
1442/// SplitMix64 finalizer — a fast, well-distributed integer mix.
1443fn mix64(mut z: u64) -> u64 {
1444 z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
1445 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1446 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1447 z ^ (z >> 31)
1448}