diff --git a/ROADMAP.md b/ROADMAP.md index 931d22418..d7f463c93 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** minor (a spill anchor given a formula that reads its old spill area sees it empty) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** minor (a spill anchor given a formula that reads its old spill area sees it empty; formulas reading an array that loses a spill conflict see `#SPILL!`) · **Target:** Microsoft Excel (Microsoft 365) GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web. @@ -133,6 +133,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| | 2026-10-14 | Recalculation: a dynamic array's anchor given a new formula that reads its old spill area (`A1` was `=SEQUENCE(3)`, becomes `=SUM(A2:A3)+1` or `=SEQUENCE(1,A2+1)`) sees that area empty instead of the old values | +| 2026-10-14 | Recalculation: when two arrays of one recalculation want the same empty cells, the formulas that read the one laid out second (at its anchor or over the cells it would have spilled into) read `#SPILL!` and empty cells instead of its values | | 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) | | 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) | | 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) | @@ -195,6 +196,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | Change | Summary | |---|---|---| | 2026-10-14 | minor | Progress log: spill anchor reading its old spill area | +| 2026-10-14 | minor | Progress log: formulas reading an array that loses a spill conflict | | 2026-10-12 | minor | Rows auto-fit oversized fonts on open and whole-row/column formatting (#833); progress log row | | 2026-10-12 | minor | Progress log: multi-area row/column delete (#758) | | 2026-10-12 | trivial | Progress log: XLSX chart sheets kept on save (#727) | diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 8e607137d..c3cf19795 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -1147,6 +1147,13 @@ impl Calc { if let Some(cell) = sheet.cells.get_mut(k.1) { cell.value = Value::Error(CellError::Spill); } + // Not blocked while the pass ran (it would have been #SPILL! there), so + // blocked by another array of this pass: the formulas that read this one, + // at its anchor or over the area it would have spilled into, read #SPILL! + // or empty cells now. + let mut deps = Vec::new(); + self.graph.dependents_of_range(k.0, range, &mut deps); + spill_changes.extend(deps.into_iter().filter(|d| d != k)); } else { for (i, c) in range.iter().enumerate() { if c != k.1 { diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 165c4af1c..6efd0fc1a 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -387,6 +387,25 @@ fn spill_into_the_old_area_of_a_blocked_neighbour() { assert_eq!([t.num("C8"), t.num("D8"), t.num("C9"), t.num("D9")], [1.0, 2.0, 3.0, 4.0]); } +#[test] +fn formulas_reading_an_array_that_loses_a_spill_conflict_read_spill_errors() { + // Two arrays of one recalculation want the same empty cell: the one laid out second is + // #SPILL!, and the formulas that read it in that pass (at its anchor, through `A2#`, or over + // the cells it would have spilled into) kept its values. + let mut t = T::new(); + t.set("B1", "=SEQUENCE(3)"); + t.set("A2", "=SEQUENCE(1,3)"); + t.set("D2", "=A2*10"); + t.set("E2", "=C2"); + t.set("F2", "=SUM(A2#)"); + t.set("G2", "=SUM(C2:C3)"); + // Both arrays recalculated together: B1 is laid out first and takes B2. + t.calc.recalc_all(&mut t.wb); + let spill = Value::Error(CellError::Spill); + assert_eq!((t.num("B2"), t.get("A2"), t.get("D2")), (2.0, spill.clone(), spill)); + assert_eq!((t.get("C2"), t.num("E2"), t.get("F2"), t.num("G2")), (Value::Empty, 0.0, Value::Error(CellError::Ref), 0.0)); +} + #[test] fn arrays_do_not_spill_in_tables() { // Excel: spilled array formulas aren't supported in tables.