From 48910d71d295322b3ec6ee0ff749d0b0272d2471 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:24:41 -0500 Subject: [PATCH] Recalc: replacing or clearing a spilling formula removes its spill Typing a value over a dynamic array formula, or clearing it, left the values it had spilled: with =SEQUENCE(3) in A1, typing 2 into A1 kept 2 and 3 in A2:A3, and =SUM(A1:A3) gave 7. Only formulas are evaluated again, so the old spill of a cell that no longer holds a formula was never written back. It is now removed when the formula goes, and the formulas reading its area are recalculated. --- crates/calc/src/recalc.rs | 13 +++++++++++++ crates/engine/src/tests.rs | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 13b47c41c..584339534 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -351,6 +351,7 @@ impl Calc { /// Updates the graph for cells whose content changed and recalculates what depends on them. pub fn cells_changed(&mut self, wb: &mut Workbook, changed: &[Key]) { + let mut cleared: Vec<(usize, RangeRef)> = Vec::new(); for &k in changed { let formula = wb.sheet(k.0).and_then(|s| s.cell(k.1)).and_then(|c| c.formula.clone()); match formula.and_then(|f| f.expr()) { @@ -360,6 +361,15 @@ impl Calc { } None => { self.graph.remove(k); + // A formula replaced by a constant (or cleared) takes its spilled values with it. + if let Some(sh) = wb.sheet_mut(k.0) + && let Some(r) = sh.spill_ranges.remove(&k.1) + { + for c in r.iter() { + sh.spill.remove(&c); + } + cleared.push((k.0, r)); + } // A formula that doesn't parse evaluates to #NAME?. if let Some(cell) = wb.sheet_mut(k.0).and_then(|s| s.cells.get_mut(k.1)) && cell.formula.is_some() @@ -398,6 +408,9 @@ impl Calc { seeds.extend(r.iter().take(65536).map(|c| (k.0, c))); } } + for (si, r) in &cleared { + seeds.extend(r.iter().take(65536).map(|c| (*si, c))); + } let t0 = prof_now(); let dirty = self.dirty_closure(wb, &seeds); let t1 = prof_now(); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f3e36cdf4..361fbce48 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -153,6 +153,18 @@ fn samples_build() { } } +#[test] +fn replacing_a_spilling_formula_clears_its_spill() { + let mut s = s(); + s.execute("cell.set", json!({"cell": "A1", "input": "=SEQUENCE(3)"})).unwrap(); + s.execute("cell.set", json!({"cell": "B1", "input": "=SUM(A1:A3)"})).unwrap(); + s.execute("cell.set", json!({"cell": "A1", "input": "2"})).unwrap(); + assert_eq!((v(&s, "A2"), v(&s, "A3"), v(&s, "B1")), (Value::Empty, Value::Empty, Value::Number(2.0))); + s.execute("cell.set", json!({"cell": "A1", "input": "=SEQUENCE(3)"})).unwrap(); + s.execute("edit.clearContents", json!({"range": "A1"})).unwrap(); + assert_eq!((v(&s, "A2"), v(&s, "B1")), (Value::Empty, Value::Number(0.0))); +} + #[test] fn every_command_survives_empty_params() { let mut s = Session::new();