Recalc: INDIRECT and OFFSET see spills laid out in the same pass

A formula whose references are only known while evaluating isn't in the
dependency graph, so when it read a spill area before the array was laid
out in the same pass, nothing evaluated it again. With the full
recalculation now laying every spill out afresh, =INDIRECT("F7") over
a spill moved by an inserted row read 0 after Calculate Now. Such
formulas are evaluated once more when a pass computed any array.
This commit is contained in:
nihilist-narwhal
2026-10-10 04:59:27 -05:00
parent 303eabaad1
commit 97f7dca886
2 changed files with 18 additions and 0 deletions
+7
View File
@@ -481,6 +481,7 @@ impl Calc {
/// Evaluates the dirty set and writes results (and spills) back.
fn run(&mut self, wb: &mut Workbook, mut dirty: Vec<Key>, full: bool) {
let mut dynamic_again = !self.graph.dynamic.is_empty();
for _round in 0..8 {
if dirty.is_empty() {
break;
@@ -579,6 +580,12 @@ impl Calc {
freed.push((k.0, o));
}
}
// Formulas with references only known while evaluating (INDIRECT, OFFSET) may have read
// a spill area before it was laid out: they are evaluated once more.
if dynamic_again && !spills.is_empty() {
dynamic_again = false;
spill_changes.extend(self.graph.dynamic.iter().copied());
}
// An area a formula no longer spills over may unblock another formula's array.
for (si, o) in freed {
for &(s, c) in self.graph.nodes.keys() {
+11
View File
@@ -219,6 +219,17 @@ fn an_array_reading_another_arrays_spill_is_updated() {
assert_eq!((v(&s, "G6"), v(&s, "G7")), (Value::Number(12.0), Value::Number(10.0)));
}
#[test]
fn indirect_reads_a_spill_laid_out_in_the_same_pass() {
let mut s = s();
s.execute("cell.set", json!({"cell": "F5", "input": "=SEQUENCE(2,2,7)"})).unwrap();
s.execute("home.insertRows", json!({"rows": "3:3"})).unwrap();
s.execute("cell.set", json!({"cell": "E5", "input": "=INDIRECT(\"F7\")"})).unwrap();
assert_eq!(v(&s, "E5"), Value::Number(9.0));
s.execute("formulas.calculateNow", json!({})).unwrap();
assert_eq!(v(&s, "E5"), Value::Number(9.0));
}
#[test]
fn every_command_survives_empty_params() {
let mut s = Session::new();