From 260da4e7527bca8cf4651ce47f3101cd858bf317 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:28:57 -0500 Subject: [PATCH] Recalc: an array that spills as before doesn't requeue its dependents After each round, every array formula that was evaluated queued the dependents of its spill area again, even when it spilled the same area with the same values. Two arrays that read each other's spill areas then queued each other in every round, up to the limit of 8: one edit evaluated 24 formulas instead of 3. An unchanged spill now leaves its dependents alone. last_recalc_cells counts the formulas of all rounds. --- crates/calc/src/recalc.rs | 25 ++++++++++++++++++++++++- crates/calc/src/tests.rs | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index b391821d7..a3daee641 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -456,6 +456,20 @@ fn spill_blocked(host: &PassHost<'_>, (sheet, anchor): Key, a: &Array) -> bool { || sh.spill.range(range.start..=range.end).any(|(c, _)| range.contains(*c) && spilled_over(*c)) } +/// Whether `arr`, anchored at `anchor`, would spill exactly what the sheet already holds over +/// `range`: the same area, nothing new in the way, and the same values. +fn spills_as_before(sheet: &gridcraft_model::Sheet, anchor: CellRef, range: RangeRef, arr: &Array) -> bool { + let end = + CellRef::new(anchor.row.saturating_add((arr.rows as u32).saturating_sub(1)), anchor.col.saturating_add((arr.cols as u32).saturating_sub(1))); + RangeRef::new(anchor, end) == range + && !sheet.merges.iter().any(|m| m.intersects(&range)) + && !sheet.tables.iter().any(|t| t.range.contains(anchor)) + && range + .iter() + .enumerate() + .all(|(i, c)| c == anchor || (!sheet.cells.has(c) && sheet.spill.get(&c) == Some(arr.data.get(i).unwrap_or(&Value::Empty)))) +} + impl PassHost<'_> { fn compute(&mut self, k: Key) -> Value { if let Some(v) = self.results.get(&k) { @@ -950,6 +964,7 @@ impl Calc { /// Evaluates the dirty set and writes results (and spills) back. fn run(&mut self, wb: &mut Workbook, mut dirty: Vec, full: bool) { let mut dynamic_again = !self.graph.dynamic.is_empty(); + self.last_recalc_cells = 0; for _round in 0..8 { if dirty.is_empty() { break; @@ -984,7 +999,7 @@ impl Calc { eprintln!(" eval {} formulas {:.1} ms", results.len(), prof_now() - tp); } let tw = prof_now(); - self.last_recalc_cells = results.len(); + self.last_recalc_cells += results.len(); self.circular = cycles; // Write back. let mut spill_changes: Vec = Vec::new(); @@ -1004,6 +1019,14 @@ impl Calc { if old.is_none() && new.is_none() { continue; } + // An array that spills exactly as before changes nothing its dependents read, so + // they aren't queued again. Otherwise two arrays that read each other's spill areas + // queue each other in every round, up to the round limit. + if let (Some(r), Some(arr)) = (old, new) + && spills_as_before(sheet, k.1, r, arr) + { + continue; + } if let Some(r) = old { for c in r.iter() { sheet.spill.remove(&c); diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 196c57de7..4529e1c19 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -67,6 +67,25 @@ fn spill_range_operator() { assert_eq!(t.num("E1"), 100.0); } +#[test] +fn arrays_that_spill_as_before_do_not_requeue_their_dependents() { + // Two arrays that each read the other's spill area, and a formula reading both spills. + let mut t = T::new(); + t.set("A1", "=SEQUENCE(2)+0*B2"); + t.set("B1", "=SEQUENCE(2)+0*A2"); + t.set("C1", "=SUM(A1#)+SUM(B1#)"); + // An edit that spills the same values again: one round, not one per round up to the limit. + t.set("A1", "=SEQUENCE(2)+0*B2+0"); + assert_eq!((t.num("A2"), t.num("B2"), t.num("C1")), (2.0, 2.0, 6.0)); + assert!(t.calc.last_recalc_cells <= 3, "{} formulas evaluated for one edit", t.calc.last_recalc_cells); + // Only the anchor's value changes: formulas reading the whole spill still see it. + t.set("A1", "={7;2}+0*B2"); + assert_eq!((t.num("A1"), t.num("C1")), (7.0, 12.0)); + // A spill that does change still reaches its dependents. + t.set("A1", "=SEQUENCE(2)*10+0*B2"); + assert_eq!((t.num("A2"), t.num("C1")), (20.0, 33.0)); +} + #[test] fn arithmetic_and_dependencies() { let mut t = T::new();