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.
This commit is contained in:
nihilist-narwhal
2026-10-13 06:54:21 -05:00
parent 84acf6293b
commit 260da4e752
2 changed files with 43 additions and 1 deletions
+24 -1
View File
@@ -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<Key>, 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<Key> = 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);
+19
View File
@@ -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();