diff --git a/ROADMAP.md b/ROADMAP.md index 070552d07..931d22418 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-12 · **Change:** minor (rows auto-fit oversized fonts on open and whole-row/column formatting) · **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) · **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. @@ -132,6 +132,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-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) | @@ -193,6 +194,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-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 b391821d7..0905b8fb2 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -1036,7 +1036,15 @@ 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.into_iter().filter(|d| d != k)); + // Not the anchor itself (that would read its own array), unless its old array + // is gone or partly freed: its new formula may have read the old values, still + // laid out while it was evaluated. + let freed = match (old, sheet.spill_ranges.get(&k.1)) { + (Some(o), Some(n)) => !(n.contains(o.start) && n.contains(o.end)), + (_, None) => true, + (None, Some(_)) => false, + }; + spill_changes.extend(deps.into_iter().filter(|d| d != k || freed)); } if let Some(o) = old && !sheet.spill_ranges.get(&k.1).is_some_and(|n| n.contains(o.start) && n.contains(o.end)) diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 196c57de7..924c2db66 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -543,6 +543,25 @@ fn spills() { assert_eq!(t.num("D2"), 40.0); } +#[test] +fn an_anchor_given_a_formula_that_reads_its_old_spill_area_sees_it_empty() { + // The new formula was evaluated while the old array was still laid out, and only the + // anchor's dependents were evaluated again once it was gone. + let mut t = T::new(); + t.set("A1", "=SEQUENCE(3)"); + assert_eq!(t.num("A3"), 3.0); + t.set("A1", "=SUM(A2:A3)+1"); + assert_eq!((t.num("A1"), t.get("A3")), (1.0, Value::Empty)); + // Still an array, now spilling elsewhere: it read A2 as 2 and spilled three columns. + t.set("A1", "=SEQUENCE(3)"); + t.set("A1", "=SEQUENCE(1,A2+1)"); + assert_eq!((t.num("A1"), t.get("B1"), t.get("A2")), (1.0, Value::Empty, Value::Empty)); + // A smaller array inside the old area: it read A3 as 3 and spilled over A1:A2. + t.set("A1", "=SEQUENCE(3)"); + t.set("A1", "=SEQUENCE(MAX(1,A3-1))"); + assert_eq!((t.num("A1"), t.get("A2"), t.get("A3")), (1.0, Value::Empty, Value::Empty)); +} + #[test] fn references_keep_their_size_past_the_used_range() { let mut t = T::new();