Skip to main content

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}