Skip to main content

vla_lang/calc/
graph.rs

1//! The dependency graph and the order of evaluation (KERNEL.7,
2//! 2026-10-08): from `refers`, as the roadmap says.
3//!
4//! Every formula cell is a node; an edge runs from a formula to each
5//! formula cell it reads, through a cell, a range, a whole column or row,
6//! a 3D span, or a defined name whose text reads them. The scanner's
7//! records (`refers::scan`) are the edges' source, so the graph follows the
8//! refers golden and nothing else; a construct the parser does not read
9//! adds no edge, and the cell that holds it is not computed. Tarjan's
10//! algorithm over the graph finds the strongly connected components in
11//! reverse topological order, which with edges pointing from a reader to
12//! what it reads is the order a dependency is computed before the formula
13//! that needs it: the evaluation order. A component of more than one cell,
14//! or one cell that reads itself, is a cycle, refused by name with the
15//! cells that close it (`calc-cycle`), as `Alonzo/SPEC.md` section 2 asks
16//! for the engine's `load` and the treaty's recalculation refuses alike.
17//! The walk is iterative: a running total down ten thousand rows is a
18//! chain ten thousand deep, which a recursive walk would not survive.
19
20use std::collections::btree_map::Range;
21use std::collections::HashMap;
22
23use crate::intrinsics::fold;
24use crate::refers::{self, Kind};
25use crate::sheet::{Cell, Content, Sheet, Workbook};
26
27use super::formula::{ref_expr, RefExpr};
28use super::value::ErrorKind;
29
30/// A cell by its sheet's tab index, its row and its column, 1-based.
31pub type CellId = (usize, u32, u32);
32
33/// The used rectangle of a sheet, as `Sheet::extent` gives it.
34pub type Extent = Option<((u32, u32), (u32, u32))>;
35
36/// A rectangle of cells on one sheet, rows and columns 1-based and
37/// inclusive; a whole column runs to the sheet's last row, a whole row to
38/// its last column, and the walk clamps either to the sheet's extent.
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub struct Area {
41    pub sheet: usize,
42    pub top: u32,
43    pub left: u32,
44    pub bottom: u32,
45    pub right: u32,
46}
47
48impl Area {
49    /// Whether the area is one cell.
50    pub fn is_cell(&self) -> bool {
51        self.top == self.bottom && self.left == self.right
52    }
53}
54
55/// The areas a reference names, read as a slice: one area held in place, or
56/// a 3D span's, one a sheet, in a vector (KERNEL.25: a reference to one
57/// sheet, which every evaluation of nearly every formula makes, allocates
58/// nothing).
59#[derive(Clone, Debug)]
60pub struct Areas(Held);
61
62#[derive(Clone, Debug)]
63enum Held {
64    One(Area),
65    Span(Vec<Area>),
66}
67
68impl Areas {
69    /// One area.
70    pub fn one(area: Area) -> Areas {
71        Areas(Held::One(area))
72    }
73}
74
75impl From<Vec<Area>> for Areas {
76    fn from(areas: Vec<Area>) -> Areas {
77        match areas.as_slice() {
78            [area] => Areas::one(*area),
79            _ => Areas(Held::Span(areas)),
80        }
81    }
82}
83
84impl std::ops::Deref for Areas {
85    type Target = [Area];
86
87    fn deref(&self) -> &[Area] {
88        match &self.0 {
89            Held::One(area) => std::slice::from_ref(area),
90            Held::Span(areas) => areas,
91        }
92    }
93}
94
95impl<'a> IntoIterator for &'a Areas {
96    type Item = &'a Area;
97    type IntoIter = std::slice::Iter<'a, Area>;
98
99    fn into_iter(self) -> Self::IntoIter {
100        self.iter()
101    }
102}
103
104/// Two lists of areas are equal when they read the same, however held.
105impl PartialEq for Areas {
106    fn eq(&self, other: &Areas) -> bool {
107        **self == **other
108    }
109}
110
111impl Eq for Areas {}
112
113/// The defined names of a workbook, each with the sheet that scopes it and
114/// its text, as the reader prints a `name` row (`Model!Local` for a
115/// sheet-scoped name) and as the writer holds one.
116pub struct Names {
117    entries: Vec<NameEntry>,
118}
119
120struct NameEntry {
121    key: String,
122    scope: Option<usize>,
123    text: String,
124}
125
126impl Names {
127    /// The workbook's names, read off `Workbook::defined_names`.
128    pub fn of(model: &Workbook) -> Names {
129        let entries = model
130            .defined_names
131            .iter()
132            .map(|(printed, text)| {
133                let (scope, bare) = split_scope(model, printed);
134                NameEntry {
135                    key: fold(bare),
136                    scope,
137                    text: text.clone(),
138                }
139            })
140            .collect();
141        Names { entries }
142    }
143
144    /// The text of the name a formula on `home` reads by `name`: the name
145    /// scoped to the sheet written in front of it when one is, else the
146    /// one scoped to the formula's own sheet, else the workbook's.
147    pub fn resolve(&self, name: &str, scope: Option<usize>, home: usize) -> Option<&str> {
148        let key = fold(name);
149        let scoped = |s: usize| {
150            self.entries
151                .iter()
152                .find(|e| e.key == key && e.scope == Some(s))
153        };
154        let global = || {
155            self.entries
156                .iter()
157                .find(|e| e.key == key && e.scope.is_none())
158        };
159        let entry = match scope {
160            Some(s) => scoped(s).or_else(global),
161            None => scoped(home).or_else(global),
162        };
163        entry.map(|e| e.text.as_str())
164    }
165
166    /// How many names there are.
167    pub fn len(&self) -> usize {
168        self.entries.len()
169    }
170
171    /// Whether there is none.
172    pub fn is_empty(&self) -> bool {
173        self.entries.is_empty()
174    }
175}
176
177/// `Model!Local` or `'Q1 Data'!Local` as its scope and its bare name; a
178/// name with no `!` is the workbook's.
179fn split_scope<'a>(model: &Workbook, printed: &'a str) -> (Option<usize>, &'a str) {
180    let Some(bang) = printed.rfind('!') else {
181        return (None, printed);
182    };
183    let (sheet_part, bare) = (&printed[..bang], &printed[bang + 1..]);
184    let sheet =
185        if sheet_part.len() >= 2 && sheet_part.starts_with('\'') && sheet_part.ends_with('\'') {
186            sheet_part[1..sheet_part.len() - 1].replace("''", "'")
187        } else {
188            sheet_part.to_string()
189        };
190    (model.find_sheet(&sheet), bare)
191}
192
193/// The areas a reference names against the model: one per sheet of a
194/// span, the formula's own sheet when none is written. A sheet the model
195/// does not hold is `#REF!`.
196pub fn areas_of(model: &Workbook, home: usize, r: &RefExpr) -> Result<Areas, ErrorKind> {
197    areas_at(model, home, r, 0, 0)
198}
199
200/// The areas a reference names from a cell `d_row` rows down and `d_col`
201/// columns right of the one its formula was read at (KERNEL.22, a shape
202/// evaluated at an offset): [`RefExpr::moved`]'s rectangle on each sheet,
203/// `#REF!` when a corner leaves the sheet, as the moved text would read.
204/// A reference whose sheets its placement found reads them as found
205/// (KERNEL.20); any other finds them by name.
206pub fn areas_at(
207    model: &Workbook,
208    home: usize,
209    r: &RefExpr,
210    d_row: i64,
211    d_col: i64,
212) -> Result<Areas, ErrorKind> {
213    let (first, last) = match r.found {
214        Some((first, last)) => (usize::from(first), usize::from(last)),
215        None => {
216            let first = match &r.sheet {
217                None => home,
218                Some(name) => model.find_sheet(name).ok_or(ErrorKind::Ref)?,
219            };
220            let last = match &r.last_sheet {
221                None => first,
222                Some(name) => model.find_sheet(name).ok_or(ErrorKind::Ref)?,
223            };
224            (first, last)
225        }
226    };
227    let (top, left, bottom, right) = r.moved(d_row, d_col).ok_or(ErrorKind::Ref)?;
228    let area = |sheet| Area {
229        sheet,
230        top,
231        left,
232        bottom,
233        right,
234    };
235    if first == last {
236        return Ok(Areas::one(area(first)));
237    }
238    let (a, b) = (first.min(last), first.max(last));
239    Ok(Areas::from((a..=b).map(area).collect::<Vec<Area>>()))
240}
241
242/// The cells a sheet holds inside an area, in row-major order, the area
243/// clamped to the sheet's extent first so that a whole column costs its
244/// rows and not Excel's million. Walked as it is read and never collected
245/// (KERNEL.25): a range read by every evaluation of a formula cost a vector
246/// each time.
247pub fn cells_in<'a>(
248    sheet: &'a Sheet,
249    extent: Extent,
250    area: &Area,
251) -> impl Iterator<Item = ((u32, u32), &'a Cell)> + 'a {
252    let Some(((e_top, e_left), (e_bottom, e_right))) = extent else {
253        return Walk::Done;
254    };
255    let top = area.top.max(e_top);
256    let bottom = area.bottom.min(e_bottom);
257    let left = area.left.max(e_left);
258    let right = area.right.min(e_right);
259    if top > bottom || left > right {
260        return Walk::Done;
261    }
262    let rows = usize::try_from(bottom - top + 1).unwrap_or(usize::MAX);
263    if rows > sheet.cells.len() {
264        // Fewer cells than rows: walk the cells and keep those inside.
265        return Walk::Sparse {
266            cells: sheet.cells.range((top, 0)..=(bottom, u32::MAX)),
267            left,
268            right,
269        };
270    }
271    Walk::Rows {
272        sheet,
273        row: top,
274        bottom,
275        left,
276        right,
277        cells: sheet.cells.range((top, left)..=(top, right)),
278    }
279}
280
281/// [`cells_in`]'s two walks: the sheet's cells between the area's first and
282/// last rows, those in its columns kept; or the area row by row, a range of
283/// the sheet's cells a row.
284enum Walk<'a> {
285    Done,
286    Sparse {
287        cells: Range<'a, (u32, u32), Cell>,
288        left: u32,
289        right: u32,
290    },
291    Rows {
292        sheet: &'a Sheet,
293        row: u32,
294        bottom: u32,
295        left: u32,
296        right: u32,
297        cells: Range<'a, (u32, u32), Cell>,
298    },
299}
300
301impl<'a> Iterator for Walk<'a> {
302    type Item = ((u32, u32), &'a Cell);
303
304    fn next(&mut self) -> Option<Self::Item> {
305        match self {
306            Walk::Done => None,
307            Walk::Sparse { cells, left, right } => {
308                let (left, right) = (*left, *right);
309                cells
310                    .find(|((_, col), _)| *col >= left && *col <= right)
311                    .map(|(k, c)| (*k, c))
312            }
313            Walk::Rows {
314                sheet,
315                row,
316                bottom,
317                left,
318                right,
319                cells,
320            } => loop {
321                if let Some((k, c)) = cells.next() {
322                    return Some((*k, c));
323                }
324                if *row >= *bottom {
325                    return None;
326                }
327                *row += 1;
328                *cells = sheet.cells.range((*row, *left)..=(*row, *right));
329            },
330        }
331    }
332}
333
334/// Whether a cell's content is a formula of any kind.
335pub fn is_formula(content: &Content) -> bool {
336    matches!(
337        content,
338        Content::Formula(_)
339            | Content::SharedMaster { .. }
340            | Content::SharedChild { .. }
341            | Content::DynamicFormula(_)
342    )
343}
344
345/// How deep a name may read a name.
346const NAME_DEPTH: u32 = 8;
347
348/// The formula cells a formula's text reads, through its references and
349/// the names they resolve to, in formula order, repeats kept.
350pub fn dependencies(
351    model: &Workbook,
352    names: &Names,
353    extents: &[Extent],
354    home: usize,
355    text: &str,
356    out: &mut Vec<CellId>,
357) {
358    collect(model, names, extents, home, text, 0, out);
359}
360
361fn collect(
362    model: &Workbook,
363    names: &Names,
364    extents: &[Extent],
365    home: usize,
366    text: &str,
367    depth: u32,
368    out: &mut Vec<CellId>,
369) {
370    for r in refers::scan(text) {
371        match r.kind {
372            Kind::Cell | Kind::Range | Kind::Column | Kind::Row | Kind::ThreeD => {
373                let Some(re) = ref_expr(&r) else { continue };
374                let Ok(areas) = areas_of(model, home, &re) else {
375                    continue;
376                };
377                for a in &areas {
378                    let sheet = &model.sheets[a.sheet];
379                    for ((row, col), cell) in cells_in(sheet, extents[a.sheet], a) {
380                        if is_formula(&cell.content) {
381                            out.push((a.sheet, row, col));
382                        }
383                    }
384                }
385            }
386            Kind::Name if depth < NAME_DEPTH => {
387                let scope = if r.sheet_name.is_empty() {
388                    None
389                } else {
390                    model.find_sheet(&r.sheet_name)
391                };
392                if let Some(t) = names.resolve(&r.part, scope, home) {
393                    collect(
394                        model,
395                        names,
396                        extents,
397                        scope.unwrap_or(home),
398                        t,
399                        depth + 1,
400                        out,
401                    );
402                }
403            }
404            _ => {}
405        }
406    }
407}
408
409/// The plan: every formula cell either in the evaluation order, each after
410/// everything it reads, or in a cycle.
411#[derive(Clone, Debug, Default, PartialEq, Eq)]
412pub struct Plan {
413    /// The formula cells with no cycle through them, dependencies first.
414    pub order: Vec<CellId>,
415    /// Each cycle's cells, sorted; the cycles sorted by their first cell.
416    pub cycles: Vec<Vec<CellId>>,
417}
418
419impl Plan {
420    /// How many formula cells the plan covers, in the order or in a cycle.
421    pub fn formulas(&self) -> usize {
422        self.order.len() + self.cycles.iter().map(Vec::len).sum::<usize>()
423    }
424}
425
426/// The plan over the formula cells given, each with its text (without its
427/// `=`), against the model and its names.
428pub fn plan(
429    model: &Workbook,
430    names: &Names,
431    extents: &[Extent],
432    texts: &HashMap<CellId, String>,
433) -> Plan {
434    let mut ids: Vec<CellId> = texts.keys().copied().collect();
435    ids.sort_unstable();
436    let index: HashMap<CellId, usize> = ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
437    let n = ids.len();
438    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
439    let mut deps = Vec::new();
440    for (vi, id) in ids.iter().enumerate() {
441        deps.clear();
442        dependencies(model, names, extents, id.0, &texts[id], &mut deps);
443        let edges = &mut adj[vi];
444        for d in &deps {
445            if let Some(&di) = index.get(d) {
446                edges.push(di);
447            }
448        }
449        edges.sort_unstable();
450        edges.dedup();
451    }
452    let components = tarjan(&adj);
453    let mut plan = Plan::default();
454    for comp in components {
455        if comp.len() == 1 && !adj[comp[0]].contains(&comp[0]) {
456            plan.order.push(ids[comp[0]]);
457        } else {
458            let mut cells: Vec<CellId> = comp.iter().map(|&i| ids[i]).collect();
459            cells.sort_unstable();
460            plan.cycles.push(cells);
461        }
462    }
463    plan.cycles.sort();
464    plan
465}
466
467/// Tarjan's strongly connected components, iteratively, in the order the
468/// algorithm emits them: a component after every component it has an edge
469/// into, which is the evaluation order when an edge runs from a reader to
470/// what it reads. A plan of texts asks it of a vector of edges a cell; it is
471/// [`tarjan_flat`]'s answer, so that one walk serves both plans (KERNEL.33).
472pub(crate) fn tarjan(adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
473    let mut offsets = Vec::with_capacity(adj.len() + 1);
474    let mut targets = Vec::new();
475    offsets.push(0);
476    for edges in adj {
477        targets.extend(edges.iter().map(|&w| w as u32));
478        offsets.push(targets.len());
479    }
480    let (members, starts) = tarjan_flat(&offsets, &targets);
481    starts
482        .windows(2)
483        .map(|c| members[c[0]..c[1]].iter().map(|&v| v as usize).collect())
484        .collect()
485}
486
487/// [`tarjan`] over one flat array of edges (KERNEL.33), the machine's plan
488/// of shapes ordering its cells with it (KERNEL.22): vertex `v`'s edges are
489/// `targets[offsets[v]..offsets[v + 1]]`, and the components come back flat
490/// in the order the walk emits them, component `c` the vertices
491/// `members[starts[c]..starts[c + 1]]`, each sorted, with no vector made for
492/// each. A vertex is a position a `u32` holds, as a plan's cells are.
493pub(crate) fn tarjan_flat(offsets: &[usize], targets: &[u32]) -> (Vec<u32>, Vec<usize>) {
494    const NONE: u32 = u32::MAX;
495    let n = offsets.len().saturating_sub(1);
496    let mut index = vec![NONE; n];
497    let mut low = vec![0u32; n];
498    let mut on_stack = vec![false; n];
499    let mut stack: Vec<u32> = Vec::new();
500    let mut members: Vec<u32> = Vec::with_capacity(n);
501    let mut starts: Vec<usize> = Vec::with_capacity(n + 1);
502    starts.push(0);
503    let mut counter = 0u32;
504    // Each vertex being visited, with the position of its next edge.
505    let mut calls: Vec<(usize, usize)> = Vec::new();
506    for root in 0..n {
507        if index[root] != NONE {
508            continue;
509        }
510        calls.push((root, offsets[root]));
511        index[root] = counter;
512        low[root] = counter;
513        counter += 1;
514        stack.push(root as u32);
515        on_stack[root] = true;
516        while let Some(&mut (v, ref mut next)) = calls.last_mut() {
517            if *next < offsets[v + 1] {
518                let w = targets[*next] as usize;
519                *next += 1;
520                if index[w] == NONE {
521                    index[w] = counter;
522                    low[w] = counter;
523                    counter += 1;
524                    stack.push(w as u32);
525                    on_stack[w] = true;
526                    calls.push((w, offsets[w]));
527                } else if on_stack[w] {
528                    low[v] = low[v].min(index[w]);
529                }
530                continue;
531            }
532            calls.pop();
533            if let Some(&(u, _)) = calls.last() {
534                low[u] = low[u].min(low[v]);
535            }
536            if low[v] == index[v] {
537                let from = members.len();
538                while let Some(w) = stack.pop() {
539                    on_stack[w as usize] = false;
540                    members.push(w);
541                    if w as usize == v {
542                        break;
543                    }
544                }
545                members[from..].sort_unstable();
546                starts.push(members.len());
547            }
548        }
549    }
550    (members, starts)
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556
557    #[test]
558    fn tarjan_emits_dependencies_before_their_readers() {
559        // 0 reads 1 and 2; 1 reads 2; 3 and 4 read each other; 5 reads itself.
560        let adj = vec![vec![1, 2], vec![2], vec![], vec![4], vec![3], vec![5]];
561        let comps = tarjan(&adj);
562        let pos = |x: usize| comps.iter().position(|c| c.contains(&x)).unwrap();
563        assert!(pos(2) < pos(1) && pos(1) < pos(0));
564        assert!(comps.contains(&vec![3, 4]));
565        assert!(comps.contains(&vec![5]));
566        assert_eq!(comps.len(), 5);
567    }
568
569    #[test]
570    fn the_flat_walk_finds_every_component_after_all_it_reads() {
571        // KERNEL.33: both plans order their cells by the one walk now, so it
572        // is held to the components found by brute force, two vertices of
573        // one component when each reaches the other, over seeded graphs of
574        // every density, cycles and self-reads among them; each component
575        // sorted, and emitted after every component it has an edge into.
576        let mut seed: u64 = 0x5EED_0033;
577        let mut below = |n: usize| {
578            seed = seed
579                .wrapping_mul(6_364_136_223_846_793_005)
580                .wrapping_add(1_442_695_040_888_963_407);
581            ((seed >> 33) % n as u64) as usize
582        };
583        for case in 0..400 {
584            let n = 1 + below(14);
585            let density = 1 + below(5);
586            let adj: Vec<Vec<usize>> = (0..n)
587                .map(|_| {
588                    let mut edges: Vec<usize> = (0..below(density * 2)).map(|_| below(n)).collect();
589                    edges.sort_unstable();
590                    edges.dedup();
591                    edges
592                })
593                .collect();
594            let mut reach = vec![vec![false; n]; n];
595            for (v, row) in reach.iter_mut().enumerate() {
596                let mut stack = vec![v];
597                while let Some(u) = stack.pop() {
598                    for &w in &adj[u] {
599                        if !row[w] {
600                            row[w] = true;
601                            stack.push(w);
602                        }
603                    }
604                }
605            }
606            let mut want: Vec<Vec<usize>> = Vec::new();
607            for (v, from_v) in reach.iter().enumerate() {
608                if want.iter().any(|c| c.contains(&v)) {
609                    continue;
610                }
611                want.push(
612                    (0..n)
613                        .filter(|&u| u == v || (from_v[u] && reach[u][v]))
614                        .collect(),
615                );
616            }
617            let comps = tarjan(&adj);
618            let mut got = comps.clone();
619            got.sort();
620            want.sort();
621            assert_eq!(got, want, "case {case}: {adj:?}");
622            let at = |v: usize| comps.iter().position(|c| c.contains(&v)).unwrap();
623            for (v, edges) in adj.iter().enumerate() {
624                for &w in edges {
625                    assert!(at(w) <= at(v), "case {case}: {v} reads {w}: {adj:?}");
626                }
627            }
628        }
629    }
630
631    #[test]
632    fn a_reference_s_areas_are_one_held_in_place_or_a_span_s_in_order() {
633        // KERNEL.25: one area is held in place and read as a slice of one;
634        // a span's are one a sheet, in the sheets' order.
635        use super::super::formula::{parse, Expr};
636        let mut wb = Workbook::new();
637        for name in ["Jan", "Feb", "Mar"] {
638            wb.sheets.push(Sheet::new(name));
639        }
640        let areas = |text: &str| {
641            let Ok(Expr::Ref(r)) = parse(text) else {
642                panic!("{text}")
643            };
644            areas_of(&wb, 0, &r)
645        };
646        let rect = |sheet, top, left, bottom, right| Area {
647            sheet,
648            top,
649            left,
650            bottom,
651            right,
652        };
653        let one = areas("=Feb!B2:C3").unwrap();
654        assert_eq!(&one[..], &[rect(1, 2, 2, 3, 3)]);
655        assert_eq!(one, Areas::one(rect(1, 2, 2, 3, 3)));
656        assert_eq!(one, Areas::from(vec![rect(1, 2, 2, 3, 3)]));
657        assert_eq!(
658            &areas("=B2").unwrap()[..],
659            &[rect(0, 2, 2, 2, 2)],
660            "the formula's own sheet"
661        );
662        let span = areas("=Jan:Mar!B2").unwrap();
663        let sheets: Vec<usize> = span.iter().map(|a| a.sheet).collect();
664        assert_eq!(sheets, vec![0, 1, 2]);
665        assert_eq!(areas("=Mar:Jan!B2").unwrap(), span);
666        assert_ne!(span, one);
667        assert_eq!(areas("=Apr!B2"), Err(ErrorKind::Ref));
668    }
669
670    #[test]
671    fn a_range_s_cells_are_walked_row_by_row_as_they_were_collected() {
672        // KERNEL.25: the walk is read as it goes. Its cells and their order
673        // are the sheet's own cells inside the area clamped to the extent,
674        // row by row, in both its walks: row by row through each row's
675        // cells, and through every cell between the first and last rows for
676        // a sheet with fewer cells than the area has rows.
677        use crate::sheet::{MAX_COLUMN, MAX_ROW};
678        let number = |n: u32| Cell {
679            content: Content::Number(f64::from(n)),
680            style: 0,
681        };
682        let mut dense = Sheet::new("Dense");
683        for row in 1..=5u32 {
684            for col in 1..=4u32 {
685                if (row + col) % 5 != 0 {
686                    dense.set(row, col, number(row * 10 + col));
687                }
688            }
689        }
690        let mut sparse = Sheet::new("Sparse");
691        for (row, col) in [(1, 2), (40, 1), (40, 3), (700, 2), (1000, 4)] {
692            sparse.set(row, col, number(row));
693        }
694        let empty = Sheet::new("Empty");
695        let rect = |top, left, bottom, right| Area {
696            sheet: 0,
697            top,
698            left,
699            bottom,
700            right,
701        };
702        let areas = [
703            rect(2, 2, 4, 3),
704            rect(1, 1, MAX_ROW, 1),
705            rect(3, 1, 3, MAX_COLUMN),
706            rect(1, 1, MAX_ROW, MAX_COLUMN),
707            rect(40, 2, 700, 3),
708            rect(6, 1, 9, 9),
709            rect(2, 5, 4, 9),
710            rect(1, 2, 1, 2),
711        ];
712        let mut walked_any = 0;
713        for sheet in [&dense, &sparse, &empty] {
714            let extent = sheet.extent();
715            for a in &areas {
716                let walked: Vec<(u32, u32)> = cells_in(sheet, extent, a).map(|(k, _)| k).collect();
717                let want: Vec<(u32, u32)> = match extent {
718                    None => Vec::new(),
719                    Some(((t, l), (b, r))) => sheet
720                        .cells
721                        .keys()
722                        .copied()
723                        .filter(|&(row, col)| {
724                            row >= a.top.max(t)
725                                && row <= a.bottom.min(b)
726                                && col >= a.left.max(l)
727                                && col <= a.right.min(r)
728                        })
729                        .collect(),
730                };
731                assert_eq!(walked, want, "{} {a:?}", sheet.name);
732                walked_any += walked.len();
733            }
734        }
735        assert_eq!(
736            walked_any, 37,
737            "28 cells of the dense sheet's, 9 of the sparse one's"
738        );
739    }
740
741    #[test]
742    fn a_deep_chain_does_not_overflow() {
743        let n = 200_000;
744        let adj: Vec<Vec<usize>> = (0..n)
745            .map(|i| if i + 1 < n { vec![i + 1] } else { vec![] })
746            .collect();
747        let comps = tarjan(&adj);
748        assert_eq!(comps.len(), n);
749        assert_eq!(comps[0], vec![n - 1]);
750        assert_eq!(comps[n - 1], vec![0]);
751    }
752}