From 303eabaad194f97330cbf0a3434e312d98950895 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:42:51 -0500 Subject: [PATCH] Recalc: an array that read another array's old spill is evaluated again When two dynamic arrays were recalculated in the same pass and the first read the second's spill area before the second was evaluated, it read the old values. Formulas reading a spill area that changed are evaluated again, but dynamic arrays were left out of that second round (only to keep an array from re-queuing itself), so with D11 = SEQUENCE(1,3,A1) and G5 = UNIQUE(VSTACK(F9:F12,A1)), changing A1 from 1 to 10 left 3 in G6 instead of 12. Only the array itself is left out now. --- crates/calc/src/recalc.rs | 3 +-- crates/engine/src/tests.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 2aa23645c..cfef5f478 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -571,7 +571,7 @@ impl Calc { if let Some(r) = changed_range { let mut deps = Vec::new(); self.graph.dependents_of_range(k.0, r, &mut deps); - spill_changes.extend(deps); + spill_changes.extend(deps.into_iter().filter(|d| d != k)); } if let Some(o) = old && !sheet.spill_ranges.get(&k.1).is_some_and(|n| n.contains(o.start) && n.contains(o.end)) @@ -599,7 +599,6 @@ impl Calc { } spill_changes.sort_by_key(|(s, c)| (*s, c.row, c.col)); spill_changes.dedup(); - spill_changes.retain(|k| !spills.contains_key(k)); if spill_changes.is_empty() { break; } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index c90802128..72d67f5cd 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -206,6 +206,19 @@ fn a_spill_that_goes_away_unblocks_another() { assert_eq!((v(&s, "E10"), v(&s, "G10")), (Value::Number(1.0), Value::Number(3.0))); } +#[test] +fn an_array_reading_another_arrays_spill_is_updated() { + // G5 is evaluated before D11 in the same pass, so it first reads D11's old spill. + let mut s = s(); + s.execute("cell.set", json!({"cell": "A1", "input": "1"})).unwrap(); + s.execute("cell.set", json!({"cell": "D11", "input": "=SEQUENCE(1,3,A1)"})).unwrap(); + s.execute("cell.set", json!({"cell": "G5", "input": "=UNIQUE(VSTACK(F9:F12,A1))"})).unwrap(); + s.execute("cell.set", json!({"cell": "A1", "input": "10"})).unwrap(); + assert_eq!((v(&s, "G6"), v(&s, "G7")), (Value::Number(12.0), Value::Number(10.0))); + s.execute("formulas.calculateNow", json!({})).unwrap(); + assert_eq!((v(&s, "G6"), v(&s, "G7")), (Value::Number(12.0), Value::Number(10.0))); +} + #[test] fn every_command_survives_empty_params() { let mut s = Session::new();