Skip to main content

ravenrs_sheet/calc/
recalc.rs

1//! Dependency graph and recalculation.
2//!
3//! The graph maps every formula cell to its static precedents (resolved areas). A reverse index
4//! answers "which formulas read this cell?": single-cell precedents in a hash map, ranges as row
5//! intervals per column (wide ranges per sheet), each an interval index so a lookup costs
6//! O(log n + matches) however many ranges share the column. After an edit, the dirty set is the
7//! transitive closure of dependents; it is evaluated in topological order, with on-demand
8//! evaluation as a safety net for dynamic references. Cycles produce `#CIRC!` unless iterative
9//! calculation is on: then a formula read while it is being evaluated gives its previous value,
10//! and the formulas that read one are evaluated again until the loop settles
11//! (`PassHost::iterate_loops`).
12
13use std::collections::VecDeque;
14
15use crate::base::{Array, CellError, CellRef, RangeRef, Value};
16use crate::formula::Expr;
17use crate::model::{CalcMode, Workbook};
18
19use crate::calc::eval::{Evaluator, Host, precedents, stack_position};
20
21mod graph;
22mod pass_host;
23
24#[cfg(test)]
25use graph::Intervals;
26pub use graph::{FxHasher, Graph, Key};
27use graph::{HashMap, HashSet, Node, Taken};
28use pass_host::{PassHost, spills_as_before};
29
30/// Calculation state kept beside a workbook.
31#[derive(Clone, Debug)]
32pub struct Calc {
33    pub graph: Graph,
34    rng: u64,
35    /// Fixed "now" for deterministic tests; `None` = system clock.
36    pub fixed_now: Option<f64>,
37    /// Cells whose formulas have errors from cycles.
38    pub circular: Vec<Key>,
39    pub last_recalc_cells: usize,
40}
41
42impl Default for Calc {
43    fn default() -> Self {
44        Calc { graph: Graph::default(), rng: 0x9E37_79B9_7F4A_7C15, fixed_now: None, circular: vec![], last_recalc_cells: 0 }
45    }
46}
47
48/// Pops one finished component off Tarjan's stack, labelling its nodes with `comp_index`. The
49/// component is everything up to and including `root`.
50fn pop_component(stack: &mut Vec<usize>, on_stack: &mut [bool], comp: &mut [usize], comp_index: usize, root: usize) {
51    while let Some(w) = stack.pop() {
52        on_stack[w] = false;
53        comp[w] = comp_index;
54        if w == root {
55            break;
56        }
57    }
58}
59
60/// Strongly connected components of a graph given as adjacency lists (Tarjan, iterative): each
61/// node's component index, and the number of nodes in each component.
62fn components(edges: &[Vec<usize>]) -> (Vec<usize>, Vec<usize>) {
63    let n = edges.len();
64    let (mut low, mut num) = (vec![0usize; n], vec![usize::MAX; n]);
65    let (mut on_stack, mut stack, mut comp) = (vec![false; n], Vec::new(), vec![usize::MAX; n]);
66    let (mut next, mut comps) = (0usize, 0usize);
67    for root in 0..n {
68        if num[root] != usize::MAX {
69            continue;
70        }
71        let mut call: Vec<(usize, usize)> = vec![(root, 0)];
72        num[root] = next;
73        low[root] = next;
74        next += 1;
75        stack.push(root);
76        on_stack[root] = true;
77        while let Some(top) = call.last_mut() {
78            let v = top.0;
79            if let Some(&w) = edges.get(v).and_then(|e| e.get(top.1)) {
80                top.1 += 1;
81                if num[w] == usize::MAX {
82                    num[w] = next;
83                    low[w] = next;
84                    next += 1;
85                    stack.push(w);
86                    on_stack[w] = true;
87                    call.push((w, 0));
88                } else if on_stack[w] {
89                    low[v] = low[v].min(num[w]);
90                }
91                continue;
92            }
93            call.pop();
94            if let Some(&(u, _)) = call.last() {
95                low[u] = low[u].min(low[v]);
96            }
97            if low[v] == num[v] {
98                pop_component(&mut stack, &mut on_stack, &mut comp, comps, v);
99                comps += 1;
100            }
101        }
102    }
103    let mut size = vec![0usize; comps];
104    for &c in &comp {
105        if let Some(s) = size.get_mut(c) {
106            *s += 1;
107        }
108    }
109    (comp, size)
110}
111
112impl Calc {
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    fn now(&self) -> f64 {
118        if let Some(n) = self.fixed_now {
119            return n;
120        }
121        now_serial()
122    }
123
124    /// Rebuilds the graph from scratch.
125    pub fn rebuild(&mut self, wb: &Workbook) {
126        self.graph = Graph::default();
127        let mut circ = Vec::new();
128        for (si, sheet) in wb.sheets.iter().enumerate() {
129            for (c, cell) in sheet.cells.iter() {
130                if let Some(f) = &cell.formula
131                    && let Some((areas, dynamic)) = f.with_expr(|e| precedents(wb, si, e))
132                {
133                    self.graph.insert((si, c), Node { areas, dynamic });
134                    if cell.value == Value::Error(CellError::Circ) {
135                        circ.push((si, c));
136                    }
137                } else if cell.py.is_some() {
138                    // A Python cell lays out its kept result, so it is calculated like a formula
139                    // that reads nothing: its spill is blocked and unblocked like a formula's.
140                    self.graph.insert((si, c), Node { areas: Vec::new(), dynamic: false });
141                }
142            }
143        }
144        self.graph.settle();
145        self.circular = if wb.calc.iterative { Vec::new() } else { self.loops_among(&circ) };
146    }
147
148    /// The circular references of a rebuilt workbook (after undo, redo or a rollback, which put
149    /// back cells without calculating them), from the formulas whose cached value is `#CIRC!`:
150    /// the listed cells still on a loop, and one cell of each loop none of them is on.
151    fn loops_among(&self, circ: &[Key]) -> Vec<Key> {
152        if circ.is_empty() {
153            return Vec::new();
154        }
155        let n = circ.len();
156        let index: HashMap<Key, usize> = circ.iter().enumerate().map(|(i, k)| (*k, i)).collect();
157        // Edges from a cell to the `#CIRC!` formulas that read it; loops are the same either way.
158        let mut deps = Vec::new();
159        let edges: Vec<Vec<usize>> = circ
160            .iter()
161            .map(|k| {
162                deps.clear();
163                self.graph.dependents(k.0, k.1, &mut deps);
164                let mut e: Vec<usize> = deps.iter().filter_map(|d| index.get(d).copied()).collect();
165                e.sort_unstable();
166                e.dedup();
167                e
168            })
169            .collect();
170        // Loop members: nodes in a component of more than one, or with an edge to themselves.
171        let (comp, size) = components(&edges);
172        let on_loop = |i: usize| comp.get(i).and_then(|c| size.get(*c)).is_some_and(|s| *s > 1) || edges.get(i).is_some_and(|e| e.contains(&i));
173        // A formula with dynamic references (INDIRECT, OFFSET) may close a loop the graph can't see.
174        let mut out: Vec<Key> =
175            self.circular.iter().copied().filter(|k| index.get(k).is_some_and(|&i| on_loop(i) || self.graph.dynamic.contains(k))).collect();
176        let mut covered: HashSet<usize> = out.iter().filter_map(|k| index.get(k)).filter_map(|&i| comp.get(i).copied()).collect();
177        let mut order: Vec<usize> = (0..n).collect();
178        order.sort_by_key(|&i| circ.get(i).map(|(s, c)| (*s, c.row, c.col)));
179        for i in order {
180            if let (Some(&c), Some(&k)) = (comp.get(i), circ.get(i))
181                && on_loop(i)
182                && covered.insert(c)
183            {
184                out.push(k);
185            }
186        }
187        out
188    }
189
190    /// Recalculates every formula (F9 / Ctrl+Alt+F9, and after loading).
191    pub fn recalc_all(&mut self, wb: &mut Workbook) {
192        self.rebuild(wb);
193        // Spills are laid out again from the formulas where they are now (a structural edit moves
194        // or deletes the formulas, not the values they spilled).
195        for i in 0..wb.sheets.len() {
196            if wb.sheet(i).is_some_and(|s| !s.spill_ranges.is_empty() || !s.spill.is_empty())
197                && let Some(sh) = wb.sheet_mut(i)
198            {
199                sh.spill.clear();
200                sh.spill_ranges.clear();
201            }
202        }
203        let all: Vec<Key> = self.graph.nodes.keys().copied().collect();
204        self.run(wb, all, true);
205    }
206
207    /// Updates the graph for cells whose content changed and recalculates what depends on them.
208    pub fn cells_changed(&mut self, wb: &mut Workbook, changed: &[Key]) {
209        let mut cleared: Vec<(usize, RangeRef)> = Vec::new();
210        for &k in changed {
211            let precedent =
212                wb.sheet(k.0).and_then(|s| s.cell(k.1)).and_then(|c| c.formula.as_ref()).and_then(|f| f.with_expr(|e| precedents(wb, k.0, e)));
213            let py = wb.sheet(k.0).and_then(|s| s.cell(k.1)).and_then(|c| c.py.as_ref());
214            let python = py.is_some();
215            // A Python cell whose result (if any) is not an array spills nothing.
216            let spills = py.and_then(|p| p.result_value()).is_some_and(|v| matches!(v, Value::Array(_)));
217            match precedent {
218                Some((areas, dynamic)) => {
219                    self.graph.insert(k, Node { areas, dynamic });
220                }
221                // See `rebuild`: its spill is laid out again from its result when it is calculated.
222                None if python => {
223                    self.graph.insert(k, Node { areas: Vec::new(), dynamic: false });
224                    // New code has no result yet: the spill the cell had (its old result's, or a
225                    // formula's) goes, and what read it is recalculated, as for a formula
226                    // replaced by a constant.
227                    if !spills
228                        && let Some(sh) = wb.sheet_mut(k.0)
229                        && let Some(r) = sh.spill_ranges.remove(&k.1)
230                    {
231                        sh.clear_spill(r);
232                        cleared.push((k.0, r));
233                    }
234                }
235                None => {
236                    self.graph.remove(k);
237                    // A formula replaced by a constant (or cleared) takes its spilled values with it.
238                    if let Some(sh) = wb.sheet_mut(k.0)
239                        && let Some(r) = sh.spill_ranges.remove(&k.1)
240                    {
241                        for c in r.iter() {
242                            sh.spill.remove(&c);
243                        }
244                        cleared.push((k.0, r));
245                    }
246                    // A formula that doesn't parse evaluates to #NAME?.
247                    if let Some(cell) = wb.sheet_mut(k.0).and_then(|s| s.cells.get_mut(k.1))
248                        && cell.formula.is_some()
249                    {
250                        cell.value = Value::Error(CellError::Name);
251                    }
252                }
253            }
254        }
255        self.graph.settle();
256        if wb.calc.mode == CalcMode::Manual {
257            // Only the edited formulas themselves are evaluated.
258            let own: Vec<Key> = changed.iter().copied().filter(|k| self.graph.nodes.contains_key(k)).collect();
259            self.run(wb, own, false);
260            return;
261        }
262        let mut seeds: Vec<Key> = changed.to_vec();
263        let mut emptied: Vec<Key> = Vec::new();
264        // Typing into (or clearing) a cell a dynamic array spills over (or would spill over)
265        // re-evaluates the anchor, which may now be blocked or unblocked.
266        for &k in changed {
267            if let Some(sh) = wb.sheet(k.0) {
268                for (anchor, r) in &sh.spill_ranges {
269                    if *anchor != k.1 && r.contains(k.1) {
270                        seeds.push((k.0, *anchor));
271                    }
272                }
273                for (c, cell) in sh.cells.iter_range(RangeRef::new(CellRef::new(k.1.row.saturating_sub(64), k.1.col.saturating_sub(64)), k.1)) {
274                    if c != k.1 && cell.value == Value::Error(CellError::Spill) {
275                        seeds.push((k.0, c));
276                    }
277                }
278                if !sh.cells.has(k.1) {
279                    match emptied.iter_mut().find(|x| x.0 == k.0) {
280                        Some(x) => x.1 = CellRef::new(x.1.row.max(k.1.row), x.1.col.max(k.1.col)),
281                        None => emptied.push(k),
282                    }
283                }
284            }
285        }
286        // An emptied cell can unblock an anchor any distance above or to the left of it.
287        for (si, end) in emptied {
288            for &(s, c) in self.graph.nodes.keys() {
289                if s == si && c.row <= end.row && c.col <= end.col && wb.sheet(s).is_some_and(|sh| sh.value(c) == Value::Error(CellError::Spill)) {
290                    seeds.push((s, c));
291                }
292            }
293        }
294        // Spill areas of changed anchors also change. They stay rectangles: one seed per spilled
295        // cell listed the formulas reading the area once per cell.
296        let mut areas: Vec<(usize, RangeRef)> = cleared;
297        for &k in changed {
298            if let Some(r) = wb.sheet(k.0).and_then(|s| s.spill_ranges.get(&k.1)) {
299                areas.push((k.0, *r));
300            }
301        }
302        let t0 = prof_now();
303        let dirty = self.dirty_closure(wb, &seeds, &areas);
304        let t1 = prof_now();
305        let n = dirty.len();
306        self.run(wb, dirty, false);
307        if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
308            eprintln!("recalc: {} seeds, closure {} cells {:.1} ms, run {:.1} ms", seeds.len(), n, t1 - t0, prof_now() - t1);
309        }
310    }
311
312    /// The formulas to recalculate after `seeds` and every cell of `areas` changed: the seeds
313    /// that are formulas, and everything that reads them, directly or through other formulas.
314    fn dirty_closure(&self, wb: &Workbook, seeds: &[Key], areas: &[(usize, RangeRef)]) -> Vec<Key> {
315        let mut seen: HashSet<Key> = HashSet::default();
316        let mut queue: VecDeque<Key> = VecDeque::new();
317        // Each range-index entry is listed once in the whole walk: many changed cells under one
318        // broad reference (or many formulas inside one) would otherwise list every formula reading
319        // it once per cell, which squared the work.
320        let mut taken = Taken::default();
321        let mut deps = Vec::new();
322        for k in seeds {
323            if self.graph.nodes.contains_key(k) && seen.insert(*k) {
324                queue.push_back(*k);
325            }
326            deps.clear();
327            self.graph.dependents_untaken(k.0, k.1, &mut taken, &mut deps);
328            for d in deps.drain(..) {
329                if seen.insert(d) {
330                    queue.push_back(d);
331                }
332            }
333        }
334        for &(si, r) in areas {
335            if let Some(sh) = wb.sheet(si) {
336                for (c, _) in sh.cells.iter_range(r) {
337                    if self.graph.nodes.contains_key(&(si, c)) && seen.insert((si, c)) {
338                        queue.push_back((si, c));
339                    }
340                }
341            }
342            deps.clear();
343            self.graph.dependents_of_range(si, r, &mut deps);
344            for d in deps.drain(..) {
345                if seen.insert(d) {
346                    queue.push_back(d);
347                }
348            }
349        }
350        for k in &self.graph.dynamic {
351            if seen.insert(*k) {
352                queue.push_back(*k);
353            }
354        }
355        while let Some(k) = queue.pop_front() {
356            deps.clear();
357            self.graph.dependents_untaken(k.0, k.1, &mut taken, &mut deps);
358            for d in deps.drain(..) {
359                if seen.insert(d) {
360                    queue.push_back(d);
361                }
362            }
363        }
364        seen.into_iter().collect()
365    }
366
367    /// Evaluates the dirty set and writes results (and spills) back.
368    fn run(&mut self, wb: &mut Workbook, mut dirty: Vec<Key>, full: bool) {
369        let mut dynamic_again = !self.graph.dynamic.is_empty();
370        self.last_recalc_cells = 0;
371        for _round in 0..8 {
372            if dirty.is_empty() {
373                break;
374            }
375            // Row-major order is a good topological guess; on-demand evaluation fixes the rest.
376            dirty.sort_by_key(|(s, c)| (*s, c.row, c.col));
377            let mut exprs: HashMap<Key, std::sync::Arc<Expr>> = HashMap::with_capacity_and_hasher(dirty.len(), Default::default());
378            for k in &dirty {
379                if let Some(e) = wb.sheet(k.0).and_then(|s| s.cell(k.1)).and_then(|c| c.formula.as_ref()).and_then(|f| f.expr_arc()) {
380                    exprs.insert(*k, e);
381                }
382            }
383            let python: HashMap<Key, Value> = dirty
384                .iter()
385                .filter_map(|k| wb.sheet(k.0).and_then(|s| s.cell(k.1)).and_then(|c| c.py.as_ref()).and_then(|p| p.result_value()).map(|v| (*k, v)))
386                .collect();
387            let now = self.now();
388            let mut rng = self.rng;
389            let tp = prof_now();
390            let (results, spills, cycles) = {
391                let mut host = PassHost::new(wb, &exprs, &mut rng, now).with_python(python);
392                host.iterate = wb.calc.iterative;
393                for k in &dirty {
394                    host.calculate(*k);
395                }
396                if host.iterate && !host.members.is_empty() {
397                    let passes = host.iterate_loops(&dirty, wb.calc.max_iterations, wb.calc.max_change);
398                    if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
399                        eprintln!("  iterative calculation: {} passes over {} formulas", passes, host.tainted.len());
400                    }
401                }
402                (host.results, host.spills, host.cycles)
403            };
404            self.rng = rng;
405            if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
406                eprintln!("  eval {} formulas {:.1} ms", results.len(), prof_now() - tp);
407            }
408            let tw = prof_now();
409            self.last_recalc_cells += results.len();
410            // The loops this round finds replace the ones listed for the formulas it evaluated.
411            if !self.circular.is_empty() {
412                let evaluated: HashSet<Key> = dirty.iter().copied().collect();
413                self.circular.retain(|k| !evaluated.contains(k));
414            }
415            self.circular.extend(cycles);
416            let mut spill_changes = self.write_back(wb, &dirty, results, &spills);
417            // Formulas with references only known while evaluating (INDIRECT, OFFSET) may have read
418            // a spill area before it was laid out: they are evaluated once more.
419            if dynamic_again && !spills.is_empty() {
420                dynamic_again = false;
421                spill_changes.extend(self.graph.dynamic.iter().copied());
422            }
423            if full {
424                // Everything was already evaluated once; spill dependents need another pass.
425            }
426            if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
427                eprintln!("  write-back {:.1} ms", prof_now() - tw);
428            }
429            spill_changes.sort_by_key(|(s, c)| (*s, c.row, c.col));
430            spill_changes.dedup();
431            if spill_changes.is_empty() {
432                break;
433            }
434            dirty = self.dirty_closure(wb, &spill_changes, &[]);
435        }
436        // Loops among formulas the run did not evaluate stay listed while they are still formulas.
437        // Iterative calculation reports none.
438        if wb.calc.iterative {
439            self.circular.clear();
440        } else {
441            self.circular.retain(|k| self.graph.nodes.contains_key(k));
442        }
443    }
444
445    fn write_back(
446        &mut self,
447        wb: &mut Workbook,
448        dirty: &[Key],
449        results: HashMap<Key, Value>,
450        spills: &HashMap<Key, std::sync::Arc<Array>>,
451    ) -> Vec<Key> {
452        let mut spill_changes = Vec::new();
453        let mut freed = Vec::new();
454        for (key, value) in results {
455            let Some(sheet) = wb.sheet_mut(key.0) else { continue };
456            if let Some(cell) = sheet.cells.get_mut(key.1)
457                && cell.value != value
458            {
459                cell.value = value;
460            }
461        }
462        for key in dirty {
463            let new = spills.get(key);
464            let Some(sheet) = wb.sheet_mut(key.0) else { continue };
465            let old = sheet.spill_ranges.get(&key.1).copied();
466            if old.is_none() && new.is_none() {
467                continue;
468            }
469            // An unchanged spill does not alter any values its dependents read.
470            if let (Some(range), Some(array)) = (old, new)
471                && spills_as_before(sheet, key.1, range, array)
472            {
473                continue;
474            }
475            if let Some(range) = old {
476                for cell in range.iter() {
477                    sheet.spill.remove(&cell);
478                }
479                sheet.spill_ranges.remove(&key.1);
480            }
481            let mut changed_range = old;
482            if let Some(array) = new {
483                let end = CellRef::new(key.1.row.saturating_add(array.rows as u32 - 1), key.1.col.saturating_add(array.cols as u32 - 1));
484                let range = RangeRef::new(key.1, end);
485                let blocked = spill_is_blocked(sheet, key.1, range, end);
486                if blocked {
487                    if let Some(cell) = sheet.cells.get_mut(key.1) {
488                        cell.value = Value::Error(CellError::Spill);
489                    }
490                    let mut blocked_dependents = Vec::new();
491                    self.graph.dependents_of_range(key.0, range, &mut blocked_dependents);
492                    spill_changes.extend(blocked_dependents.into_iter().filter(|dependent| dependent != key));
493                } else {
494                    fill_spill(sheet, key.1, range, array);
495                    changed_range = Some(changed_range.map_or(range, |previous| previous.union(&range)));
496                }
497            }
498            if let Some(range) = changed_range {
499                let mut dependents = Vec::new();
500                self.graph.dependents_of_range(key.0, range, &mut dependents);
501                let freed_old_range = match (old, sheet.spill_ranges.get(&key.1)) {
502                    (Some(previous), Some(current)) => !(current.contains(previous.start) && current.contains(previous.end)),
503                    (_, None) => true,
504                    (None, Some(_)) => false,
505                };
506                spill_changes.extend(dependents.into_iter().filter(|dependent| dependent != key || freed_old_range));
507            }
508            if let Some(previous) = old
509                && !sheet.spill_ranges.get(&key.1).is_some_and(|current| current.contains(previous.start) && current.contains(previous.end))
510            {
511                freed.push((key.0, previous));
512            }
513        }
514        for (sheet_index, range) in freed {
515            for &(sheet, cell) in self.graph.nodes.keys() {
516                let was_blocked_cell = cell.row <= range.end.row
517                    && cell.col <= range.end.col
518                    && wb.sheet(sheet).is_some_and(|sheet| sheet.value(cell) == Value::Error(CellError::Spill));
519                if sheet == sheet_index && was_blocked_cell {
520                    spill_changes.push((sheet, cell));
521                }
522            }
523        }
524        spill_changes
525    }
526}
527
528fn spill_is_blocked(sheet: &crate::model::Sheet, anchor: CellRef, range: RangeRef, end: CellRef) -> bool {
529    end.row >= crate::base::MAX_ROWS
530        || end.col >= crate::base::MAX_COLS
531        || range.iter().any(|cell| cell != anchor && (sheet.cells.has(cell) || sheet.spill.contains_key(&cell)))
532        || sheet.merges.iter().any(|merge| merge.intersects(&range))
533        || sheet.tables.iter().any(|table| table.range.contains(anchor))
534}
535
536fn fill_spill(sheet: &mut crate::model::Sheet, anchor: CellRef, range: RangeRef, array: &Array) {
537    for (index, cell) in range.iter().enumerate() {
538        if cell != anchor {
539            sheet.spill.insert(cell, array.data.get(index).cloned().unwrap_or_default());
540        }
541    }
542    sheet.spill_ranges.insert(anchor, range);
543}
544
545/// Current date-time as a serial (1900 system), from the system clock (browser clock on wasm).
546/// UTC; the local offset isn't available without platform calls. Falls back to a fixed date only
547/// when no clock is available.
548pub fn now_serial() -> f64 {
549    crate::base::date::unix_now_ms().map_or(46302.5, crate::base::date::serial_from_unix_ms)
550}
551
552/// Evaluates a formula in the context of a cell without storing it (conditional formats, data
553/// validation, Evaluate Formula, Name Manager previews, the CLI `eval`).
554pub fn evaluate(wb: &Workbook, sheet: usize, at: CellRef, formula: &str) -> Value {
555    let body = formula.strip_prefix('=').unwrap_or(formula);
556    let Ok(expr) = crate::formula::parse(body) else { return Value::Error(CellError::Name) };
557    evaluate_expr(wb, sheet, at, &expr)
558}
559
560pub fn evaluate_expr(wb: &Workbook, sheet: usize, at: CellRef, expr: &Expr) -> Value {
561    let exprs = HashMap::default();
562    let mut rng = 0x1234_5678_9ABC_DEF0u64;
563    let mut host = PassHost::new(wb, &exprs, &mut rng, now_serial());
564    let mut ev = Evaluator::new(&mut host, sheet, at);
565    ev.value(expr)
566}
567
568fn prof_now() -> f64 {
569    crate::base::date::unix_now_ms().unwrap_or(0.0)
570}
571
572#[cfg(test)]
573pub(crate) mod tests {
574    use std::time::Duration;
575
576    use crate::base::{CellRef, RangeRef, Value};
577    use crate::model::{Cell, Formula, Workbook};
578
579    use super::{Calc, HashMap, Host, Intervals, Key, PassHost};
580
581    /// xorshift64 for repeatable pseudo-random cases.
582    fn rng(state: &mut u64) -> u64 {
583        *state ^= *state << 13;
584        *state ^= *state >> 7;
585        *state ^= *state << 17;
586        *state
587    }
588
589    fn found(iv: &Intervals<u32>, a: u32, b: u32) -> Vec<u32> {
590        let mut v = Vec::new();
591        iv.query(a, b, &mut |t| v.push(*t));
592        v.sort_unstable();
593        v
594    }
595
596    /// A 1,000 x 1,000 Python result laid out from A1 in a pass, with the same values already
597    /// spilled on the sheet from the last calculation, as a full recalculation finds them.
598    fn big_python_spill() -> (Workbook, HashMap<Key, Value>) {
599        let n = 1_000usize;
600        let data: Vec<Value> = (0..n * n).map(|i| Value::Number(i as f64)).collect();
601        let array = crate::base::Array::new(n, n, data.clone()).unwrap();
602        let mut wb = Workbook::new();
603        let sh = wb.sheet_mut(0).unwrap();
604        let anchor = CellRef::new(0, 0);
605        let py = crate::model::PyCode::new("big");
606        sh.set_cell(anchor, Cell { value: Value::Number(0.0), py: Some(std::sync::Arc::new(py)), ..Cell::default() });
607        let area = RangeRef::new(anchor, CellRef::new(n as u32 - 1, n as u32 - 1));
608        sh.spill_ranges.insert(anchor, area);
609        for (i, c) in area.iter().enumerate().skip(1) {
610            sh.spill.insert(c, data[i].clone());
611        }
612        let mut python = HashMap::default();
613        python.insert((0, anchor), Value::Array(std::sync::Arc::new(array)));
614        (wb, python)
615    }
616
617    #[test]
618    fn a_read_beside_a_large_spill_walks_none_of_it() {
619        let (wb, python) = big_python_spill();
620        let exprs = HashMap::default();
621        let mut rng = 1u64;
622        let mut host = PassHost::new(&wb, &exprs, &mut rng, 0.0).with_python(python);
623        assert_eq!(host.cell_value(0, CellRef::new(0, 0)), Value::Number(0.0));
624        // Beside the spill (column ALM, the 1,001st), over the rows it covers.
625        let beside = RangeRef::parse("ALM1:ALM1000").unwrap();
626        host.spill_visits = 0;
627        assert!(host.range_values(0, beside).iter().all(Value::is_empty));
628        assert!(host.visit_range(0, beside, &mut |_| true), "a disjoint read takes the fast path");
629        assert_eq!(host.spill_visits, 0, "nothing of the spill is walked");
630        // A read inside it still sees the spilled values, walking only the cells it reads.
631        let inside = RangeRef::parse("B2:C3").unwrap();
632        host.spill_visits = 0;
633        assert_eq!(host.range_values(0, inside), [1_001.0, 1_002.0, 2_001.0, 2_002.0].map(Value::Number).to_vec());
634        assert!(host.spill_visits <= 8, "{}", host.spill_visits);
635        assert!(!host.visit_range(0, inside, &mut |_| true), "an overlapping read leaves the block to range_values");
636    }
637
638    #[test]
639    fn intervals_match_a_linear_scan() {
640        let mut iv: Intervals<u32> = Intervals::default();
641        let mut model: Vec<(u32, u32, u32)> = Vec::new();
642        let mut s = 0x9E37_79B9_7F4A_7C15;
643        for step in 0..20_000u32 {
644            let roll = rng(&mut s) % 10;
645            if roll < 6 || model.is_empty() {
646                let r0 = (rng(&mut s) % 500) as u32;
647                let r1 = r0 + (rng(&mut s) % 40) as u32;
648                if !model.contains(&(r0, r1, step)) {
649                    iv.insert(r0, r1, step);
650                    model.push((r0, r1, step));
651                }
652            } else {
653                let i = (rng(&mut s) % model.len() as u64) as usize;
654                let (r0, r1, t) = model.swap_remove(i);
655                iv.remove(r0, r1, t);
656                if roll == 9 {
657                    // Removed and put back before the next merge.
658                    iv.insert(r0, r1, t);
659                    model.push((r0, r1, t));
660                }
661            }
662            if rng(&mut s).is_multiple_of(50) {
663                iv.settle();
664            }
665            if step.is_multiple_of(97) {
666                let a = (rng(&mut s) % 560) as u32;
667                let b = a + (rng(&mut s) % 30) as u32;
668                let mut want: Vec<u32> = model.iter().filter(|(r0, r1, _)| *r0 <= b && *r1 >= a).map(|(_, _, t)| *t).collect();
669                want.sort_unstable();
670                assert_eq!(found(&iv, a, b), want, "rows {a}..={b} at step {step}");
671            }
672        }
673        iv.settle();
674        for row in 0..560 {
675            let mut want: Vec<u32> = model.iter().filter(|(r0, r1, _)| *r0 <= row && row <= *r1).map(|(_, _, t)| *t).collect();
676            want.sort_unstable();
677            assert_eq!(found(&iv, row, row), want, "row {row}");
678        }
679        for (r0, r1, t) in std::mem::take(&mut model) {
680            iv.remove(r0, r1, t);
681        }
682        iv.settle();
683        assert!(iv.is_empty());
684    }
685
686    #[test]
687    fn interval_queries_cost_log_n_plus_matches() {
688        // 40k one-row ranges (like =SUM(E1:J1) down a column) and 1k ranges over everything:
689        // a point query visits O(log n) tree nodes per match, not every range in the column.
690        let n = 40_000u32;
691        let mut iv: Intervals<u32> = Intervals::default();
692        for r in 0..n {
693            iv.insert(r, r, r);
694        }
695        for t in 0..1_000 {
696            iv.insert(0, n, n + t);
697        }
698        iv.settle();
699        let mut worst = 0;
700        for row in (0..n).step_by(7) {
701            let mut k = 0;
702            let visited = iv.query(row, row, &mut |_| k += 1);
703            assert_eq!(k, 1_001);
704            worst = worst.max(visited);
705        }
706        // 1,001 matches in a tree of depth 16: well under 4 nodes per match plus the path.
707        assert!(worst < 4 * 1_001 + 64, "visited {worst} nodes");
708        let mut k = 0;
709        let visited = iv.query(n + 1, n + 5, &mut |_| k += 1);
710        assert_eq!(k, 0);
711        assert!(visited < 64, "visited {visited} nodes for no match");
712    }
713
714    #[test]
715    fn untaken_queries_list_each_interval_once_in_all() {
716        // 1,000 ranges over every row of 5,000: point queries at every row used to list all 1,000
717        // each time (5 million matches); sharing `taken`, each range is listed once in all.
718        let n = 5_000u32;
719        let mut iv: Intervals<u32> = Intervals::default();
720        for t in 0..1_000 {
721            iv.insert(0, n, t);
722        }
723        for r in 0..n {
724            iv.insert(r, r, 10_000 + r);
725        }
726        iv.settle();
727        let mut taken = Vec::new();
728        let mut listed = Vec::new();
729        for row in 0..=n + 3 {
730            iv.query_untaken(row, row, &mut taken, &mut |t| {
731                listed.push(*t);
732                true
733            });
734        }
735        listed.sort_unstable();
736        let want: Vec<u32> = (0..1_000).chain(10_000..10_000 + n).collect();
737        assert_eq!(listed, want);
738        // An entry the caller declines is offered again.
739        let mut taken = Vec::new();
740        let mut offered = 0;
741        for _ in 0..3 {
742            iv.query_untaken(7, 7, &mut taken, &mut |t| {
743                if *t == 10_007 {
744                    offered += 1;
745                }
746                *t != 10_007
747            });
748        }
749        assert_eq!(offered, 3);
750    }
751
752    #[test]
753    fn dirty_closure_matches_a_walk_of_every_cell() {
754        // Random formulas over single cells, narrow ranges and wide (over 64 columns) ranges, some
755        // removed or left unsettled: the closure equals the one found by listing every dependent
756        // of every changed cell, area cells included, as before `taken` and areas existed.
757        use super::{HashSet, Node};
758        use crate::calc::eval::Area;
759        use std::collections::VecDeque;
760        let mut s = 0x2545_F491_4F6C_DD1D;
761        for case in 0..40 {
762            let mut wb = Workbook::new();
763            let mut calc = Calc::new();
764            let pick = |s: &mut u64, n: u64| (rng(s) % n) as u32;
765            let mut keys: Vec<Key> = Vec::new();
766            for _ in 0..300 {
767                let k = (0, CellRef::new(pick(&mut s, 120), pick(&mut s, 150)));
768                let mut areas = Vec::new();
769                for _ in 0..1 + pick(&mut s, 3) {
770                    let start = CellRef::new(pick(&mut s, 120), pick(&mut s, 150));
771                    let end = match pick(&mut s, 3) {
772                        0 => start,
773                        1 => CellRef::new(start.row + pick(&mut s, 40), start.col + pick(&mut s, 5)),
774                        _ => CellRef::new(start.row + pick(&mut s, 20), start.col + 65 + pick(&mut s, 30)),
775                    };
776                    areas.push(Area { sheet: 0, range: RangeRef::new(start, end) });
777                }
778                if let Some(sh) = wb.sheet_mut(0) {
779                    sh.cells.set(k.1, Cell::formula(Formula::new("=1")));
780                }
781                calc.graph.insert(k, Node { areas, dynamic: false });
782                keys.push(k);
783                if rng(&mut s).is_multiple_of(7) {
784                    calc.graph.settle();
785                }
786            }
787            for _ in 0..30 {
788                let k = keys[(rng(&mut s) % keys.len() as u64) as usize];
789                calc.graph.remove(k);
790                if let Some(sh) = wb.sheet_mut(0) {
791                    sh.cells.remove(k.1);
792                }
793            }
794            if case % 2 == 0 {
795                calc.graph.settle();
796            }
797            let seeds: Vec<Key> = (0..1 + pick(&mut s, 25)).map(|_| (0, CellRef::new(pick(&mut s, 140), pick(&mut s, 220)))).collect();
798            let areas: Vec<(usize, RangeRef)> = (0..pick(&mut s, 3))
799                .map(|_| {
800                    let start = CellRef::new(pick(&mut s, 120), pick(&mut s, 150));
801                    (0, RangeRef::new(start, CellRef::new(start.row + pick(&mut s, 30), start.col + pick(&mut s, 80))))
802                })
803                .collect();
804            let got: HashSet<Key> = calc.dirty_closure(&wb, &seeds, &areas).into_iter().collect();
805            // The walk before: every area cell is a seed, and every cell lists all its dependents.
806            let mut all = seeds.clone();
807            for (si, r) in &areas {
808                all.extend(r.iter().map(|c| (*si, c)));
809            }
810            let mut want: HashSet<Key> = HashSet::default();
811            let mut queue: VecDeque<Key> = VecDeque::new();
812            let mut deps = Vec::new();
813            for k in &all {
814                if calc.graph.nodes.contains_key(k) && want.insert(*k) {
815                    queue.push_back(*k);
816                }
817                deps.clear();
818                calc.graph.dependents(k.0, k.1, &mut deps);
819                queue.extend(deps.iter().copied().filter(|d| want.insert(*d)));
820            }
821            while let Some(k) = queue.pop_front() {
822                deps.clear();
823                calc.graph.dependents(k.0, k.1, &mut deps);
824                queue.extend(deps.iter().copied().filter(|d| want.insert(*d)));
825            }
826            assert_eq!(got, want, "case {case}");
827        }
828    }
829
830    #[test]
831    fn clearing_a_spill_recalculates_formulas_reading_it_once_each() {
832        // 300 formulas read the area an anchor spills into; clearing the anchor changes all of it
833        // and every one of them is recalculated (they were listed once per spilled cell).
834        let mut wb = Workbook::new();
835        let mut calc = Calc::new();
836        let set = |wb: &mut Workbook, calc: &mut Calc, at: CellRef, input: &str| {
837            if let Some(sh) = wb.sheet_mut(0) {
838                if input.is_empty() {
839                    sh.cells.remove(at);
840                } else {
841                    sh.cells.set(at, Cell::formula(Formula::new(input)));
842                }
843            }
844            calc.cells_changed(wb, &[(0, at)]);
845        };
846        set(&mut wb, &mut calc, CellRef::new(0, 0), "=SEQUENCE(2000)");
847        for r in 0..300 {
848            set(&mut wb, &mut calc, CellRef::new(r, 2), "=COUNT(A1:A2000)");
849        }
850        assert_eq!(wb.sheet(0).map(|s| s.value(CellRef::new(299, 2))), Some(Value::Number(2000.0)));
851        set(&mut wb, &mut calc, CellRef::new(0, 0), "");
852        for r in 0..300 {
853            assert_eq!(wb.sheet(0).map(|s| s.value(CellRef::new(r, 2))), Some(Value::Number(0.0)), "row {r}");
854        }
855    }
856
857    /// `rows` rows of six numbers (E:J) and `=SUM(E{r}:J{r})` in K.
858    fn sum_rows(rows: u32) -> Workbook {
859        let mut wb = Workbook::new();
860        if let Some(sh) = wb.sheet_mut(0) {
861            for r in 0..rows {
862                for c in 4..10 {
863                    sh.cells.set(CellRef::new(r, c), Cell::value(Value::Number(f64::from(r % 97 + c))));
864                }
865                sh.cells.set(CellRef::new(r, 10), Cell::formula(Formula::new(&format!("=SUM(E{0}:J{0})", r + 1))));
866            }
867        }
868        wb
869    }
870
871    /// Opening (full recalc), changing every value of a column, and re-entering every formula.
872    fn open_and_bulk_edit(rows: u32) -> Duration {
873        let mut wb = sum_rows(rows);
874        let mut calc = Calc::new();
875        let started = crate::base::date::unix_now_ms().unwrap_or(0.0);
876        calc.recalc_all(&mut wb);
877        let values: Vec<Key> = (0..rows).map(|r| (0, CellRef::new(r, 4))).collect();
878        if let Some(sh) = wb.sheet_mut(0) {
879            for (_, c) in &values {
880                sh.cells.set(*c, Cell::value(Value::Number(1.0)));
881            }
882        }
883        calc.cells_changed(&mut wb, &values);
884        let formulas: Vec<Key> = (0..rows).map(|r| (0, CellRef::new(r, 10))).collect();
885        calc.cells_changed(&mut wb, &formulas);
886        let ended = crate::base::date::unix_now_ms().unwrap_or(started);
887        let elapsed = Duration::from_secs_f64((ended - started).max(0.0) / 1000.0);
888        let last = wb.sheet(0).map(|s| s.value(CellRef::new(rows - 1, 10)));
889        let r = rows - 1;
890        assert_eq!(last, Some(Value::Number(f64::from(1 + (5..10).map(|c| r % 97 + c).sum::<u32>()))));
891        elapsed
892    }
893
894    #[test]
895    fn range_formulas_recalc_and_bulk_edit_correctly() {
896        // The values behind the timing test below, checked on every run.
897        open_and_bulk_edit(500);
898    }
899
900    #[test]
901    #[ignore = "timing-sensitive; run with --ignored in release"]
902    fn recalc_with_range_formulas_scales_linearly() {
903        // Each row's SUM reads a range in the same columns. Recalculating on open used to walk
904        // every row per range read (the used range), and finding a cell's dependents scanned
905        // every range in its column: 4x the rows took 16x the time. Linear is 4x; allow 8x.
906        let best = |rows| (0..3).map(|_| open_and_bulk_edit(rows)).min().unwrap_or_default();
907        let small = best(5_000);
908        let large = best(20_000);
909        assert!(large < small * 8 + Duration::from_millis(100), "5k rows {small:?}, 20k rows {large:?}");
910    }
911
912    #[test]
913    fn range_dependents_after_edits() {
914        // Formulas that read ranges, wide ranges and whole columns, found again after edits.
915        let mut wb = Workbook::new();
916        let mut calc = Calc::new();
917        let set = |wb: &mut Workbook, calc: &mut Calc, at: &str, input: &str| {
918            let c = CellRef::parse(at).unwrap();
919            let cell = if input.starts_with('=') { Cell::formula(Formula::new(input)) } else { Cell::value(Value::Number(input.parse().unwrap())) };
920            wb.sheet_mut(0).unwrap().cells.set(c, cell);
921            calc.cells_changed(wb, &[(0, c)]);
922        };
923        let num = |wb: &Workbook, at: &str| {
924            let v = wb.sheet(0).unwrap().value(CellRef::parse(at).unwrap());
925            v.as_f64().unwrap_or_else(|| panic!("{at} = {v:?}"))
926        };
927        set(&mut wb, &mut calc, "A1", "1");
928        set(&mut wb, &mut calc, "B2", "2");
929        set(&mut wb, &mut calc, "CZ3", "3");
930        set(&mut wb, &mut calc, "DA5", "=SUM(A1:B2)");
931        set(&mut wb, &mut calc, "DA6", "=SUM(A1:CZ3)");
932        set(&mut wb, &mut calc, "DA7", "=SUM(B:B)");
933        set(&mut wb, &mut calc, "DA8", "=SUM(2:2)");
934        assert_eq!((num(&wb, "DA5"), num(&wb, "DA6"), num(&wb, "DA7"), num(&wb, "DA8")), (3.0, 6.0, 2.0, 2.0));
935        set(&mut wb, &mut calc, "B2", "20");
936        assert_eq!((num(&wb, "DA5"), num(&wb, "DA6"), num(&wb, "DA7"), num(&wb, "DA8")), (21.0, 24.0, 20.0, 20.0));
937        // Re-entering a formula with another range drops the old one.
938        set(&mut wb, &mut calc, "DA5", "=SUM(A1:A2)");
939        set(&mut wb, &mut calc, "B2", "5");
940        assert_eq!(num(&wb, "DA5"), 1.0);
941        let mut deps = Vec::new();
942        calc.graph.dependents(0, CellRef::parse("B2").unwrap(), &mut deps);
943        deps.sort_unstable();
944        let at = |s: &str| (0, CellRef::parse(s).unwrap());
945        assert_eq!(deps, vec![at("DA6"), at("DA7"), at("DA8")]);
946        deps.clear();
947        calc.graph.dependents_of_range(0, RangeRef::new(CellRef::parse("A1").unwrap(), CellRef::parse("A9").unwrap()), &mut deps);
948        deps.sort_unstable();
949        deps.dedup();
950        assert_eq!(deps, vec![at("DA5"), at("DA6"), at("DA8")]);
951        set(&mut wb, &mut calc, "CZ3", "30");
952        assert_eq!(num(&wb, "DA6"), 36.0);
953        // Ranges on another sheet.
954        wb.sheets.push(std::sync::Arc::new(crate::model::Sheet::new("Data")));
955        set(&mut wb, &mut calc, "DA9", "=SUM(Data!A1:A3)+SUM(Data!A:CZ)");
956        assert_eq!(num(&wb, "DA9"), 0.0);
957        wb.sheet_mut(1).unwrap().cells.set(CellRef::parse("A2").unwrap(), Cell::value(Value::Number(4.0)));
958        calc.cells_changed(&mut wb, &[(1, CellRef::parse("A2").unwrap())]);
959        assert_eq!(num(&wb, "DA9"), 8.0);
960        deps.clear();
961        calc.graph.dependents(1, CellRef::parse("A2").unwrap(), &mut deps);
962        assert_eq!(deps, vec![at("DA9"), at("DA9")]);
963    }
964
965    /// A formula at the nesting limit reading thousands of formulas not calculated yet suspends the
966    /// evaluations in progress once, not once per formula read.
967    #[test]
968    fn wide_read_at_the_nesting_limit_suspends_once() {
969        const WIDTH: u32 = 3000;
970        let mut wb = Workbook::new();
971        let mut exprs = HashMap::default();
972        let mut put = |wb: &mut Workbook, at: String, text: String| {
973            let at = CellRef::parse(&at).unwrap();
974            wb.sheet_mut(0).unwrap().cells.set(at, Cell::formula(Formula::new(&text)));
975            exprs.insert((0, at), std::sync::Arc::new(crate::formula::parse(&text[1..]).unwrap()));
976        };
977        // A1 reads A2, ..., A64 (the 64th formula deep) reads a wide range of formulas.
978        for r in 1..64 {
979            put(&mut wb, format!("A{r}"), format!("=A{}+1", r + 1));
980        }
981        put(&mut wb, "A64".to_string(), format!("=SUM(B1:B{WIDTH})"));
982        for r in 1..=WIDTH {
983            put(&mut wb, format!("B{r}"), format!("={r}*2"));
984        }
985        let mut rng = 1u64;
986        let mut host = PassHost::new(&wb, &exprs, &mut rng, 0.0);
987        host.calculate((0, CellRef::parse("A1").unwrap()));
988        let total = (WIDTH * (WIDTH + 1)) as f64;
989        assert_eq!(host.results.get(&(0, CellRef::parse("A1").unwrap())), Some(&Value::Number(total + 63.0)));
990        assert!(host.cycles.is_empty());
991        assert!(host.suspensions <= 3, "{} suspensions", host.suspensions);
992    }
993
994    /// For tests of `PassHost::visit_range`, which can't be reached from outside this module: the
995    /// values it hands over for `range` while the formulas in `pending` (cell, formula) have not been
996    /// calculated yet, or `None` when it declines the block.
997    pub(crate) fn probe_visit_range(wb: &Workbook, pending: &[(&str, &str)], range: RangeRef) -> Option<Vec<Value>> {
998        let mut exprs = HashMap::default();
999        for (at, formula) in pending {
1000            let expr = crate::formula::parse(formula.strip_prefix('=').unwrap_or(formula)).ok()?;
1001            exprs.insert((0, CellRef::parse(at)?), std::sync::Arc::new(expr));
1002        }
1003
1004        // A cell in `pending` is calculated the first time something reads it, which is the state
1005        // of a real pass partway through; `visit_range` has to cope with the formulas it reads
1006        // being calculated under it.
1007        let mut rng = 1u64;
1008        let mut host = PassHost::new(wb, &exprs, &mut rng, 0.0);
1009
1010        let mut seen = Vec::new();
1011        host.visit_range(0, range, &mut |v| {
1012            seen.push(v.clone());
1013            true
1014        })
1015        .then_some(seen)
1016    }
1017}