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