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::cell::{RefCell, RefMut};
42use std::collections::{BTreeMap, BTreeSet, VecDeque};
43use std::sync::Arc;
44
45use chrono::{NaiveDate, TimeZone, Timelike, Utc};
46use chrono_tz::Tz;
47use icu_casemap::CaseMapperBorrowed;
48use truecalc_core::eval::EvalHook;
49use truecalc_core::{Engine, EngineFlavor, ErrorKind, Ref, Resolver, Value as CoreValue};
50
51use crate::address::Address;
52use crate::authored_index::AuthoredCellIndex;
53use crate::casefold::simple_fold;
54use crate::cell::Cell;
55use crate::depgraph::{CellRef, DependencyGraph, NameTarget, Precedent, RangeRef};
56use crate::graph_cache::CachedGraph;
57use crate::grid_spills::GridSpillIndex;
58use crate::named_ref;
59use crate::sheet_index::SheetIndex;
60use crate::spill::{spill_rect, SpillRect, BLOCKED_SPILL_ERROR};
61use crate::table_ref;
62use crate::value::Value;
63use crate::workbook::Workbook;
64use crate::worksheet::Worksheet;
65
66/// The error a cell on (or downstream of) a circular dependency takes.
67///
68/// Google Sheets reports a circular dependency as `#REF!` (surfaced in the UI
69/// as "Circular dependency detected"). A dedicated workbook-level cycle
70/// fixture is not yet in the repo (the P3.6 set covers cross-sheet, named
71/// ranges, and date-type), so this exact code is **not** fixture-pinned here;
72/// the in-repo cycle tests assert the engine's behavior (a cycle is detected,
73/// every cell on it takes this error, and recalc terminates), and the code is
74/// re-verified once a `cycles` fixture lands (issue note).
75pub const CIRCULAR_ERROR: &str = "#REF!";
76
77/// The deterministic context a recalc evaluates against (scope ADR Decision 3).
78///
79/// Same workbook + same `RecalcContext` ⇒ byte-identical recomputed grid. The
80/// context is an **input to recalc**, never part of the workbook value or its
81/// JSON (value-object ADR): two recalcs with different contexts legitimately
82/// differ, and the property tests compare like-context runs only.
83///
84/// # Volatile pinning
85///
86/// - **`NOW()` / `TODAY()`** are pinned: [`timestamp_ms`](Self::timestamp_ms)
87/// (a UTC instant) is converted to a local spreadsheet serial against the
88/// **vendored** [`timezone`](Self::timezone) (`chrono-tz`, not the host tz
89/// database), and that serial is passed to core's
90/// `evaluate_with_resolver_at`. The conversion is the determinism envelope:
91/// same instant + same timezone + same truecalc version ⇒ same serial.
92/// - **`RAND()` / `RANDBETWEEN()` / `RANDARRAY()`** carry a
93/// [`rng_seed`](Self::rng_seed) and a per-cell key helper ([`Self::rng_key`])
94/// implementing the ADR's `prf(seed, sheet_index, row, col, draw_index)`
95/// scheme. **Caveat:** core's RNG functions presently read the system clock
96/// directly and take no per-cell key (`crates/core/.../math/rand`), so the
97/// workbook layer cannot yet inject this seed into them — full PRF-keyed RNG
98/// determinism requires a core change and is tracked for P4. `rng_seed` is
99/// carried now so the API is stable; recalc therefore guarantees determinism
100/// for non-RNG workbooks (which is every P3.6 fixture).
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct RecalcContext {
103 /// The evaluation instant, in milliseconds since the Unix epoch (UTC).
104 /// `NOW()`/`TODAY()` derive from this.
105 timestamp_ms: i64,
106 /// The IANA timezone the instant is rendered into a local serial against,
107 /// from the vendored `chrono-tz` snapshot.
108 timezone: Tz,
109 /// Keys the deterministic per-cell RNG draws (ADR `prf(...)`); see the
110 /// type-level caveat about core support.
111 rng_seed: u64,
112}
113
114impl RecalcContext {
115 /// Builds a context from a UTC instant (Unix milliseconds), an IANA
116 /// timezone id (e.g. `"Etc/GMT"`, `"America/New_York"`), and an RNG seed.
117 ///
118 /// Returns `None` if `tz` is not a known IANA id in the vendored database.
119 pub fn new(timestamp_ms: i64, tz: &str, rng_seed: u64) -> Option<Self> {
120 let timezone: Tz = tz.parse().ok()?;
121 Some(Self {
122 timestamp_ms,
123 timezone,
124 rng_seed,
125 })
126 }
127
128 /// The UTC instant this context pins volatile time to (Unix milliseconds).
129 pub fn timestamp_ms(&self) -> i64 {
130 self.timestamp_ms
131 }
132
133 /// The vendored IANA timezone the instant is localized against.
134 pub fn timezone(&self) -> Tz {
135 self.timezone
136 }
137
138 /// The RNG seed keying deterministic per-cell draws.
139 pub fn rng_seed(&self) -> u64 {
140 self.rng_seed
141 }
142
143 /// The local spreadsheet serial datetime this context pins `NOW()`/`TODAY()`
144 /// to: the UTC `timestamp_ms` rendered into `timezone`, expressed as days
145 /// since the 1899-12-30 epoch (integer part) plus time-of-day (fraction) —
146 /// the `now_serial` core's `evaluate_at` family consumes.
147 ///
148 /// Returns `None` only if the instant is unrepresentable (e.g. out of
149 /// `chrono`'s range), which cannot happen for any realistic timestamp.
150 pub fn now_serial(&self) -> Option<f64> {
151 let utc = Utc.timestamp_millis_opt(self.timestamp_ms).single()?;
152 let local = utc.with_timezone(&self.timezone).naive_local();
153 let epoch = NaiveDate::from_ymd_opt(1899, 12, 30)?;
154 let days = local.date().signed_duration_since(epoch).num_days() as f64;
155 let secs = local.time().num_seconds_from_midnight() as f64;
156 Some(days + secs / 86_400.0)
157 }
158
159 /// The pinned "now" as an absolute UTC instant in nanoseconds, for the
160 /// zone-aware `TZNOW`. Derived from the same `timestamp_ms` as
161 /// [`now_serial`](Self::now_serial), so `NOW()` and `TZNOW()` share one
162 /// deterministic clock.
163 pub fn now_utc_nanos(&self) -> Option<i64> {
164 self.timestamp_ms.checked_mul(1_000_000)
165 }
166
167 /// The ADR's per-draw RNG key `prf(rng_seed, sheet_index, row, col,
168 /// draw_index)`, a deterministic, order-independent mixing of the cell
169 /// identity into the seed.
170 ///
171 /// Exposed (and unit-tested) so the keying scheme is fixed and ready for
172 /// the core integration that will consume it; see the type-level caveat.
173 pub fn rng_key(&self, sheet_index: u32, row: u32, col: u32, draw_index: u32) -> u64 {
174 // SplitMix64-style finalizer chained over the identity tuple — pure,
175 // order-independent, and identical across surfaces.
176 let mut h = self.rng_seed;
177 for part in [
178 sheet_index as u64,
179 row as u64,
180 col as u64,
181 draw_index as u64,
182 ] {
183 h = mix64(h ^ mix64(part));
184 }
185 h
186 }
187}
188
189/// One cell whose evaluated value a recalc changed.
190///
191/// Returned (in deterministic order) by [`Workbook::recalc`] and
192/// [`Workbook::recalc_incremental`]: the "change events" of v1, delivered as a
193/// value rather than via a callback (value-object ADR). Ordering is pinned —
194/// by sheet **tab index**, then row, then column (scope ADR Decision 3) — so
195/// the change list is reproducible.
196#[derive(Debug, Clone, PartialEq)]
197pub struct Change {
198 /// The sheet's name (its authored casing).
199 pub sheet: String,
200 /// The recomputed cell's address.
201 pub addr: Address,
202 /// The cell's value before this recalc (the stored result).
203 pub old: Value,
204 /// The cell's value after this recalc.
205 pub new: Value,
206}
207
208/// The dirty set an incremental recalc will recompute, paired with the queue of
209/// cells whose dependents have not been walked yet.
210///
211/// Issues #926 and #930 were one defect at two sites: a seeding stage inserted
212/// a cell into the dirty set *after* the closure walk had finished, so the cell
213/// recomputed but nothing downstream of it did — a silently stale value under a
214/// method that promises `incremental ≡ full`. Fixing each site by moving it
215/// above the walk left the next seeding stage free to reintroduce the bug, so
216/// the ordering is enforced by the type instead: [`insert`](Self::insert) is the
217/// only way into the dirty set and it always queues the cell, and
218/// [`close_over_dependents`](Self::close_over_dependents) drains that queue. A
219/// stage that dirties a cell without dirtying its dependents is not expressible.
220///
221/// Stays a public (if `#[doc(hidden)]`) re-export rather than `pub(crate)`
222/// (issue #946): `crates/workbook/tests/authored_cell_index_tests.rs`
223/// constructs one directly via [`new`](Self::new) to drive
224/// [`Workbook::seed_spill_sensitive_built_index`], and that test file — an
225/// integration test, so it compiles against the crate's public API only,
226/// regardless of living in the same package — has no other way to reach that
227/// `#[doc(hidden)] pub fn` from outside the crate boundary.
228#[doc(hidden)]
229#[derive(Debug, Default)]
230pub struct DirtyFrontier {
231 dirty: BTreeSet<CellRef>,
232 queue: VecDeque<CellRef>,
233}
234
235impl DirtyFrontier {
236 /// An empty frontier.
237 pub fn new() -> Self {
238 Self::default()
239 }
240
241 /// Marks `cell` dirty and queues it for the dependents walk.
242 ///
243 /// Every call site in this file discards "was this newly dirty" (issue
244 /// #946): over-seeding is safe by construction (a re-evaluated cell whose
245 /// value is unchanged emits no change event, per the module-level comment
246 /// on the spill-occupancy seeding stage above), so no caller has a
247 /// correctness reason to branch on it, and a caller that does want to
248 /// know can check `self.dirty` before calling.
249 fn insert(&mut self, cell: CellRef) {
250 if self.dirty.insert(cell.clone()) {
251 self.queue.push_back(cell);
252 }
253 }
254
255 /// Walks `direct_dependents_of` out of every queued cell until the dirty set
256 /// is closed under it — the transitive closure over every seeded cell,
257 /// whichever stage seeded it.
258 fn close_over_dependents(&mut self, graph: &DependencyGraph) {
259 while let Some(cell) = self.queue.pop_front() {
260 for dep in graph.direct_dependents_of(&cell) {
261 self.insert(dep);
262 }
263 }
264 }
265
266 /// How many cells are dirty.
267 fn len(&self) -> usize {
268 self.dirty.len()
269 }
270
271 /// The dirty cells, for handing to `recompute`.
272 fn cells(&self) -> &BTreeSet<CellRef> {
273 &self.dirty
274 }
275}
276
277impl Workbook {
278 /// Recomputes **every** formula cell in dependency order against `ctx`,
279 /// writing each new result back into the grid and returning the ordered
280 /// list of cells whose value changed.
281 ///
282 /// Formula cells are evaluated in topological order (precedents first), so
283 /// each reads its inputs already current. Cells on a dependency cycle —
284 /// and any cell that cannot be ordered because it (transitively) reads one
285 /// — take the circular-dependency error ([`CIRCULAR_ERROR`]); recalc always
286 /// terminates. Volatile functions are pinned by `ctx` (scope ADR
287 /// Decision 3).
288 ///
289 /// Changes are returned sorted by (sheet tab index, row, column).
290 pub fn recalc(&mut self, ctx: &RecalcContext) -> Vec<Change> {
291 let cached = self.dependency_graph_cached();
292 // Evaluate every formula cell; ordering and cycle handling are shared
293 // with the incremental path.
294 let to_eval: BTreeSet<CellRef> = cached.graph.formula_cells().cloned().collect();
295 self.recompute(&cached, ctx, to_eval)
296 }
297
298 /// The dependency graph and evaluation order for the workbook **as it is
299 /// now**, from the cache when it is warm and freshly built otherwise.
300 ///
301 /// `build` plus `evaluation_order` is the largest fixed cost of a
302 /// recalculation on a large workbook, and it used to be paid in full by
303 /// every call on both the full and the incremental path — however small the
304 /// edit, and even when nothing had changed at all. The invariant that makes
305 /// reusing it sound (a warm entry equals a build against the current
306 /// workbook) is maintained by the mutation API; see the `graph_cache`
307 /// module docs for exactly which mutations invalidate and which are proven
308 /// not to.
309 fn dependency_graph_cached(&mut self) -> Arc<CachedGraph> {
310 if let Some(entry) = self.cached_graph_entry() {
311 return entry;
312 }
313 let graph = DependencyGraph::build(self);
314 // Derived from the graph, so it is cached with it rather than beside
315 // it: it cannot go stale independently, and `recompute` runs several
316 // times per incremental recalc (the spill widen loop) while ordering
317 // the same graph every time.
318 let (order, cycle) = graph.evaluation_order();
319 // Likewise derived from the graph and cached alongside it: a formula
320 // cell's volatility cannot change without the formula text changing,
321 // which already invalidates this cache — so this is the one place
322 // `is_volatile` needs to run, once per formula cell per graph build,
323 // instead of once per formula cell per incremental recalc (issue
324 // #983).
325 let sheets = SheetIndex::build(self);
326 let volatile: BTreeSet<CellRef> = graph
327 .formula_cells()
328 .filter(|cell| self.is_volatile(&sheets, cell))
329 .cloned()
330 .collect();
331 let entry = Arc::new(CachedGraph {
332 graph,
333 order,
334 cycle,
335 volatile,
336 });
337 self.store_cached_graph(entry.clone());
338 entry
339 }
340
341 /// The spill-anchor-rectangle map for the workbook **as it is now**, from
342 /// the cache when it is warm and freshly built (and stored) otherwise.
343 ///
344 /// A separate cache from [`dependency_graph_cached`](Self::dependency_graph_cached),
345 /// invalidated on a genuinely separate schedule — see the
346 /// `spill_anchor_cache` module docs for why recalc's own value write-back
347 /// (which keeps the graph cache warm on purpose) must invalidate this one.
348 fn anchor_rectangles_cached(&mut self) -> Arc<BTreeMap<CellRef, SpillRect>> {
349 if let Some(entry) = self.cached_anchor_entry() {
350 return entry;
351 }
352 let entry = Arc::new(self.anchor_rectangles());
353 self.store_cached_anchors(entry.clone());
354 entry
355 }
356
357 /// [`anchor_rectangles_cached`](Self::anchor_rectangles_cached) for a
358 /// caller that only holds `&self` — returns the warm entry if the cache
359 /// happens to be warm, otherwise computes a fresh map **without storing
360 /// it** (an uncached `&self` cannot populate the cache). Never worse than
361 /// the pre-cache behavior for such a caller; the hot recalc path always
362 /// pre-warms through `anchor_rectangles_cached` first; see the call site in
363 /// [`recalc_incremental_measured`](Self::recalc_incremental_measured).
364 fn anchor_rectangles_ref(&self) -> Arc<BTreeMap<CellRef, SpillRect>> {
365 self.cached_anchor_entry()
366 .unwrap_or_else(|| Arc::new(self.anchor_rectangles()))
367 }
368
369 /// Recomputes only the formula cells affected by an edit and returns the
370 /// ordered changes.
371 ///
372 /// `edited` lists the cells a mutation touched (the cell written, or — for
373 /// a named-range retarget — the name's old and new target cells; callers
374 /// pass whatever changed). The recalc closure is the transitive
375 /// [`direct_dependents`](DependencyGraph::direct_dependents_of) of those
376 /// cells, **plus** every volatile formula cell (always dirty, scope ADR
377 /// Decision 3). Everything outside the closure keeps its stored result.
378 ///
379 /// The result is identical to the subset of [`recalc`](Self::recalc)'s
380 /// output for the same edits — the `incremental ≡ full` guarantee.
381 pub fn recalc_incremental(
382 &mut self,
383 ctx: &RecalcContext,
384 edited: &[(String, Address)],
385 ) -> Vec<Change> {
386 self.recalc_incremental_measured(ctx, edited).0
387 }
388
389 /// [`recalc_incremental`](Self::recalc_incremental), plus **how many cells
390 /// the dirty closure ended up holding**.
391 ///
392 /// Instrumentation, not a feature: "how narrow is the dirty set?" is the
393 /// exact-count metric behind incremental recalc, and wall-clock is too
394 /// machine-dependent to pin in a test. The count comes out of the same
395 /// frontier the recompute consumed, so it cannot drift from what actually
396 /// ran. Hidden from the docs because callers want
397 /// [`recalc_incremental`](Self::recalc_incremental).
398 #[doc(hidden)]
399 pub fn recalc_incremental_measured(
400 &mut self,
401 ctx: &RecalcContext,
402 edited: &[(String, Address)],
403 ) -> (Vec<Change>, usize) {
404 let cached = self.dependency_graph_cached();
405 let graph = &cached.graph;
406 let folder = CaseMapperBorrowed::new();
407 // Every sheet's tab index, folded once, for the whole incremental pass
408 // (issue #952). The sheet list cannot change while a recalc runs, so
409 // one index serves the volatile sweep, the spill seeding, the snapshot
410 // and the final diff — each of which used to re-scan and re-fold the
411 // whole sheet list once per formula cell.
412 let sheets = SheetIndex::build(self);
413
414 // One seeding phase — every source below feeds the same
415 // [`DirtyFrontier`] — followed by one closure walk. Every seeded cell
416 // therefore propagates to its own dependents, whichever stage seeded it
417 // (issues #926, #930).
418 let mut frontier = DirtyFrontier::new();
419
420 // (a) The edited cells and their dependents.
421 let edited_refs: Vec<CellRef> = edited
422 .iter()
423 .map(|(sheet, addr)| CellRef {
424 sheet: simple_fold(&folder, sheet),
425 addr: *addr,
426 })
427 .collect();
428 for seed in &edited_refs {
429 // The edited cell itself recomputes only if it is a formula; its
430 // dependents always do.
431 if graph.is_formula(seed) {
432 frontier.insert(seed.clone());
433 }
434 for dep in graph.direct_dependents_of(seed) {
435 frontier.insert(dep);
436 }
437 }
438
439 // (b) Volatile cells are always dirty (scope ADR Decision 3). The set
440 // is cached on the graph at build time (issue #983) — no
441 // re-derivation here.
442 for cell in &cached.volatile {
443 frontier.insert(cell.clone());
444 }
445
446 // Spill-occupancy seeding (issue #591). A cell's spill footprint or
447 // blocked status can change without the dependency graph carrying an
448 // edge that would dirty the cells depending on that change, because a
449 // spilled cell is not a formula node (P3.2) and a *blocked* anchor
450 // stores an error rather than an array that reads its blocker. Two
451 // concrete violations of `incremental ≡ full` (P3.3) follow:
452 //
453 // - **Shrink / replace-with-scalar.** Setting a former array anchor to
454 // a scalar vacates its old footprint, but `set` has already discarded
455 // the prior array, so the widen loop's `before = anchor_rectangles()`
456 // no longer sees the old rectangle and never dirties the readers of
457 // the vacated cells (e.g. `D1 = =B1+1` after `A1` stops spilling onto
458 // `B1`).
459 // - **Unblock.** Clearing or overwriting the cell that blocks a spill
460 // must let the anchor re-expand, but a blocked anchor has no edge to
461 // its blocker, so clearing the blocker never re-dirties the anchor.
462 //
463 // Seeding the dirty set with every spill-occupancy-sensitive cell makes
464 // the closure independent of which edit triggered the recalc, so the
465 // result matches a full recalc despite the lost pre-edit footprint.
466 // Over-seeding is safe: a re-evaluated cell whose value is unchanged
467 // emits no change event (`diff_against_snapshot`), so `incremental ≡
468 // full` is preserved while the minimal-closure guarantee still holds for
469 // ordinary (non-spill) edits, which seed nothing here.
470 // Pre-warm the anchor-rectangle cache under `&mut self`: everything
471 // downstream of here (`seed_spill_sensitive` and the widen loop below)
472 // only holds `&self` or reads through `anchor_rectangles_ref`, so this
473 // is the one place in the hot path that can populate a cold cache
474 // rather than merely check it.
475 self.anchor_rectangles_cached();
476 self.seed_spill_sensitive(&sheets, graph, &mut frontier);
477
478 // The single transitive closure over everything seeded above.
479 frontier.close_over_dependents(graph);
480
481 // A cell that reads a *spilled* cell has no dependency-graph edge to its
482 // spilling anchor (a spilled cell is not a formula node, P3.2), so the
483 // closure above can miss a spilled-cell reader when an anchor's spill
484 // footprint changes. We widen the dirty set to those readers and re-run
485 // until it stabilizes, so an incremental recalc reproduces the full one
486 // (`incremental ≡ full`, P3.3) even across spills (§5).
487 //
488 // To return change events with correct *pre-operation* `old` values
489 // despite the multiple internal recomputes, accumulate the pre-image
490 // of every cell this loop actually writes, lazily, from `recompute`'s
491 // own returned change list — rather than snapshotting every formula
492 // cell up front. `apply_changes` (called at the end of `recompute`) is
493 // the only code path that writes to the grid, and every write it
494 // makes is recorded in the `Change` it returns, carrying the
495 // pre-write value as `old` (read from the grid immediately before
496 // that write). So folding each pass's returned changes into `pre`
497 // first-wins (`entry(..).or_insert(old)`) records exactly the
498 // pre-recalc value of every cell this call ever touches, the first
499 // time it changes — cheaper than an upfront O(formula cells)
500 // snapshot, and provably equivalent to it: see the widen-loop rewind
501 // group in `recalc_differential_tests.rs`. The loop is bounded by the
502 // formula-cell count (each pass strictly grows the dirty set or
503 // stops).
504 //
505 // The widened readers go through the same frontier and are closed over
506 // too, so this stage cannot dirty a cell without dirtying what reads it
507 // either.
508 let mut pre: BTreeMap<CellRef, Value> = BTreeMap::new();
509 let max_widen = graph.formula_cells().count().saturating_add(2).max(1);
510 for pass in 0..max_widen {
511 if pass > 0 {
512 // Rewind to the pre-recalc grid before re-running with the
513 // widened set. Each attempt is then **one** `recompute` from
514 // the same starting grid over a larger closure — structurally
515 // what a full recalc is — so the result is a function of the
516 // final closure and not of how many attempts it took to find
517 // it.
518 //
519 // Without the rewind, attempt two continues from attempt one's
520 // output, which is one extra evaluation of every dirty cell. A
521 // cell that reads inside its own spill footprint has no fixed
522 // point to settle into — its footprint flips each time it is
523 // evaluated — so that extra evaluation lands it on the opposite
524 // phase from the full recalc, and `incremental ≡ full` fails on
525 // a workbook nothing else about the edit distinguishes. It also
526 // made the seeding's *breadth* load-bearing for byte-identity:
527 // a wide dirty set hid the second attempt by having already
528 // dirtied whatever the widening would add.
529 //
530 // `pre` only grows with cells `recompute` has actually
531 // written by this point (fact 0.1/0.2 above), so rewinding to
532 // it restores every one of those writes; its own returned
533 // change list is discarded rather than folded in, since a
534 // rewind's `old` is a *post-pass* value, not the true
535 // pre-recalc one `pre` already holds correctly.
536 self.apply_changes(&sheets, pre.clone());
537 }
538 let before = self.anchor_rectangles_cached();
539 let changes = self.recompute(&cached, ctx, frontier.cells().clone());
540 for change in changes {
541 let cell = CellRef {
542 sheet: simple_fold(&folder, &change.sheet),
543 addr: change.addr,
544 };
545 pre.entry(cell).or_insert(change.old);
546 }
547 let after = self.anchor_rectangles_cached();
548
549 let was = frontier.len();
550 for (sheet, addr) in changed_rectangle_cells(&before, &after) {
551 let spilled_ref = CellRef { sheet, addr };
552 for dep in graph.direct_dependents_of(&spilled_ref) {
553 frontier.insert(dep);
554 }
555 }
556 // Defensive (issue #946): every cell `changed_rectangle_cells`
557 // can insert above is, by construction, a reader of a *spilled*
558 // (never-authored) cell — and `seed_spill_sensitive`'s rules 3/4
559 // already mark *every* such reader spill-sensitive up front,
560 // whatever seeded it, so this closure has not been observed to
561 // add anything the seeding-phase closure above did not already
562 // reach: temporarily deleting just this line left the full local
563 // test suite green (issue #946 additionally reports a
564 // 24,000-workbook differential corpus staying green under the
565 // same change). Kept anyway as a structural invariant — the
566 // insert two lines up must not silently violate `incremental ≡
567 // full` the day `seed_spill_sensitive`'s rules are ever narrowed
568 // to be less conservative.
569 frontier.close_over_dependents(graph);
570 if frontier.len() == was {
571 break;
572 }
573 }
574 let closure = frontier.len();
575 self.record_pre_image_count(pre.len());
576 (self.diff_against_snapshot(&sheets, pre), closure)
577 }
578
579 /// Explains one cell's value against the **currently stored grid** (issue
580 /// #743): evaluates `addr`'s formula once through `hook`, resolving every
581 /// precedent read to its **stored** value (the same grid-backed
582 /// [`Resolver`] semantics `recalc` uses), and returns the value — provably
583 /// the same value `recalc`/`recalc_incremental` would write for this cell,
584 /// provided the grid is already current for its precedents.
585 ///
586 /// This is a point-in-time explain, not a recalc: unlike
587 /// [`Workbook::recalc`], `trace_cell` does **not** recompute anything
588 /// transitively — a precedent's value is whatever is already on the grid
589 /// (or, for a cell inside another anchor's placed spill, the
590 /// reconstructed spilled element — schema spec §5). If the grid is stale
591 /// relative to unapplied edits, `trace_cell` faithfully explains the
592 /// *stale* value; call `recalc` or `recalc_incremental` first if the
593 /// caller needs a fresh grid.
594 ///
595 /// Two pieces of `recalc`'s behavior can't be reproduced from the target
596 /// cell in isolation, so `trace_cell` matches them explicitly rather than
597 /// diverging (an on-demand, single-cell call — a user clicking a cell —
598 /// can afford this; see the two call sites below):
599 ///
600 /// - **Spill occupancy** (schema spec §5): an array result is only stored
601 /// if its target rectangle is free on the current grid; otherwise
602 /// `recalc` stores [`BLOCKED_SPILL_ERROR`] instead, exactly like
603 /// [`Workbook::place_spill`] applies for a real recompute.
604 /// - **Dependency cycles**: `recalc` never evaluates a cycle member's
605 /// formula at all — it short-circuits straight to
606 /// [`CIRCULAR_ERROR`] (see [`DependencyGraph::cycle_cells`] and
607 /// `recompute`). Evaluating the formula anyway would diverge whenever it
608 /// *catches* the error (e.g. `IFERROR`), since its precedents' stored
609 /// values already carry the propagated error but `recalc` never gave the
610 /// formula the chance to run.
611 ///
612 /// `addr` need not be a formula cell: a literal (or empty, or spilled
613 /// non-anchor) cell has no expression to trace, so this returns its
614 /// resolved value directly without invoking `hook` — `hook` observes no
615 /// events in that case, by design (there is nothing to walk). Passing a
616 /// hook is optional in the sense that evaluating with `hook = None`'s
617 /// counterpart, [`Engine::evaluate_with_resolver_at_keyed`], produces this
618 /// same value: `trace_cell` adds observation, it does not change what gets
619 /// computed.
620 pub fn trace_cell(
621 &self,
622 sheet: &str,
623 addr: Address,
624 ctx: &RecalcContext,
625 hook: &mut dyn EvalHook,
626 ) -> Value {
627 let folder = CaseMapperBorrowed::new();
628 let own_sheet = simple_fold(&folder, sheet);
629 let cell_ref = CellRef {
630 sheet: own_sheet.clone(),
631 addr,
632 };
633
634 // A cycle member never gets its formula evaluated by `recalc` — it is
635 // skipped in every pass of `recompute` and then unconditionally
636 // assigned `CIRCULAR_ERROR`, regardless of what the formula itself
637 // might do with its (already error-tainted) precedents. Match that
638 // before evaluating anything. Building the graph is an on-demand,
639 // single-cell, interactive call (a user clicking a cell), so
640 // correctness beats avoiding the graph walk here.
641 // Reads the cache when it is warm — a warm entry equals a build
642 // against the current workbook — but cannot populate it from `&self`,
643 // so a cold explain still builds. `cycle_cells` is recomputed either
644 // way: the cached `cycle` is the evaluation pass's set, which is the
645 // same set, but reusing it here would couple `trace_cell` to
646 // `evaluation_order`'s contract for no measurable gain on a
647 // single-cell call.
648 let cached = self.cached_graph_entry();
649 let owned;
650 let graph: &DependencyGraph = match &cached {
651 Some(entry) => &entry.graph,
652 None => {
653 owned = DependencyGraph::build(self);
654 &owned
655 }
656 };
657 if graph.cycle_cells().contains(&cell_ref) {
658 return Value::Error(CIRCULAR_ERROR.to_owned());
659 }
660
661 // No per-pass recompute state: every precedent read falls straight
662 // through to the stored grid (see `GridResolver::cell_value`'s
663 // fallback chain), which is exactly "explain given the current grid".
664 let empty_values: BTreeMap<CellRef, Value> = BTreeMap::new();
665 let empty_spills: BTreeMap<CellRef, SpillRect> = BTreeMap::new();
666 let empty_cells: BTreeSet<CellRef> = BTreeSet::new();
667 // Nothing is being recomputed, so every anchor on the stored grid is
668 // authoritative and the index excludes none of them.
669 let sheets = SheetIndex::build(self);
670 let grid_spills = GridSpillIndex::build(self, &empty_cells);
671 let mut resolver = GridResolver {
672 workbook: self,
673 own_sheet: &own_sheet,
674 sheets: &sheets,
675 new_values: &empty_values,
676 spills: &empty_spills,
677 prev_values: &empty_values,
678 prev_spills: &empty_spills,
679 cycle: &empty_cells,
680 grid_spills: &grid_spills,
681 current_cell: Some((&own_sheet, addr)),
682 scratch_key: fresh_scratch_key(),
683 };
684
685 let Some(formula) = self.cell_at(&sheets, &cell_ref).and_then(Cell::formula) else {
686 // Not a formula: nothing to trace. Resolve the cell's own value
687 // through the same fallback chain a precedent read would use, so
688 // e.g. a spilled (non-anchor) cell still resolves correctly.
689 return core_to_workbook(resolver.cell_value(&own_sheet, addr));
690 };
691 let formula = formula.to_owned();
692
693 let engine = match self.engine() {
694 EngineFlavor::Sheets => Engine::sheets(),
695 EngineFlavor::Excel => Engine::excel(),
696 };
697 let sheet_index = sheets.index_of_folded(&own_sheet).unwrap_or(0) as u32;
698 let rng_cell = Some((ctx.rng_seed(), sheet_index, addr.row, addr.column));
699
700 let core = engine.evaluate_with_resolver_at_keyed_hooked(
701 &formula,
702 &mut resolver,
703 ctx.now_serial(),
704 ctx.now_utc_nanos(),
705 rng_cell,
706 Some(hook),
707 );
708 let raw = core_to_workbook(core);
709
710 // Match `eval_formula_cell`'s spill placement: an array result is
711 // only stored if its target rectangle is free on the *current*
712 // stored grid (`place_spill`/`spill_blocked` read `self.cell_at`
713 // directly, so passing fresh, empty per-pass maps here reads exactly
714 // that — no real spill state is mutated).
715 self.place_spill(&sheets, &cell_ref, raw, &empty_values, &mut BTreeMap::new())
716 }
717
718 /// Shared evaluation core: evaluates `to_eval` (a set of formula cells) in
719 /// dependency order through a grid-backed resolver, applies cycle errors,
720 /// writes results back, and returns the changes in pinned order.
721 fn recompute(
722 &mut self,
723 cached: &CachedGraph,
724 ctx: &RecalcContext,
725 to_eval: BTreeSet<CellRef>,
726 ) -> Vec<Change> {
727 let now_serial = ctx.now_serial();
728 let now_utc_nanos = ctx.now_utc_nanos();
729 let rng_seed = ctx.rng_seed();
730
731 // Cells on a cycle short-circuit to the circular error; the rest are
732 // evaluated in topological order. Both come from one pass over the
733 // graph's formula-cell edges: when the graph is cyclic the order is a
734 // best-effort one over the acyclic remainder, so cells that do not
735 // touch the cycle still evaluate and cycle-tainted cells fall out as
736 // the error below.
737 // Taken from the cached entry rather than derived per call: ordering
738 // the formula-cell edges is the other half of the fixed per-recalc cost
739 // the graph cache exists to remove, and the incremental path runs this
740 // function once per widen pass.
741 let (order, cycle) = (&cached.order, &cached.cycle);
742
743 // Evaluate in order, resolving array spills as we go (plan item 3.5,
744 // schema spec §5). `new_values` holds each formula's result — a spilling
745 // anchor stores its full `array` (its serialized form, §6); a blocked
746 // anchor stores the Sheets blocked-spill error and no array. `spills`
747 // records the rectangle each *successfully placed* anchor occupies, so
748 // (a) a later anchor competing for one of its cells blocks, and (b) the
749 // resolver returns spilled values to cells that read them (spilled cells
750 // participate in recalc as precedents, §5).
751 //
752 // A cell that *reads* a spilled cell has no dependency-graph edge to the
753 // spilling anchor (a spilled cell is not a formula node, P3.2), so the
754 // topological order does not guarantee the anchor is evaluated first. We
755 // therefore iterate the pass to a fixpoint: each pass re-evaluates every
756 // `to_eval` cell against the prior pass's spills, so a reader that ran
757 // before its anchor in one pass sees the spilled value in the next. The
758 // grid is finite and spill geometry is monotone (an anchor's array
759 // depends only on its own non-spilled precedents), so this converges; we
760 // cap the iteration count at the node count as a hard safety bound.
761 //
762 // Seed the "previous pass" state from the stored grid so an *incremental*
763 // recalc — whose `to_eval` is only the dirty closure — still resolves a
764 // read of a cell spilled by an anchor that is **not** dirty this pass:
765 // that anchor's array is already on the grid, so its spill rectangle is
766 // available as a fallback even though it is never re-placed this recalc.
767 // A full recalc re-places every anchor, overriding the seed.
768 //
769 // Issue #985: `seed_spills_from_grid` below and `GridSpillIndex::build`
770 // further down are both full-grid scans for the identical `Value::Array`
771 // predicate `anchor_rectangles` already answers (the #984 cache), taken
772 // an instant apart from it — nothing mutates the grid between this check
773 // and either scan; the only mutation, `apply_changes`, runs after both,
774 // at the end of this function. So when that map is empty, both scans are
775 // provably empty too, and can be skipped outright. This cannot hide a
776 // spill *this* pass is about to create: a newly spilling cell is placed
777 // by `place_spill` inside the evaluation loop below, a mechanism neither
778 // scan is involved in — they only backstop *pre-existing*, not-being-
779 // recomputed spills (the #591 staleness rule and the incremental-seed
780 // fallback, respectively).
781 //
782 // Read-only (`cached_anchor_entry`), not the mutating
783 // `anchor_rectangles_cached`, and not `anchor_rectangles_ref` either:
784 // `recompute` is shared with the full-recalc path (`Workbook::recalc`),
785 // which must never populate this cache — see the `spill_anchor_cache`
786 // module docs and `spill_anchor_cache_tests.rs`'s
787 // `a_full_recalc_that_changes_a_spill_leaves_no_stale_entry_for_the_next_incremental_call`.
788 // Checking `cached_anchor_entry` directly (rather than probing through
789 // `anchor_rectangles_ref`, whose cache-miss fallback runs a fresh,
790 // uncached `anchor_rectangles()` scan of its own) means this short-
791 // circuit costs a cache-miss scan to even ask the question on a cold
792 // full recalc — the common case for `Workbook::recalc`, which never
793 // warms this cache — turning the two scans below into three instead of
794 // skipping either. Gating on "the cache is *already* warm and empty"
795 // gets the identical win whenever the cache happens to be warm (the
796 // incremental hot path, which always pre-warms it before calling here:
797 // an O(1) `Arc` clone plus an `is_empty()`), while a cold cache simply
798 // falls through to running both real scans unconditionally — exactly
799 // the pre-#985 cost, no wasted probe.
800 let no_spills = self
801 .cached_anchor_entry()
802 .is_some_and(|anchors| anchors.is_empty());
803 let (mut new_values, mut spills) = if no_spills {
804 (BTreeMap::new(), BTreeMap::new())
805 } else {
806 self.record_seed_spills_from_grid_call();
807 self.seed_spills_from_grid()
808 };
809
810 // Build the engine — and therefore the function registry — **once** for
811 // the whole recalc, not once per formula cell per pass (issue #886).
812 // `Engine` holds only a `Copy` flavor and a `Registry` of `fn`-pointer
813 // entries; every evaluation entry point takes `&self` and builds its
814 // mutable per-evaluation state (`Context`/`EvalCtx`) inside the call, so
815 // one instance is safely shared by every cell. Registry construction is
816 // ~99 µs against ~0.6 µs to parse and evaluate a cell, so building it
817 // per cell was ~97% of recalc time. The folded sheet-name → index map
818 // used for the per-cell RNG key is hoisted for the same reason: it was a
819 // `CaseMapperBorrowed::new()` plus a linear, allocating scan per cell.
820 let engine = match self.engine() {
821 EngineFlavor::Sheets => Engine::sheets(),
822 EngineFlavor::Excel => Engine::excel(),
823 };
824 let sheets = SheetIndex::build(self);
825 // The stored grid's spill anchors, indexed once for the whole recompute
826 // (issue #910). Both of its inputs are fixed here: the stored grid does
827 // not change until `apply_changes` runs after the last pass, and
828 // `to_eval` is fixed on entry. Without it, every read of an *empty*
829 // cell fell through to a scan of every authored cell on the sheet.
830 let grid_spills = if no_spills {
831 GridSpillIndex::default()
832 } else {
833 self.record_grid_spill_index_build_call();
834 GridSpillIndex::build(self, &to_eval)
835 };
836
837 let max_passes = order.len().saturating_add(2).max(1);
838 for _ in 0..max_passes {
839 let mut next_values: BTreeMap<CellRef, Value> = BTreeMap::new();
840 let mut next_spills: BTreeMap<CellRef, SpillRect> = BTreeMap::new();
841 for cell in order {
842 if cycle.contains(cell) {
843 continue; // handled in the cycle pass below
844 }
845 if !to_eval.contains(cell) {
846 continue;
847 }
848 // Evaluate against this pass's values/spills placed so far, with
849 // the *previous* pass's values/spills as a fallback. The
850 // fallback is what lets a reader that comes *before* its spill
851 // anchor in the order still see the spilled value: the anchor
852 // placed its spill in the previous pass, so the reader resolves
853 // it from `prev_*` even though `next_*` has not reached the
854 // anchor yet this pass.
855 let raw = self.eval_formula_cell(
856 cell,
857 &engine,
858 &sheets,
859 now_serial,
860 now_utc_nanos,
861 rng_seed,
862 &next_values,
863 &next_spills,
864 &new_values,
865 &spills,
866 cycle,
867 &grid_spills,
868 );
869 // Resolve array results into a placed spill or a blocked-spill
870 // error; a placed spill records its rectangle so later anchors
871 // and readers see it. Occupancy is judged against authored cells
872 // and the spills placed so far this pass.
873 let stored = self.place_spill(&sheets, cell, raw, &next_values, &mut next_spills);
874 next_values.insert(cell.clone(), stored);
875 }
876 let converged = next_values == new_values && next_spills == spills;
877 new_values = next_values;
878 spills = next_spills;
879 if converged {
880 break;
881 }
882 }
883 // Cycle cells (and downstream cells the order could not place) take the
884 // circular error.
885 for cell in &to_eval {
886 if !new_values.contains_key(cell) {
887 new_values.insert(cell.clone(), Value::Error(CIRCULAR_ERROR.to_owned()));
888 }
889 }
890
891 self.apply_changes(&sheets, new_values)
892 }
893
894 /// Evaluates a single formula cell through a resolver that reads the *new*
895 /// values computed so far this recalc, falling back to the stored grid for
896 /// everything else.
897 ///
898 /// `engine`, `sheets` and `grid_spills` are built once per recalc by
899 /// the caller and shared across every cell of the pass (issues #886, #904,
900 /// #910 and #952).
901 #[allow(clippy::too_many_arguments)]
902 fn eval_formula_cell(
903 &self,
904 cell: &CellRef,
905 engine: &Engine,
906 sheets: &SheetIndex,
907 now_serial: Option<f64>,
908 now_utc_nanos: Option<i64>,
909 rng_seed: u64,
910 new_values: &BTreeMap<CellRef, Value>,
911 spills: &BTreeMap<CellRef, SpillRect>,
912 prev_values: &BTreeMap<CellRef, Value>,
913 prev_spills: &BTreeMap<CellRef, SpillRect>,
914 cycle: &BTreeSet<CellRef>,
915 grid_spills: &GridSpillIndex,
916 ) -> Value {
917 let formula = match self.cell_at(sheets, cell).and_then(Cell::formula) {
918 Some(f) => f.to_owned(),
919 None => return Value::Empty,
920 };
921 let sheet_index = sheets.index_of_folded(&cell.sheet).unwrap_or(0) as u32;
922 let rng_cell = Some((rng_seed, sheet_index, cell.addr.row, cell.addr.column));
923 let mut resolver = GridResolver {
924 workbook: self,
925 own_sheet: &cell.sheet,
926 sheets,
927 new_values,
928 spills,
929 prev_values,
930 prev_spills,
931 cycle,
932 grid_spills,
933 current_cell: Some((&cell.sheet, cell.addr)),
934 scratch_key: fresh_scratch_key(),
935 };
936 let core = engine.evaluate_with_resolver_at_keyed(
937 &formula,
938 &mut resolver,
939 now_serial,
940 now_utc_nanos,
941 rng_cell,
942 );
943 core_to_workbook(core)
944 }
945
946 /// Turns a freshly evaluated formula result into its **stored** value,
947 /// applying Sheets spill semantics (plan item 3.5, schema spec §5).
948 ///
949 /// A non-array result is stored verbatim. An array result is a spill anchor:
950 /// it occupies the `m × n` rectangle anchored at `cell`. If every non-anchor
951 /// cell of that rectangle is free — not authored, and not already claimed by
952 /// an earlier anchor's placed spill (`placed`) — and the rectangle stays in
953 /// the sheet's address bounds, the spill is *placed*: its rectangle is
954 /// recorded in `placed` and the anchor stores the full array (its serialized
955 /// form, §6; the spilled cells are reconstructed, never serialized). If any
956 /// target is occupied or the rectangle is out of bounds, the spill is
957 /// **blocked**: the anchor takes the Sheets blocked-spill error
958 /// ([`BLOCKED_SPILL_ERROR`]) and stores no array (§5, §12).
959 fn place_spill(
960 &self,
961 sheets: &SheetIndex,
962 cell: &CellRef,
963 value: Value,
964 new_values: &BTreeMap<CellRef, Value>,
965 placed: &mut BTreeMap<CellRef, SpillRect>,
966 ) -> Value {
967 let Value::Array(ref rows) = value else {
968 return value; // scalar result: stored as-is
969 };
970 let nrows = rows.len();
971 let ncols = rows.first().map_or(0, Vec::len);
972 // `core_array_to_workbook` guarantees a rectangular, ≥ 2-cell array.
973 let Some(rect) = spill_rect(cell.addr, nrows, ncols) else {
974 // Out-of-bounds rectangle is blocked (§5).
975 return Value::Error(BLOCKED_SPILL_ERROR.to_owned());
976 };
977 if self.spill_blocked(sheets, cell, &rect, new_values, placed) {
978 return Value::Error(BLOCKED_SPILL_ERROR.to_owned());
979 }
980 placed.insert(cell.clone(), rect);
981 value
982 }
983
984 /// Whether the spill `rect` anchored at `cell` is blocked: any non-anchor
985 /// cell of the rectangle is authored on that sheet, is itself an evaluated
986 /// formula in this recalc (`new_values`), or already lies in an earlier
987 /// anchor's placed spill (`placed`). Schema spec §5.
988 fn spill_blocked(
989 &self,
990 sheets: &SheetIndex,
991 cell: &CellRef,
992 rect: &SpillRect,
993 new_values: &BTreeMap<CellRef, Value>,
994 placed: &BTreeMap<CellRef, SpillRect>,
995 ) -> bool {
996 for addr in rect.spilled_cells() {
997 let target = CellRef {
998 sheet: cell.sheet.clone(),
999 addr,
1000 };
1001 // An authored cell in the way (literal or formula).
1002 if self.cell_at(sheets, &target).is_some() {
1003 return true;
1004 }
1005 // A formula cell evaluated this recalc that is not itself authored
1006 // in the grid cannot exist, but a formula reader could be in
1007 // `new_values`; treat any computed cell here as occupied for safety.
1008 if new_values.contains_key(&target) {
1009 return true;
1010 }
1011 // A cell already claimed by an earlier anchor's spill.
1012 if placed
1013 .values()
1014 .any(|r| r.anchor != cell.addr && r.contains(addr))
1015 {
1016 return true;
1017 }
1018 }
1019 false
1020 }
1021
1022 /// Builds the spill state implied by the **stored** grid: every authored
1023 /// cell whose stored value is an `array` is a spill anchor occupying its
1024 /// reconstructed rectangle (schema spec §5). Returns the anchor → array map
1025 /// and the anchor → rectangle map, used to seed an incremental recalc so a
1026 /// read of a spilled cell whose anchor is not dirty this pass still resolves
1027 /// (the anchor placed the spill in a prior recalc). An out-of-bounds stored
1028 /// array — which a valid document never contains (`from_json` rejects it,
1029 /// validate.rs §5) — is skipped.
1030 fn seed_spills_from_grid(&self) -> (BTreeMap<CellRef, Value>, BTreeMap<CellRef, SpillRect>) {
1031 let folder = CaseMapperBorrowed::new();
1032 let mut values: BTreeMap<CellRef, Value> = BTreeMap::new();
1033 let mut spills: BTreeMap<CellRef, SpillRect> = BTreeMap::new();
1034 for sheet in self.sheets() {
1035 let folded = simple_fold(&folder, sheet.name());
1036 for (addr, cell) in sheet.iter() {
1037 let Value::Array(rows) = cell.value() else {
1038 continue;
1039 };
1040 let nrows = rows.len();
1041 let ncols = rows.first().map_or(0, Vec::len);
1042 if let Some(rect) = spill_rect(addr, nrows, ncols) {
1043 let key = CellRef {
1044 sheet: folded.clone(),
1045 addr,
1046 };
1047 values.insert(key.clone(), cell.value().clone());
1048 spills.insert(key, rect);
1049 }
1050 }
1051 }
1052 (values, spills)
1053 }
1054
1055 /// Writes the recomputed values back, emitting a [`Change`] for each cell
1056 /// whose value actually changed, in pinned (sheet index, row, column) order.
1057 fn apply_changes(
1058 &mut self,
1059 sheets: &SheetIndex,
1060 new_values: BTreeMap<CellRef, Value>,
1061 ) -> Vec<Change> {
1062 // Resolve folded sheet names to tab index + authored name through the
1063 // index built once for the recalc (issue #952); this used to be a
1064 // linear, folding scan of the sheet list per changed cell.
1065 let mut changes: Vec<(usize, Change)> = Vec::new();
1066 for (cell, new) in new_values {
1067 let Some(idx) = sheets.index_of_folded(&cell.sheet) else {
1068 continue; // sheet vanished (cannot happen mid-recalc)
1069 };
1070 let sheet_name = self.sheets()[idx].name().to_owned();
1071 let old = self.sheets()[idx]
1072 .get(cell.addr)
1073 .map(|c| c.value().clone())
1074 .unwrap_or(Value::Empty);
1075 if old == new {
1076 continue;
1077 }
1078 // Preserve the formula text; only the stored value updates.
1079 let formula = self.sheets()[idx]
1080 .get(cell.addr)
1081 .and_then(|c| c.formula())
1082 .map(str::to_owned);
1083 if let Some(formula) = formula {
1084 // Structure-preserving by construction: an existing formula
1085 // cell keeps its formula text and only its stored value moves,
1086 // so no node and no edge changes — see the `graph_cache`
1087 // module docs. The one way a stored value *can* reach the graph
1088 // is a declared table's header text, handled once after the
1089 // loop rather than per cell.
1090 self.sheets_mut_untracked()[idx]
1091 .set(cell.addr, Cell::with_formula(formula, new.clone()));
1092 // Structure-preserving for the graph cache (above) does not
1093 // mean structure-preserving for the spill-anchor cache: this
1094 // write is exactly how a spill is placed, resized, or
1095 // removed. See the `spill_anchor_cache` module docs for why
1096 // this is a genuinely separate invalidation condition from
1097 // the graph cache's.
1098 if matches!(&old, Value::Array(_)) || matches!(&new, Value::Array(_)) {
1099 self.invalidate_anchor_cache();
1100 }
1101 }
1102 changes.push((
1103 idx,
1104 Change {
1105 sheet: sheet_name,
1106 addr: cell.addr,
1107 old,
1108 new,
1109 },
1110 ));
1111 }
1112 // A structured reference resolves its column by matching the stored
1113 // text of a declared table's header row, so in a workbook that declares
1114 // tables a recomputed value *is* a graph input. Rather than test each
1115 // written cell against every table's header rectangle, drop the cache
1116 // whenever a table exists and anything changed: tables are rare, the
1117 // check is O(1), and being wrong here is a stale graph.
1118 if !changes.is_empty() && !self.tables().is_empty() {
1119 self.invalidate_graph_cache();
1120 }
1121 // Pin order: sheet tab index, then row, then column.
1122 changes.sort_by(|a, b| {
1123 a.0.cmp(&b.0)
1124 .then(a.1.addr.row.cmp(&b.1.addr.row))
1125 .then(a.1.addr.column.cmp(&b.1.addr.column))
1126 });
1127 changes.into_iter().map(|(_, c)| c).collect()
1128 }
1129
1130 /// Whether `cell`'s formula calls any volatile function (`NOW`, `TODAY`,
1131 /// `RAND`, `RANDBETWEEN`, `RANDARRAY` — core's `VOLATILE_FUNCTIONS`).
1132 /// A volatile cell is always dirty in incremental recalc.
1133 fn is_volatile(&self, sheets: &SheetIndex, cell: &CellRef) -> bool {
1134 let Some(formula) = self.cell_at(sheets, cell).and_then(Cell::formula) else {
1135 return false;
1136 };
1137 let upper = formula.to_ascii_uppercase();
1138 truecalc_core::Registry::VOLATILE_FUNCTIONS
1139 .iter()
1140 .any(|name| contains_call(&upper, name))
1141 }
1142
1143 /// Adds every spill-occupancy-sensitive formula cell to `frontier` (issue
1144 /// #591), so an incremental recalc reproduces a full recalc across any spill
1145 /// footprint or blocked-status change even though the dependency graph
1146 /// carries no edge for those transitions and `set` discarded the pre-edit
1147 /// footprint.
1148 ///
1149 /// A cell is seeded when it is, or reads something that can become or cease
1150 /// being, a spill:
1151 ///
1152 /// 1. **Every array anchor** (a formula cell whose stored value is an
1153 /// array) — re-placed so a footprint that should shrink or grow does so,
1154 /// and so a write into its region re-blocks it.
1155 /// 2. **Every blocked-spill anchor** (a formula cell whose stored value is
1156 /// the blocked-spill error) — re-attempted so clearing/overwriting its
1157 /// blocker lets it re-expand (the unblock case).
1158 /// 3. **Every reader of a non-authored single cell** — that precedent is
1159 /// empty or spilled today and may flip either way, e.g. `D1 = =B1+1`
1160 /// whose `B1` was spilled by a now-shrunk anchor (the vacated-reader
1161 /// case), or a reader of a cell a spill is about to grow onto.
1162 /// 4. **Every reader of a range that overlaps a current spill rectangle, or
1163 /// that holds any non-authored cell** — a range aggregation whose window
1164 /// includes spilled cells re-aggregates when that spill changes
1165 /// (grow/shrink/block); a non-authored cell in the window catches the
1166 /// case no other mechanism can see, a spill a *previous* recalc (not
1167 /// necessarily this edit) retired, since neither the dependency graph
1168 /// nor the widen loop has a way to reconstruct a footprint that is
1169 /// already gone by the time either runs (issue #949).
1170 /// 5. **Every reader of a *name* whose current target is one of those** —
1171 /// the name's target is put through rule 3 or rule 4, exactly as if the
1172 /// formula had referenced it directly.
1173 ///
1174 /// The blocked-spill error string equals [`BLOCKED_SPILL_ERROR`]; a cell
1175 /// merely *holding* that error that is not actually a former/blocked spill
1176 /// anchor is harmless to re-evaluate (it recomputes to the same value).
1177 ///
1178 /// Uses the workbook's cached authored-cell index (issue #991 fallback)
1179 /// rather than building one fresh on every call — see the
1180 /// `authored_cell_index_cache` module docs. The index is still built
1181 /// lazily, only if a range precedent is actually examined (issue #927
1182 /// follow-up), but once built it is persisted on the workbook so a later
1183 /// call with a warm, still-valid cache skips the `O(authored cells)`
1184 /// build entirely rather than repeating it. `&mut self` only to store a
1185 /// freshly built index; nothing here mutates the grid.
1186 fn seed_spill_sensitive(
1187 &mut self,
1188 sheets: &SheetIndex,
1189 graph: &DependencyGraph,
1190 frontier: &mut DirtyFrontier,
1191 ) {
1192 let mut authored = self.cached_authored_index_entry();
1193 let was_warm = authored.is_some();
1194 self.seed_spill_sensitive_body(sheets, graph, frontier, &mut authored);
1195 // Only store when this call actually built a fresh index: if the
1196 // cache started warm, `authored` is the same `Arc` already stored,
1197 // and re-storing it would bump `authored_index_builds()` for a call
1198 // that built nothing (the exact-count instrumentation this cache
1199 // exists to support).
1200 if !was_warm {
1201 if let Some(index) = authored {
1202 self.store_cached_authored_index(index);
1203 }
1204 }
1205 }
1206
1207 /// [`seed_spill_sensitive`](Self::seed_spill_sensitive), plus whether it
1208 /// built the authored-cell index.
1209 ///
1210 /// Instrumentation, not a feature: the index is built lazily now, on the
1211 /// first range precedent actually examined, rather than once unconditionally
1212 /// per seeding pass — a workbook with no range precedents anywhere must not
1213 /// pay for a full sheet sweep it never needed (issue #927 follow-up).
1214 /// Hidden from the docs because callers want
1215 /// [`seed_spill_sensitive`](Self::seed_spill_sensitive).
1216 ///
1217 /// Deliberately **uncached**, unlike the real recalc path above: this
1218 /// always builds a fresh local index rather than consulting or
1219 /// populating the workbook's persistent authored-index cache, so its
1220 /// return value keeps meaning exactly what its name says — "did *this*
1221 /// call need to build the index" — regardless of whether some other
1222 /// call already warmed the workbook's cache. Routing this through that
1223 /// cache would make the answer depend on cache warmth instead, silently
1224 /// changing what the existing tests in `authored_cell_index_tests.rs`
1225 /// assert.
1226 #[doc(hidden)]
1227 pub fn seed_spill_sensitive_built_index(
1228 &self,
1229 graph: &DependencyGraph,
1230 frontier: &mut DirtyFrontier,
1231 ) -> bool {
1232 self.seed_spill_sensitive_indexed(&SheetIndex::build(self), graph, frontier)
1233 }
1234
1235 /// [`seed_spill_sensitive_built_index`](Self::seed_spill_sensitive_built_index)
1236 /// against a sheet index the caller already built for this recalc.
1237 /// Always builds its own local index (see that function's doc comment for
1238 /// why) via [`seed_spill_sensitive_body`](Self::seed_spill_sensitive_body),
1239 /// the shared loop this and the cached hot path both drive. Split out so
1240 /// the recalc path folds the sheet list once for the whole pass rather
1241 /// than once per formula cell examined here (issue #952).
1242 fn seed_spill_sensitive_indexed(
1243 &self,
1244 sheets: &SheetIndex,
1245 graph: &DependencyGraph,
1246 frontier: &mut DirtyFrontier,
1247 ) -> bool {
1248 let mut authored: Option<Arc<AuthoredCellIndex>> = None;
1249 self.seed_spill_sensitive_body(sheets, graph, frontier, &mut authored);
1250 authored.is_some()
1251 }
1252
1253 /// The real body of spill-sensitive seeding (issue #591): walks every
1254 /// formula cell and inserts it into `frontier` per the five rules
1255 /// documented on [`seed_spill_sensitive`](Self::seed_spill_sensitive).
1256 /// Shared by the cached hot path and the uncached, test-instrumented
1257 /// path — `authored` is the caller's index slot, seeded from the
1258 /// persistent cache or from a fresh `None` depending on which caller this
1259 /// is; this body neither knows nor cares which, so the two callers cannot
1260 /// drift apart in the actual seeding logic.
1261 fn seed_spill_sensitive_body(
1262 &self,
1263 sheets: &SheetIndex,
1264 graph: &DependencyGraph,
1265 frontier: &mut DirtyFrontier,
1266 authored: &mut Option<Arc<AuthoredCellIndex>>,
1267 ) {
1268 let rects = self.anchor_rectangles_ref();
1269 for cell in graph.formula_cells() {
1270 // (1)/(2): the cell itself is (or held) a spill.
1271 let is_spill_cell = match self.cell_at(sheets, cell).map(Cell::value) {
1272 Some(Value::Array(_)) => true,
1273 Some(Value::Error(code)) | Some(Value::ErrorMsg(code, _)) => {
1274 code == BLOCKED_SPILL_ERROR
1275 }
1276 _ => false,
1277 };
1278 let mut seed = is_spill_cell;
1279 // (3)/(4)/(5): the cell reads a spill-sensitive precedent.
1280 if !seed {
1281 if let Some(precedents) = graph.precedents_of(cell) {
1282 seed = precedents.iter().any(|p| {
1283 self.precedent_is_spill_sensitive(sheets, p, graph, &rects, authored)
1284 });
1285 }
1286 }
1287 if seed {
1288 frontier.insert(cell.clone());
1289 }
1290 }
1291 }
1292
1293 /// Whether a single precedent reads a cell that is, or could become, a
1294 /// spilled cell (issue #591).
1295 ///
1296 /// `authored` builds the index on first use and reuses it after —
1297 /// `AuthoredCellIndex::build` only runs if a range is actually examined.
1298 fn precedent_is_spill_sensitive(
1299 &self,
1300 sheets: &SheetIndex,
1301 precedent: &Precedent,
1302 graph: &DependencyGraph,
1303 rects: &BTreeMap<CellRef, SpillRect>,
1304 authored: &mut Option<Arc<AuthoredCellIndex>>,
1305 ) -> bool {
1306 match precedent {
1307 Precedent::Cell(c) => self.cell_is_spill_sensitive(sheets, c),
1308 Precedent::Range(r) => self.range_is_spill_sensitive(r, rects, authored),
1309 // A name is an indirection, not a separate kind of reference: what
1310 // its reader actually reads is the name's current target, so put
1311 // that target through the same rule the reader would have got by
1312 // referencing it directly (issue #925).
1313 //
1314 // Treating every name as spill-sensitive instead made *any* formula
1315 // reading *any* defined name dirty on every incremental recalc,
1316 // whatever the name pointed at — which is most of a real model, and
1317 // it also masked dropped name edges from the value-asserting suites
1318 // (the reader was seeded whether or not the edge was walked).
1319 //
1320 // A name with no current target resolves to an error rather than to
1321 // cells; it has nothing that can spill, so it seeds nothing.
1322 Precedent::Name(name) => match graph.name_target_of(name) {
1323 Some(NameTarget::Cell(c)) => self.cell_is_spill_sensitive(sheets, &c),
1324 Some(NameTarget::Range(r)) => self.range_is_spill_sensitive(&r, rects, authored),
1325 None => false,
1326 },
1327 Precedent::Unresolved(_) => false,
1328 }
1329 }
1330
1331 /// Rule 3: a single-cell target that is not authored is empty or spilled
1332 /// today, and may flip either way (grow/shrink/block/unblock).
1333 fn cell_is_spill_sensitive(&self, sheets: &SheetIndex, c: &CellRef) -> bool {
1334 self.cell_at(sheets, c).is_none()
1335 }
1336
1337 /// Rule 4: a range precedent is spill-sensitive if it overlaps a current
1338 /// spill rectangle (a spill could grow/shrink/block within it) *or* if it
1339 /// contains any non-authored cell — which catches a cell a spill *used to*
1340 /// cover but no longer does (the lost pre-edit footprint of a
1341 /// shrink/collapse), since that cell is now empty.
1342 ///
1343 /// The narrower alternative — reasoning from the edit's own cells about
1344 /// which footprint could have vacated into `r` — is unsound: a spill
1345 /// footprint a *previous* recalc retired (not this edit) can leave a
1346 /// non-authored cell in `r` with no edited cell anywhere near it, and a
1347 /// completely correct `edited` list gives no way to find that cell from the
1348 /// post-edit grid alone (issue #949). Recovering the narrower
1349 /// rule needs the previous recalc's placed rectangles carried forward on
1350 /// the workbook, which this rule does not have.
1351 fn range_is_spill_sensitive(
1352 &self,
1353 r: &RangeRef,
1354 rects: &BTreeMap<CellRef, SpillRect>,
1355 authored: &mut Option<Arc<AuthoredCellIndex>>,
1356 ) -> bool {
1357 rects
1358 .iter()
1359 .any(|(anchor, rect)| anchor.sheet == r.sheet && rect_overlaps_range(rect, r))
1360 || authored
1361 .get_or_insert_with(|| Arc::new(AuthoredCellIndex::build(self)))
1362 .range_has_unauthored_cell(r)
1363 }
1364
1365 /// Every spill rectangle currently on the stored grid (anchor → rectangle),
1366 /// derived from authored cells whose stored value is an array (schema spec
1367 /// §5). Used to detect when an incremental recompute changed a spill
1368 /// footprint so the affected readers can be dirtied.
1369 fn anchor_rectangles(&self) -> BTreeMap<CellRef, SpillRect> {
1370 let folder = CaseMapperBorrowed::new();
1371 let mut rects = BTreeMap::new();
1372 for sheet in self.sheets() {
1373 let folded = simple_fold(&folder, sheet.name());
1374 for (addr, cell) in sheet.iter() {
1375 let Value::Array(rows) = cell.value() else {
1376 continue;
1377 };
1378 let nrows = rows.len();
1379 let ncols = rows.first().map_or(0, Vec::len);
1380 if let Some(rect) = spill_rect(addr, nrows, ncols) {
1381 rects.insert(
1382 CellRef {
1383 sheet: folded.clone(),
1384 addr,
1385 },
1386 rect,
1387 );
1388 }
1389 }
1390 }
1391 rects
1392 }
1393
1394 /// Emits the change list for an incremental recalc by diffing the final grid
1395 /// against the pre-operation `snapshot`: one [`Change`] per formula cell
1396 /// whose value differs, in the pinned (sheet tab index, row, column) order.
1397 fn diff_against_snapshot(
1398 &self,
1399 sheets: &SheetIndex,
1400 snapshot: BTreeMap<CellRef, Value>,
1401 ) -> Vec<Change> {
1402 let mut changes: Vec<(usize, Change)> = Vec::new();
1403 for (cell, old) in snapshot {
1404 let Some(idx) = sheets.index_of_folded(&cell.sheet) else {
1405 continue;
1406 };
1407 let new = self.sheets()[idx]
1408 .get(cell.addr)
1409 .map(|c| c.value().clone())
1410 .unwrap_or(Value::Empty);
1411 if old == new {
1412 continue;
1413 }
1414 changes.push((
1415 idx,
1416 Change {
1417 sheet: self.sheets()[idx].name().to_owned(),
1418 addr: cell.addr,
1419 old,
1420 new,
1421 },
1422 ));
1423 }
1424 changes.sort_by(|a, b| {
1425 a.0.cmp(&b.0)
1426 .then(a.1.addr.row.cmp(&b.1.addr.row))
1427 .then(a.1.addr.column.cmp(&b.1.addr.column))
1428 });
1429 changes.into_iter().map(|(_, c)| c).collect()
1430 }
1431
1432 /// The cell at a [`CellRef`] (folded sheet + address), or `None`.
1433 ///
1434 /// `sheets` is the caller's per-recalc [`SheetIndex`]. Resolving the sheet
1435 /// used to be a linear `position` scan that case-folded — and so allocated
1436 /// — every sheet name it passed, performed once per formula cell; on a
1437 /// 200-sheet workbook that scan was 90% of `recalc` (issue #952).
1438 fn cell_at(&self, sheets: &SheetIndex, cell: &CellRef) -> Option<&Cell> {
1439 let idx = sheets.index_of_folded(&cell.sheet)?;
1440 self.sheets()[idx].get(cell.addr)
1441 }
1442}
1443
1444/// A [`Resolver`] backed by the workbook grid, reading the values computed so
1445/// far this recalc before falling back to the stored grid.
1446struct GridResolver<'a> {
1447 workbook: &'a Workbook,
1448 own_sheet: &'a str,
1449 /// Every sheet's tab index, built once per recalc by the caller.
1450 /// Resolving a read's target sheet is a map probe against this (issues
1451 /// #904, #952); it used to be a linear scan of the sheet list that
1452 /// case-folded — and so allocated — every sheet name, **per element
1453 /// scanned**, to find a sheet that cannot change between the elements of
1454 /// one range.
1455 sheets: &'a SheetIndex,
1456 new_values: &'a BTreeMap<CellRef, Value>,
1457 /// Spills placed so far **this pass** (anchor → rectangle): a read of a cell
1458 /// inside one of these rectangles resolves to the spilled array element
1459 /// (schema spec §5 — spilled cells participate as precedents).
1460 spills: &'a BTreeMap<CellRef, SpillRect>,
1461 /// The **previous** pass's values, used as a fallback so a reader ordered
1462 /// before its spill anchor still sees the spilled value (the anchor placed
1463 /// it last pass). Empty on the first pass.
1464 prev_values: &'a BTreeMap<CellRef, Value>,
1465 /// The previous pass's spills (same fallback role as `prev_values`).
1466 prev_spills: &'a BTreeMap<CellRef, SpillRect>,
1467 cycle: &'a BTreeSet<CellRef>,
1468 /// The stored grid's spill anchors, indexed once per recalc, already
1469 /// excluding the anchors being recomputed (issue #591 — see
1470 /// [`GridSpillIndex::build`]). Backs the `grid_spilled_value` fallback,
1471 /// which used to re-derive this by scanning every authored cell on the
1472 /// sheet on every read of an empty cell (issue #910).
1473 grid_spills: &'a GridSpillIndex,
1474 /// The evaluating cell's own `(folded sheet name, address)` — set at the
1475 /// same call site that computes `rng_cell`'s `(sheet_index, row, col)`, so
1476 /// both stay in sync by construction. Used by `resolve_table_ref` to look
1477 /// up the single current-row cell and to infer a table from an
1478 /// unqualified `[@col]` reference's containment. Always `Some` at every
1479 /// current construction site (kept `Option` defensively, since a
1480 /// resolver constructed without a specific evaluating cell would have
1481 /// nothing to thread here).
1482 current_cell: Option<(&'a str, Address)>,
1483 /// A reusable `CellRef` for this resolver's map probes.
1484 ///
1485 /// `cell_value` probes three `CellRef`-keyed maps, and the owned sheet name
1486 /// those keys need used to be allocated fresh **per element scanned**
1487 /// (issue #904). Every element of one range shares a sheet name, so
1488 /// [`GridResolver::probe_key`] refills this key in place instead:
1489 /// `String::clear` keeps the buffer, so the `push_str` that follows reuses
1490 /// it. `RefCell` rather than `&mut self` because `resolve_range` and
1491 /// `resolve_table_ref` hold shared borrows of `self` across their
1492 /// `cell_value` calls.
1493 scratch_key: RefCell<CellRef>,
1494}
1495
1496/// An empty scratch key for a freshly built [`GridResolver`]. The address is a
1497/// placeholder: `probe_key` overwrites both fields before any probe reads them.
1498fn fresh_scratch_key() -> RefCell<CellRef> {
1499 RefCell::new(CellRef {
1500 sheet: String::new(),
1501 addr: Address::new(1, 1).expect("A1 is in bounds"),
1502 })
1503}
1504
1505impl GridResolver<'_> {
1506 /// The current value of a resolved cell: this recalc's fresh value if it
1507 /// was already computed, else the stored grid value, else empty. A cell on
1508 /// a cycle resolves to the circular error (so a cell that *reads* a cycle
1509 /// inherits the taint).
1510 fn cell_value(&self, sheet_folded: &str, addr: Address) -> CoreValue {
1511 {
1512 let key = self.probe_key(sheet_folded, addr);
1513 if self.cycle.contains(&key) {
1514 return CoreValue::Error(ErrorKind::Ref);
1515 }
1516 if let Some(v) = self.new_values.get(&key) {
1517 return workbook_to_core(v);
1518 }
1519 }
1520 if let Some(c) = self.sheet(sheet_folded).and_then(|s| s.get(addr)) {
1521 return workbook_to_core(c.value());
1522 }
1523 // Not authored and not freshly computed: it may be a spilled cell of an
1524 // anchor placed this pass — or, if the anchor is ordered *after* this
1525 // reader, of the previous pass (schema spec §5). Resolve through the
1526 // spill, preferring this pass's placement.
1527 if let Some(v) = self.spilled_value(sheet_folded, addr, self.spills, self.new_values) {
1528 return workbook_to_core(&v);
1529 }
1530 if let Some(v) = self.spilled_value(sheet_folded, addr, self.prev_spills, self.prev_values)
1531 {
1532 return workbook_to_core(&v);
1533 }
1534 // A cell whose value the previous pass computed but this pass has not
1535 // reached yet (a reader's plain-cell precedent ordered after it).
1536 if let Some(v) = self.prev_values.get(&self.probe_key(sheet_folded, addr)) {
1537 return workbook_to_core(v);
1538 }
1539 // Final fallback (matters for *incremental* recalc): the cell may be
1540 // spilled by an anchor that is not dirty this recalc, so it never enters
1541 // the per-pass maps. Its array is on the stored grid; reconstruct the
1542 // element directly (schema spec §5).
1543 if let Some(v) = self.grid_spilled_value(sheet_folded, addr) {
1544 return workbook_to_core(&v);
1545 }
1546 CoreValue::Empty
1547 }
1548
1549 /// The map-probe key for `(sheet_folded, addr)`, refilling the scratch
1550 /// [`CellRef`] in place rather than allocating a fresh owned sheet name per
1551 /// probe (issue #904). Consecutive probes of one range share a sheet name,
1552 /// so the `push_str` reuses the buffer `clear` left behind.
1553 ///
1554 /// The borrow must not be held across anything that could re-enter
1555 /// `cell_value`; every use below is scoped to a single probe.
1556 fn probe_key(&self, sheet_folded: &str, addr: Address) -> RefMut<'_, CellRef> {
1557 let mut key = self.scratch_key.borrow_mut();
1558 if key.sheet != sheet_folded {
1559 key.sheet.clear();
1560 key.sheet.push_str(sheet_folded);
1561 }
1562 key.addr = addr;
1563 key
1564 }
1565
1566 /// The sheet whose folded name is `sheet_folded`, via the recalc-wide index
1567 /// (issue #904): a map probe, with no case-folding and no allocation.
1568 fn sheet(&self, sheet_folded: &str) -> Option<&Worksheet> {
1569 let index = self.sheets.index_of_folded(sheet_folded)?;
1570 self.workbook.sheets().get(index)
1571 }
1572
1573 /// The value spilled to `addr` on `sheet_folded` per the **stored grid**:
1574 /// finds the anchor whose stored rectangle covers `addr` and reconstructs
1575 /// the element (schema spec §5). Used as the incremental-recalc fallback for
1576 /// spills whose anchor is not re-evaluated this pass.
1577 ///
1578 /// Only the sheet's *spill anchors* are examined, from the recalc-wide
1579 /// [`GridSpillIndex`] — which also applies the `#591` exclusion of anchors
1580 /// being recomputed. This used to scan every authored cell on the sheet, on
1581 /// every read of an empty cell, at one allocation per cell scanned (issue
1582 /// #910).
1583 fn grid_spilled_value(&self, sheet_folded: &str, addr: Address) -> Option<Value> {
1584 let anchors = self.grid_spills.anchors(sheet_folded);
1585 if anchors.is_empty() {
1586 return None;
1587 }
1588 let sheet = self.sheet(sheet_folded)?;
1589 for &(anchor_addr, rect) in anchors {
1590 if anchor_addr == addr {
1591 continue;
1592 }
1593 let Some((i, j)) = rect.offset_of(addr) else {
1594 continue;
1595 };
1596 let Some(Value::Array(rows)) = sheet.get(anchor_addr).map(Cell::value) else {
1597 continue; // unreachable: the index only holds array anchors
1598 };
1599 return rows.get(i).and_then(|r| r.get(j)).cloned();
1600 }
1601 None
1602 }
1603
1604 /// The value spilled to `addr` on `sheet_folded` per a given `spills` map
1605 /// and its backing `values`: the `[i][j]` element of the anchor's stored
1606 /// array (schema spec §5). `None` if `addr` is not a non-anchor cell of any
1607 /// spill in `spills`.
1608 fn spilled_value(
1609 &self,
1610 sheet_folded: &str,
1611 addr: Address,
1612 spills: &BTreeMap<CellRef, SpillRect>,
1613 values: &BTreeMap<CellRef, Value>,
1614 ) -> Option<Value> {
1615 for (anchor, rect) in spills {
1616 if anchor.sheet != sheet_folded {
1617 continue;
1618 }
1619 if anchor.addr == addr {
1620 continue; // the anchor itself is in `values`
1621 }
1622 let Some((i, j)) = rect.offset_of(addr) else {
1623 continue;
1624 };
1625 if let Some(Value::Array(rows)) = values.get(anchor) {
1626 return rows.get(i).and_then(|r| r.get(j)).cloned();
1627 }
1628 }
1629 None
1630 }
1631
1632 /// Resolves the folded target sheet name for a `Ref`'s optional sheet
1633 /// qualifier, or `None` if the named sheet does not exist.
1634 ///
1635 /// Through the recalc-wide index (issue #952): this used to be
1636 /// `workbook.sheet(name)` — a linear scan that case-folded every sheet name
1637 /// it passed — plus a second fold of the name it found, run once per
1638 /// *qualified* reference resolved, so a cross-sheet formula paid it on
1639 /// every evaluation of every cell.
1640 fn target_sheet(&self, sheet: &Option<String>) -> Option<String> {
1641 match sheet {
1642 None => Some(self.own_sheet.to_owned()),
1643 Some(name) => self.sheets.folded_of_name(name).map(str::to_owned),
1644 }
1645 }
1646}
1647
1648impl Resolver for GridResolver<'_> {
1649 fn resolve(&mut self, r: &Ref) -> CoreValue {
1650 match r {
1651 Ref::Cell { sheet, addr } => {
1652 let Some(target) = self.target_sheet(sheet) else {
1653 return CoreValue::Error(ErrorKind::Ref);
1654 };
1655 match Address::new(addr.row, addr.col) {
1656 Some(a) => self.cell_value(&target, a),
1657 None => CoreValue::Error(ErrorKind::Ref),
1658 }
1659 }
1660 Ref::Range { sheet, start, end } => {
1661 let Some(target) = self.target_sheet(sheet) else {
1662 return CoreValue::Error(ErrorKind::Ref);
1663 };
1664 self.resolve_range(&target, start, end)
1665 }
1666 Ref::Name(name) => {
1667 // Resolve the name to its canonical ref, then resolve that.
1668 let folder = CaseMapperBorrowed::new();
1669 let folded = simple_fold(&folder, name);
1670 let target = self
1671 .workbook
1672 .names()
1673 .iter()
1674 .find(|nr| simple_fold(&folder, &nr.name) == folded);
1675 match target {
1676 None => CoreValue::Error(ErrorKind::Name),
1677 // Re-parse the name's canonical `Sheet!A1` ref so a name
1678 // pointing at a cell or a range resolves identically to a
1679 // literal ref of the same shape.
1680 Some(nr) => self.resolve_name_ref(&nr.r#ref),
1681 }
1682 }
1683 Ref::Table {
1684 table,
1685 column,
1686 this_row,
1687 } => self.resolve_table_ref(table.as_deref(), column, *this_row),
1688 }
1689 }
1690}
1691
1692impl GridResolver<'_> {
1693 /// Materializes a range as a core `Value::Array` of its cells in row-major
1694 /// reading order — the shape the P1.3 [`Resolver`] contract specifies
1695 /// ("a range -> a Value::Array of the cells in reading order") and the shape
1696 /// core's aggregations (SUM/AVERAGE/COUNT/SUMIF) and shape functions
1697 /// consume.
1698 ///
1699 /// A single-column, multi-row range (a *vertical* range) is materialized
1700 /// as a nested `Array` of one-element row `Array`s — core's Nx1 column
1701 /// shape (see `to_2d`/`from_2d` in the array functions) — so elementwise
1702 /// operations over it (e.g. `=A1:A3*2`) spill down like Google Sheets,
1703 /// instead of losing their column orientation to a flat row. Every other
1704 /// shape (a single row, a single cell, or a genuine 2-D block) keeps the
1705 /// existing flat row-major array, unchanged. The own/target sheet has
1706 /// already been resolved.
1707 fn resolve_range(
1708 &self,
1709 sheet_folded: &str,
1710 start: &truecalc_core::CellAddr,
1711 end: &truecalc_core::CellAddr,
1712 ) -> CoreValue {
1713 let (r0, r1) = (start.row.min(end.row), start.row.max(end.row));
1714 let (c0, c1) = (start.col.min(end.col), start.col.max(end.col));
1715 let is_vertical = r1 > r0 && c0 == c1;
1716 let mut cells: Vec<CoreValue> = Vec::new();
1717 for r in r0..=r1 {
1718 for c in c0..=c1 {
1719 match Address::new(r, c) {
1720 Some(a) => {
1721 let v = self.cell_value(sheet_folded, a);
1722 // A spill anchor stores the full array; its individual
1723 // elements are visited when the range iteration reaches
1724 // the spilled positions (which resolve via spilled_value).
1725 // Use only the [0][0] element here to avoid double-counting.
1726 let scalar = match v {
1727 CoreValue::Array(ref rows) => match rows.first() {
1728 Some(CoreValue::Array(ref cols)) => {
1729 cols.first().cloned().unwrap_or(CoreValue::Empty)
1730 }
1731 Some(other) => other.clone(),
1732 None => CoreValue::Empty,
1733 },
1734 other => other,
1735 };
1736 cells.push(if is_vertical {
1737 CoreValue::Array(vec![scalar])
1738 } else {
1739 scalar
1740 });
1741 }
1742 None => cells.push(if is_vertical {
1743 CoreValue::Array(vec![CoreValue::Error(ErrorKind::Ref)])
1744 } else {
1745 CoreValue::Error(ErrorKind::Ref)
1746 }),
1747 }
1748 }
1749 }
1750 CoreValue::Array(cells)
1751 }
1752
1753 /// Resolves a `Ref::Table`: whole-column (`this_row: false`) materializes
1754 /// the column's data-row values as an array, using the **same** vertical
1755 /// wrapping [`resolve_range`](Self::resolve_range) uses for a
1756 /// single-column range (its `is_vertical` branch: one array element per
1757 /// row, each itself a one-element array — core's Nx1 column shape) — so
1758 /// `T[col]` broadcasts and spills identically to an equivalent explicit
1759 /// `A2:A12`-style reference. Current-row (`this_row: true`) looks up the
1760 /// single cell at `(current row, column)`.
1761 ///
1762 /// An unqualified reference (`table: None`) infers the table from
1763 /// `self.current_cell`'s containment within the table's *data* rows
1764 /// (excluding the header row); a qualified reference looks the table up
1765 /// by name directly. `#REF!` if the table doesn't exist, the column
1766 /// doesn't exist (looked up by reading the header row), or — for
1767 /// current-row only — the evaluating cell isn't inside the resolved
1768 /// table's data rows.
1769 fn resolve_table_ref(&self, table: Option<&str>, column: &str, this_row: bool) -> CoreValue {
1770 let folder = CaseMapperBorrowed::new();
1771 let target_table = match table {
1772 Some(name) => {
1773 let folded = simple_fold(&folder, name);
1774 self.workbook
1775 .tables()
1776 .iter()
1777 .find(|t| simple_fold(&folder, &t.name) == folded)
1778 }
1779 None => {
1780 let Some((sheet, addr)) = self.current_cell else {
1781 return CoreValue::Error(ErrorKind::Ref);
1782 };
1783 self.workbook.tables().iter().find(|t| {
1784 named_ref::parse_canonical_ref(&t.r#ref)
1785 .ok()
1786 .and_then(|parsed| table_ref::parsed_range_bounds(&t.r#ref, &parsed))
1787 .is_some_and(|b| {
1788 simple_fold(&folder, &b.sheet) == sheet
1789 && b.row_start < addr.row
1790 && addr.row <= b.row_end
1791 && b.col_start <= addr.column
1792 && addr.column <= b.col_end
1793 })
1794 })
1795 }
1796 };
1797 let Some(t) = target_table else {
1798 return CoreValue::Error(ErrorKind::Ref);
1799 };
1800 let Ok(parsed) = named_ref::parse_canonical_ref(&t.r#ref) else {
1801 return CoreValue::Error(ErrorKind::Ref);
1802 };
1803 let Some(bounds) = table_ref::parsed_range_bounds(&t.r#ref, &parsed) else {
1804 return CoreValue::Error(ErrorKind::Ref);
1805 };
1806 let sheet_folded = simple_fold(&folder, &bounds.sheet);
1807
1808 // Find the column's index by reading the header row (`bounds.row_start`).
1809 // Case-insensitive, same as the table-name and sheet-name lookups
1810 // above: column names are case-folded at table-definition time
1811 // (`table_ref::header_row_columns`), so lookup must match.
1812 let column_folded = simple_fold(&folder, column);
1813 let mut col = None;
1814 for c in bounds.col_start..=bounds.col_end {
1815 if let Some(a) = Address::new(bounds.row_start, c) {
1816 if let CoreValue::Text(header) = self.cell_value(&sheet_folded, a) {
1817 if simple_fold(&folder, &header) == column_folded {
1818 col = Some(c);
1819 break;
1820 }
1821 }
1822 }
1823 }
1824 let Some(col) = col else {
1825 return CoreValue::Error(ErrorKind::Ref);
1826 };
1827
1828 if this_row {
1829 let Some((cell_sheet, cell_addr)) = self.current_cell else {
1830 return CoreValue::Error(ErrorKind::Ref);
1831 };
1832 if cell_sheet != sheet_folded
1833 || cell_addr.row <= bounds.row_start
1834 || cell_addr.row > bounds.row_end
1835 {
1836 return CoreValue::Error(ErrorKind::Ref);
1837 }
1838 match Address::new(cell_addr.row, col) {
1839 Some(a) => self.cell_value(&sheet_folded, a),
1840 None => CoreValue::Error(ErrorKind::Ref),
1841 }
1842 } else {
1843 let data_start = bounds.row_start + 1;
1844 let mut cells = Vec::new();
1845 for r in data_start..=bounds.row_end {
1846 let scalar = match Address::new(r, col) {
1847 Some(a) => {
1848 let v = self.cell_value(&sheet_folded, a);
1849 // Same spill-anchor unwrap as `resolve_range`: a spill
1850 // anchor stores its full array, so use only the
1851 // [0][0] element here — otherwise a table-column cell
1852 // that happens to be a spill anchor would nest its
1853 // whole array as this row's "scalar" instead of
1854 // resolving to the same value an equivalent
1855 // `A2:A12`-style range would produce.
1856 match v {
1857 CoreValue::Array(ref rows) => match rows.first() {
1858 Some(CoreValue::Array(ref cols)) => {
1859 cols.first().cloned().unwrap_or(CoreValue::Empty)
1860 }
1861 Some(other) => other.clone(),
1862 None => CoreValue::Empty,
1863 },
1864 other => other,
1865 }
1866 }
1867 None => CoreValue::Error(ErrorKind::Ref),
1868 };
1869 // Same wrapping as `resolve_range`'s `is_vertical` branch: one
1870 // array element per data row, each a one-element array.
1871 cells.push(CoreValue::Array(vec![scalar]));
1872 }
1873 CoreValue::Array(cells)
1874 }
1875 }
1876
1877 /// Resolves a named range's canonical `ref` string (`Sheet!A1` /
1878 /// `Sheet!A1:B2`) the same way a literal reference resolves.
1879 fn resolve_name_ref(&mut self, r: &str) -> CoreValue {
1880 // The ref string parses as a one-reference formula; extract and resolve.
1881 // Parsed without an `Engine`: parsing is flavor-independent and never
1882 // reads the function registry, so constructing one per resolved
1883 // named-range reference was pure waste (issue #900).
1884 let formula = format!("={r}");
1885 match truecalc_core::parse_formula(&formula) {
1886 Ok(expr) => {
1887 let refs = truecalc_core::extract_refs(&expr);
1888 match refs.first() {
1889 Some(first) => self.resolve(first),
1890 None => CoreValue::Error(ErrorKind::Ref),
1891 }
1892 }
1893 Err(_) => CoreValue::Error(ErrorKind::Ref),
1894 }
1895 }
1896}
1897
1898/// Maps a core evaluated [`CoreValue`] to the workbook [`Value`] (schema §6).
1899/// Core arrays (flat or nested rows) become a rectangular 2-D workbook array;
1900/// a 1×1 array collapses to its scalar (schema §6).
1901fn core_to_workbook(v: CoreValue) -> Value {
1902 match v {
1903 CoreValue::Number(n) => Value::Number(n),
1904 CoreValue::Text(s) => Value::Text(s),
1905 CoreValue::Bool(b) => Value::Boolean(b),
1906 CoreValue::Error(e) => Value::Error(e.to_string()),
1907 CoreValue::ErrorMsg(e, m) => Value::ErrorMsg(e.to_string(), m),
1908 CoreValue::Empty => Value::Empty,
1909 CoreValue::Date(n) => Value::Date(n),
1910 CoreValue::Zoned(z) => Value::Zoned(z),
1911 CoreValue::Sparkline(spec) => Value::Sparkline(spec),
1912 CoreValue::Array(elems) => core_array_to_workbook(elems),
1913 }
1914}
1915
1916/// Normalizes a core array (which may be flat scalars or nested rows) into the
1917/// workbook's row-major 2-D shape, collapsing a 1×1 array to its scalar.
1918fn core_array_to_workbook(elems: Vec<CoreValue>) -> Value {
1919 if elems.is_empty() {
1920 // An empty array has no scalar form; surface as #REF! (a degenerate
1921 // spill the P3.5 engine will own). Kept minimal here.
1922 return Value::Error("#REF!".to_owned());
1923 }
1924 let nested = elems.iter().all(|e| matches!(e, CoreValue::Array(_)));
1925 let rows: Vec<Vec<Value>> = if nested {
1926 elems
1927 .into_iter()
1928 .map(|row| match row {
1929 CoreValue::Array(cells) => cells.into_iter().map(core_to_workbook).collect(),
1930 other => vec![core_to_workbook(other)],
1931 })
1932 .collect()
1933 } else {
1934 vec![elems.into_iter().map(core_to_workbook).collect()]
1935 };
1936 if rows.len() == 1 && rows[0].len() == 1 {
1937 return rows.into_iter().next().unwrap().into_iter().next().unwrap();
1938 }
1939 Value::Array(rows)
1940}
1941
1942/// Maps a workbook [`Value`] back to a core [`CoreValue`] for feeding a stored
1943/// cell value into evaluation through the resolver.
1944fn workbook_to_core(v: &Value) -> CoreValue {
1945 match v {
1946 Value::Number(n) => CoreValue::Number(*n),
1947 Value::Text(s) => CoreValue::Text(s.clone()),
1948 Value::Boolean(b) => CoreValue::Bool(*b),
1949 Value::Error(code) | Value::ErrorMsg(code, _) => {
1950 CoreValue::Error(error_kind_from_code(code))
1951 }
1952 Value::Empty => CoreValue::Empty,
1953 Value::Date(n) => CoreValue::Date(*n),
1954 Value::Zoned(z) => CoreValue::Zoned(z.clone()),
1955 Value::Sparkline(spec) => CoreValue::Sparkline(spec.clone()),
1956 Value::Array(rows) => CoreValue::Array(
1957 rows.iter()
1958 .map(|row| CoreValue::Array(row.iter().map(workbook_to_core).collect()))
1959 .collect(),
1960 ),
1961 }
1962}
1963
1964/// Parses a Sheets error code string back to a core [`ErrorKind`]; an unknown
1965/// code maps to `#REF!` (the most conservative reference error).
1966fn error_kind_from_code(code: &str) -> ErrorKind {
1967 match code {
1968 "#DIV/0!" => ErrorKind::DivByZero,
1969 "#VALUE!" => ErrorKind::Value,
1970 "#REF!" => ErrorKind::Ref,
1971 "#NAME?" => ErrorKind::Name,
1972 "#NUM!" => ErrorKind::Num,
1973 "#N/A" => ErrorKind::NA,
1974 "#NULL!" => ErrorKind::Null,
1975 _ => ErrorKind::Ref,
1976 }
1977}
1978
1979/// Whether `upper` (an upper-cased formula) calls the function `name`, i.e.
1980/// `name` appears followed by `(` (ignoring spaces). Avoids matching a name
1981/// that is merely a substring of a longer identifier.
1982fn contains_call(upper: &str, name: &str) -> bool {
1983 let bytes = upper.as_bytes();
1984 let nb = name.as_bytes();
1985 let mut i = 0;
1986 while let Some(pos) = find_from(bytes, nb, i) {
1987 // Preceding char must not be an identifier char.
1988 let before_ok = pos == 0 || !is_ident_byte(bytes[pos - 1]);
1989 // Following non-space char must be '('.
1990 let mut j = pos + nb.len();
1991 while j < bytes.len() && bytes[j] == b' ' {
1992 j += 1;
1993 }
1994 let after_ok = j < bytes.len() && bytes[j] == b'(';
1995 if before_ok && after_ok {
1996 return true;
1997 }
1998 i = pos + 1;
1999 }
2000 false
2001}
2002
2003fn find_from(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
2004 if needle.is_empty() || from + needle.len() > haystack.len() {
2005 return None;
2006 }
2007 (from..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
2008}
2009
2010fn is_ident_byte(b: u8) -> bool {
2011 b.is_ascii_alphanumeric() || b == b'_'
2012}
2013
2014/// The set of `(folded sheet, address)` cells whose spill coverage changed
2015/// between two anchor-rectangle maps: the union of all cells in any rectangle
2016/// that appeared, vanished, or resized (schema spec §5). Their readers may now
2017/// be stale and must be dirtied in an incremental recalc.
2018fn changed_rectangle_cells(
2019 before: &BTreeMap<CellRef, SpillRect>,
2020 after: &BTreeMap<CellRef, SpillRect>,
2021) -> BTreeSet<(String, Address)> {
2022 let mut out: BTreeSet<(String, Address)> = BTreeSet::new();
2023 let mut consider = |anchor: &CellRef, rect: &SpillRect| {
2024 // The anchor cell itself is a formula node with its own graph edges;
2025 // only the spilled cells need this spill-aware dirtying.
2026 for addr in rect.spilled_cells() {
2027 out.insert((anchor.sheet.clone(), addr));
2028 }
2029 };
2030 for (anchor, rect) in before {
2031 match after.get(anchor) {
2032 Some(same) if same == rect => {}
2033 _ => consider(anchor, rect),
2034 }
2035 }
2036 for (anchor, rect) in after {
2037 match before.get(anchor) {
2038 Some(same) if same == rect => {}
2039 _ => consider(anchor, rect),
2040 }
2041 }
2042 out
2043}
2044
2045/// Whether a spill rectangle and a range reference overlap (same sheet assumed
2046/// checked by the caller): their inclusive row/column extents intersect (issue
2047/// #591). Used to seed range aggregations that read spilled cells.
2048fn rect_overlaps_range(rect: &SpillRect, range: &RangeRef) -> bool {
2049 let rect_r0 = rect.anchor.row;
2050 let rect_r1 = rect.anchor.row + rect.rows - 1;
2051 let rect_c0 = rect.anchor.column;
2052 let rect_c1 = rect.anchor.column + rect.cols - 1;
2053 rect_r0 <= range.end.row
2054 && rect_r1 >= range.start.row
2055 && rect_c0 <= range.end.column
2056 && rect_c1 >= range.start.column
2057}
2058
2059/// SplitMix64 finalizer — a fast, well-distributed integer mix.
2060fn mix64(mut z: u64) -> u64 {
2061 z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
2062 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2063 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2064 z ^ (z >> 31)
2065}