From d713a2da0b68e67ab243dfc859f330cd6a4812e7 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 4 Nov 2026 01:02:19 +0000 Subject: [PATCH] address review: keep the cached value of a follower dropped for the shared-formula copy limit inside array ranges --- crates/xlsx/src/sheet_read.rs | 7 ++++++- crates/xlsx/src/tests/malformed.rs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/xlsx/src/sheet_read.rs b/crates/xlsx/src/sheet_read.rs index 5b2ffe61f..ed176bd5e 100644 --- a/crates/xlsx/src/sheet_read.rs +++ b/crates/xlsx/src/sheet_read.rs @@ -47,6 +47,9 @@ struct RowState { /// Bytes of unparsable shared-master text copied into follower cells so far: each follower /// keeps its own copy, so a long master and many followers must stay bounded together. shared_fallback_bytes: u64, + /// The cell being read lost its shared formula to the copy limit above: its cached value is + /// kept, even inside an array range (where a formula's cached values otherwise give way to it). + shared_copy_refused: bool, dropped: u64, rows: u64, } @@ -575,6 +578,7 @@ fn read_cell(cx: &mut Ctx<'_>, sheet: &mut Sheet, st: &mut RowState, c: &El, pos return; } let dynamic = c.attr("cm").is_some(); + st.shared_copy_refused = false; let formula = c.child("f").and_then(|f| read_formula(cx, st, f, pos, dynamic)); // A `PY(index,0|1)` formula is a Python cell whose code is a script of the Python part. The // older `PY("…",0|1)` form is read from the formula's own text: the formula parser would fold @@ -595,7 +599,7 @@ fn read_cell(cx: &mut Ctx<'_>, sheet: &mut Sheet, st: &mut RowState, c: &El, pos if py.is_some() { record_python_spill(st, sheet, pos); } - let spilled_from = if formula.is_none() && py.is_none() { dynamic_range(st, pos) } else { None }; + let spilled_from = if formula.is_none() && py.is_none() && !st.shared_copy_refused { dynamic_range(st, pos) } else { None }; let python_anchor = spilled_from.is_some_and(|r| sheet.cells.get(r.start).is_some_and(|a| a.py.is_some())); let value = match spilled_from { // A Python cell's spilled values are kept: they are its result until it runs again. @@ -652,6 +656,7 @@ fn read_formula_text(cx: &mut Ctx<'_>, st: &mut RowState, f: &El, pos: CellRef, let total = st.shared_fallback_bytes.saturating_add(sh.text.len() as u64); if total > MAX_SHARED_FALLBACK_BYTES { cx.warn("some shared formulas could not be parsed and exceed the copy limit; those cells kept their cached values"); + st.shared_copy_refused = true; return None; } st.shared_fallback_bytes = total; diff --git a/crates/xlsx/src/tests/malformed.rs b/crates/xlsx/src/tests/malformed.rs index 0d232187a..c104de6eb 100644 --- a/crates/xlsx/src/tests/malformed.rs +++ b/crates/xlsx/src/tests/malformed.rs @@ -274,6 +274,24 @@ fn unparsable_shared_master_copies_stay_bounded() { assert!(rep.warnings.iter().any(|w| w.contains("exceed the copy limit")), "{:?}", rep.warnings); } +/// A follower dropped for the copy limit keeps its cached value even where an array range covers it. +#[test] +fn followers_past_the_copy_limit_keep_their_values_inside_array_ranges() { + let master = format!("1{}", "+1".repeat(40_000)); + let mut rows = format!( + r#"11{master}1"# + ); + for r in 2..=2000 { + rows.push_str(&format!(r#"{r}"#)); + } + let (wb, rep) = read_xlsx(&minimal(&format!("{rows}"), &[], "", "")).unwrap(); + let s = wb.sheet(0).unwrap(); + let last = gridcraft_core::CellRef::new(1999, 2); + assert!(s.cell(last).is_some_and(|c| c.formula.is_none()), "the limit was reached"); + assert_eq!(s.value(last), gridcraft_core::Value::Number(2000.0)); + assert!(rep.warnings.iter().any(|w| w.contains("exceed the copy limit")), "{:?}", rep.warnings); +} + /// Formula text built to overflow a recursive parser or tree walk (a million `%`, a 200,000-term /// sum): the cells keep their formula text and cached value, nothing overflows the stack. #[test]