diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 13b47c41c..de9e3902a 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -184,6 +184,26 @@ struct PassHost<'a> { depth: usize, } +/// Whether an array result at `k` can't spill: its area runs off the sheet, overlaps merged cells, +/// holds other cells, or another formula's spill (unless that formula is recalculated in this +/// pass, and so may spill elsewhere now). +fn spill_blocked(host: &PassHost<'_>, (sheet, anchor): Key, a: &Array) -> bool { + let Some(sh) = host.wb.sheet(sheet) else { return false }; + let end = + CellRef::new(anchor.row.saturating_add((a.rows as u32).saturating_sub(1)), anchor.col.saturating_add((a.cols as u32).saturating_sub(1))); + let range = RangeRef::new(anchor, end); + let own = sh.spill_ranges.get(&anchor); + let spilled_over = |c: CellRef| { + !own.is_some_and(|o| o.contains(c)) + && sh.spill_ranges.iter().any(|(o, r)| *o != anchor && r.contains(c) && !host.exprs.contains_key(&(sheet, *o))) + }; + end.row >= gridcraft_core::MAX_ROWS + || end.col >= gridcraft_core::MAX_COLS + || sh.merges.iter().any(|m| m.intersects(&range)) + || sh.cells.iter_range(range).any(|(c, cell)| c != anchor && (!cell.value.is_empty() || cell.formula.is_some())) + || sh.spill.range(range.start..=range.end).any(|(c, _)| range.contains(*c) && spilled_over(*c)) +} + impl PassHost<'_> { fn compute(&mut self, k: Key) -> Value { if let Some(v) = self.results.get(&k) { @@ -216,6 +236,14 @@ impl PassHost<'_> { Value::Empty => Value::Number(0.0), v => v, }; + // An array that can't spill is #SPILL! for the formulas evaluated after it in this pass too. + let v = match self.spills.get(&k) { + Some(a) if spill_blocked(self, k, a) => { + self.spills.remove(&k); + Value::Error(CellError::Spill) + } + _ => v, + }; self.results.insert(k, v.clone()); v } diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 67db0830d..e911ef989 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -105,6 +105,45 @@ fn operators() { assert_eq!(t.num("A7"), 0.0); } +#[test] +fn blocked_spill_reads_as_spill_error() { + // Calculated in the same pass, a formula over a blocked spill read the array that couldn't + // spill. + let mut t = T::new(); + let s = t.wb.sheet_mut(0).unwrap(); + for a in ["A1", "B1", "C1", "A2", "C2", "A3", "B3", "C3"] { + s.cells.set(c(a), Cell::value(Value::Number(1.0))); + } + s.cells.set(c("B2"), Cell::formula(Formula::new("=SEQUENCE(2,2,9)"))); + s.cells.set(c("E1"), Cell::formula(Formula::new("=SUM(A1:C3)"))); + t.calc.recalc_all(&mut t.wb); + assert_eq!(t.get("B2"), Value::Error(CellError::Spill)); + assert_eq!(t.get("E1"), Value::Error(CellError::Spill)); + assert_eq!(t.num("C3"), 1.0); +} + +#[test] +fn blocked_spill_read_through_offset() { + // The reader's reference is only known while evaluating, so it isn't in the dependency graph. + let mut t = T::new(); + t.set("E4", "5"); + t.set("E8", "=SUM(OFFSET(E2,0,0,2,2))"); + t.set("E3", "=SEQUENCE(2,2,3)"); + t.calc.recalc_all(&mut t.wb); + assert_eq!(t.get("E3"), Value::Error(CellError::Spill)); + assert_eq!(t.get("E8"), Value::Error(CellError::Spill)); +} + +#[test] +fn spill_into_the_old_area_of_a_blocked_neighbour() { + // C7's array becomes blocked by C8, whose array then takes over C7's old area. + let mut t = T::new(); + t.set("C7", "=SEQUENCE(2,2)"); + t.set("C8", "=SEQUENCE(2,2)"); + assert_eq!(t.get("C7"), Value::Error(CellError::Spill)); + assert_eq!([t.num("C8"), t.num("D8"), t.num("C9"), t.num("D9")], [1.0, 2.0, 3.0, 4.0]); +} + #[test] fn ifs_and_lazy() { let mut t = T::new();