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