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();