From e51c1d98fb54574e15dc386085befce69aa9e861 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:30:09 -0500 Subject: [PATCH] Recalc: clearing a distant blocker unblocks the spill Clearing a cell re-evaluated #SPILL! anchors only within 64 rows and columns above and to the left of it, so =SEQUENCE(100) in A1 stayed #SPILL! after the blocker in A90 was cleared. Emptied cells now look for blocked anchors any distance above or to the left, with one pass over the formulas per sheet however many cells were cleared. Fixes #139. --- crates/calc/src/recalc.rs | 15 +++++++++++++++ crates/engine/src/tests.rs | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 32d588705..e537ccf0b 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -396,6 +396,7 @@ impl Calc { return; } let mut seeds: Vec = changed.to_vec(); + let mut emptied: Vec = Vec::new(); // Typing into (or clearing) a cell a dynamic array spills over (or would spill over) // re-evaluates the anchor, which may now be blocked or unblocked. for &k in changed { @@ -410,6 +411,20 @@ impl Calc { seeds.push((k.0, c)); } } + if !sh.cells.has(k.1) { + match emptied.iter_mut().find(|x| x.0 == k.0) { + Some(x) => x.1 = CellRef::new(x.1.row.max(k.1.row), x.1.col.max(k.1.col)), + None => emptied.push(k), + } + } + } + } + // An emptied cell can unblock an anchor any distance above or to the left of it. + for (si, end) in emptied { + for &(s, c) in self.graph.nodes.keys() { + if s == si && c.row <= end.row && c.col <= end.col && wb.sheet(s).is_some_and(|sh| sh.value(c) == Value::Error(CellError::Spill)) { + seeds.push((s, c)); + } } } // Spill areas of changed anchors also change. diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9343faeda..b7f7323fd 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -184,6 +184,16 @@ fn structural_edits_move_spills_with_their_formula() { assert_eq!(col(&s), [Value::Empty, Value::Empty, Value::Empty, Value::Empty]); } +#[test] +fn clearing_a_distant_blocker_unblocks_the_spill() { + let mut s = s(); + s.execute("cell.set", json!({"cell": "A1", "input": "=SEQUENCE(100)"})).unwrap(); + s.execute("cell.set", json!({"cell": "A90", "input": "x"})).unwrap(); + assert_eq!(v(&s, "A1"), Value::Error(gridcraft_core::CellError::Spill)); + s.execute("edit.clearContents", json!({"range": "A90"})).unwrap(); + assert_eq!((v(&s, "A1"), v(&s, "A100")), (Value::Number(1.0), Value::Number(100.0))); +} + #[test] fn every_command_survives_empty_params() { let mut s = Session::new();