Skip to main content

truecalc_workbook/
depgraph.rs

1//! Dependency graph for the workbook runtime (plan item 3.2, issue #534).
2//!
3//! The graph records, for every formula cell, *which cells, ranges, and named
4//! ranges it reads* — its **precedents** — derived once from the parsed
5//! formula via [`truecalc_core::extract_refs`] (P1.3). The reverse edges (a
6//! cell's **dependents**: the formula cells that must recalculate when it
7//! changes) are what the recalc engine (P3.3, #535) walks to propagate a dirty
8//! set, and what topological ordering and cycle detection run over.
9//!
10//! # What this layer is (and is not)
11//!
12//! This is the dependency *graph only*. It owns no values and performs no
13//! evaluation: [`DependencyGraph::build`] reads a [`Workbook`] and produces the
14//! edges; recalculation is P3.3. It exposes a [topological order /
15//! cycle-detection primitive](DependencyGraph::topological_order) because P3.3
16//! needs it, but it never evaluates a formula.
17//!
18//! # How edges are derived ([`extract_refs`])
19//!
20//! For each formula cell the graph parses the verbatim formula (parsing is
21//! flavor-independent and does not consult the workbook's locked engine,
22//! issue #900), calls [`extract_refs`] on the AST, and resolves
23//! each [`Ref`] to a concrete graph node:
24//!
25//! - [`Ref::Cell`] → a single-cell precedent; a bare `A1` resolves against the
26//!   formula cell's *own* sheet, a qualified `Sheet1!A1` against the named
27//!   sheet.
28//! - [`Ref::Range`] → a **range node** (range-node compression): `A1:A100000`
29//!   is one node, not 100 000 edges. A changed cell finds its range-dependents
30//!   by testing membership in each live range node, so the graph stays linear
31//!   in the number of *distinct ranges*, not their area.
32//! - [`Ref::Name`] → a **name node** (name → target indirection): the formula
33//!   depends on the name, the name depends on its current target cell/range.
34//!   Retargeting a name (P3.4) therefore dirties the name's dependents without
35//!   rebuilding their edges, and a write inside a name's target range dirties
36//!   the name's dependents transitively.
37//!
38//! A reference that cannot be resolved (an unknown sheet, an unknown name, a
39//! malformed or unparseable formula) is recorded as an [`Unresolved`]
40//! precedent rather than dropped: it carries no edge (nothing can dirty it),
41//! but it is preserved so the recalc engine can surface the Sheets error the
42//! formula will ultimately produce (`#REF!` / `#NAME?`), fixture-verified in
43//! P3.3 rather than assumed here.
44//!
45//! [`extract_refs`]: truecalc_core::extract_refs
46//! [`Ref`]: truecalc_core::Ref
47//! [`Ref::Cell`]: truecalc_core::Ref::Cell
48//! [`Ref::Range`]: truecalc_core::Ref::Range
49//! [`Ref::Name`]: truecalc_core::Ref::Name
50//! [`Unresolved`]: Precedent::Unresolved
51
52use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
53
54use icu_casemap::CaseMapperBorrowed;
55use truecalc_core::{CellAddr, Ref};
56
57use crate::address::Address;
58use crate::casefold::simple_fold;
59use crate::named_ref;
60use crate::sheet_index::SheetIndex;
61use crate::value::Value;
62use crate::workbook::Workbook;
63
64/// A fully resolved cell coordinate: a sheet (by its position-independent,
65/// case-folded name) and an in-bounds [`Address`].
66///
67/// Sheets are keyed by **folded name**, not tab index, so the key survives a
68/// sheet move (P3.1 `move_sheet`) and matches the case-insensitive sheet
69/// lookup of [`Workbook::sheet`](crate::Workbook::sheet). A rename changes the
70/// key, which is why a rename forces a graph rebuild (see the module docs and
71/// [`DependencyGraph::build`]).
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub struct CellRef {
74    /// The target sheet's name, simple-case-folded (schema spec §2).
75    pub sheet: String,
76    /// The in-bounds A1 address within that sheet.
77    pub addr: Address,
78}
79
80impl CellRef {
81    fn new(sheet: String, addr: Address) -> Self {
82        Self { sheet, addr }
83    }
84
85    /// Builds the graph key for `addr` on the sheet named `sheet`, applying the
86    /// simple case folding (schema spec §2) the graph indexes sheets by.
87    ///
88    /// Named `from_display_name` — not `resolve` — because it does not consult
89    /// a [`Workbook`](crate::Workbook): it only folds the caller's spelling
90    /// into the graph's key form, the same way [`new`](Self::new) builds a
91    /// `CellRef` from an already-folded one. `resolve_ref` and
92    /// `resolve_query_cell` elsewhere in this crate *do* look a sheet up
93    /// against a workbook; this does not, so it does not share their name.
94    ///
95    /// The [`sheet`](Self::sheet) field is public but holds a *folded* name, so
96    /// a `CellRef` constructed literally from a user-facing sheet name (a tab
97    /// label, a JSON key, an API argument) silently matches nothing in the
98    /// graph whenever that name is not already folded. Any sheet name that did
99    /// not come out of the graph itself should reach a query through here.
100    ///
101    /// Folding is idempotent, so passing an already-folded name is a no-op.
102    /// The sheet is *not* required to exist — an unknown sheet simply produces
103    /// a key nothing in the graph matches.
104    pub fn from_display_name(sheet: &str, addr: Address) -> Self {
105        let folder = CaseMapperBorrowed::new();
106        Self::new(simple_fold(&folder, sheet), addr)
107    }
108}
109
110/// A resolved rectangular range: a sheet (folded name) and an inclusive,
111/// top-left-first corner pair.
112///
113/// Range-node compression hinges on this being a single value regardless of
114/// area: membership of a [`CellRef`] is an `O(1)` rectangle test
115/// ([`RangeRef::contains`]), so finding the formula cells that read a changed
116/// cell through a range costs one test per *distinct range*, never one per cell
117/// in the range.
118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub struct RangeRef {
120    /// The target sheet's name, simple-case-folded.
121    pub sheet: String,
122    /// Top-left corner (minimum row, minimum column).
123    pub start: Address,
124    /// Bottom-right corner (maximum row, maximum column).
125    pub end: Address,
126}
127
128impl RangeRef {
129    /// Whether `cell` lies inside this range (same sheet, within the inclusive
130    /// rectangle). The membership test behind range-node compression.
131    pub fn contains(&self, cell: &CellRef) -> bool {
132        cell.sheet == self.sheet
133            && cell.addr.row >= self.start.row
134            && cell.addr.row <= self.end.row
135            && cell.addr.column >= self.start.column
136            && cell.addr.column <= self.end.column
137    }
138}
139
140/// One resolved precedent of a formula cell: what a single [`Ref`] in the
141/// formula points at, after sheet/name resolution.
142///
143/// [`extract_refs`](truecalc_core::extract_refs) yields one [`Ref`] per
144/// reference occurrence (duplicates preserved); the graph maps each to one of
145/// these. [`Unresolved`](Precedent::Unresolved) keeps a reference that has no
146/// concrete target (unknown sheet/name, unparseable formula) so the recalc
147/// engine can still emit the right Sheets error.
148#[derive(Debug, Clone, PartialEq, Eq, Hash)]
149pub enum Precedent {
150    /// A single cell (`A1` on the formula's own sheet, or `Sheet1!A1`).
151    Cell(CellRef),
152    /// A rectangular range (`A1:D4`, `Sheet1!A1:B2`) — a compressed range node.
153    Range(RangeRef),
154    /// A workbook-scoped named range, by its case-folded name. The name's
155    /// current target (cell or range) supplies the transitive edges.
156    Name(String),
157    /// A reference that did not resolve to a concrete target: an unknown sheet
158    /// or name, or a formula that failed to parse. Carries the canonical
159    /// reference text for diagnostics; it produces no dirty-propagation edge.
160    Unresolved(String),
161}
162
163/// The dependency graph of a [`Workbook`]: precedents and reverse-edge indexes
164/// derived from every formula cell via [`extract_refs`](truecalc_core::extract_refs).
165///
166/// Built with [`DependencyGraph::build`]; queried with
167/// [`precedents_of`](Self::precedents_of),
168/// [`direct_dependents_of`](Self::direct_dependents_of),
169/// [`topological_order`](Self::topological_order), and
170/// [`cycle_cells`](Self::cycle_cells). A traversal that walks precedents
171/// transitively also wants [`formula_precedent_cells`](Self::formula_precedent_cells)
172/// (what to walk next) and [`name_target_of`](Self::name_target_of) (what a
173/// name currently points at). It is a pure derived view — it borrows
174/// nothing from the workbook after `build` returns and holds no values.
175///
176/// Rebuild rules (issue #534, "Rebuild rules on set/clear/rename"): the graph
177/// is a function of the workbook's formulas, sheet names, and named-range
178/// targets, so any edit that changes those — `set`/`clear` of a formula cell,
179/// a sheet rename, a named-range retarget — invalidates it. The P3.4 mutation
180/// API rebuilds (or incrementally updates) the graph after such edits; the
181/// graph-rebuild equivalence tests assert that a from-scratch
182/// [`build`](Self::build) after an arbitrary edit sequence equals the
183/// maintained graph.
184#[derive(Debug, Clone, PartialEq)]
185pub struct DependencyGraph {
186    /// Every formula cell, with its resolved precedents in formula order
187    /// (duplicates from `extract_refs` deduplicated per cell). The key set is
188    /// exactly the set of graph nodes that carry a formula.
189    precedents: BTreeMap<CellRef, Vec<Precedent>>,
190    /// Reverse cell→formula edges: for a precedent *cell*, the formula cells
191    /// that read it directly. The `O(1)` half of dependent lookup.
192    cell_dependents: HashMap<CellRef, BTreeSet<CellRef>>,
193    /// Reverse range edges: each distinct range node and the formula cells that
194    /// read it. Range-node compression — one entry per range, tested by
195    /// rectangle membership at query time.
196    range_dependents: Vec<(RangeRef, BTreeSet<CellRef>)>,
197    /// Name → its dependent formula cells (formulas that reference the name).
198    name_dependents: HashMap<String, BTreeSet<CellRef>>,
199    /// Name → its resolved current target (the indirection layer). Absent if
200    /// the name is undefined or dangles; retargeting updates this entry.
201    name_targets: HashMap<String, NameTarget>,
202    /// Formula cells indexed by sheet and row: `sheet → row → the formula
203    /// cells on that row, in column order`. This is what makes "which formula
204    /// cells does this range cover?" cost the covered rows rather than the
205    /// whole workbook (issue #908).
206    ///
207    /// Only rows that actually *contain* a formula cell are keyed — never the
208    /// dense span of a range — so a reference over ten million mostly-empty
209    /// rows visits only the handful of rows that hold formulas.
210    formula_rows: HashMap<String, BTreeMap<u32, Vec<Address>>>,
211}
212
213/// What a named range currently resolves to (the name→target indirection).
214///
215/// Exactly the two shapes a resolved name target can take — unlike
216/// [`Precedent`], it has no `Name` variant (names do not chain) and no
217/// `Unresolved` variant (an unresolved name has no target at all, hence
218/// [`name_target_of`](DependencyGraph::name_target_of) returning `None`
219/// rather than this type wrapped around an absence).
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub enum NameTarget {
222    Cell(CellRef),
223    Range(RangeRef),
224}
225
226impl DependencyGraph {
227    /// Builds the dependency graph from `workbook`.
228    ///
229    /// Walks every populated cell on every sheet; for each *formula* cell,
230    /// parses the formula (flavor-independent, no engine needed — issue #900),
231    /// extracts its refs
232    /// ([`extract_refs`](truecalc_core::extract_refs)), resolves each to a
233    /// concrete node, and records both the forward precedent list and the
234    /// reverse edges. Named-range targets are resolved up front so name
235    /// indirection edges are available.
236    ///
237    /// Resolution is total: an unresolvable reference becomes a
238    /// [`Precedent::Unresolved`] rather than an error, so a workbook with a
239    /// dangling `Sheet9!A1` or an unknown name still builds (the recalc engine
240    /// turns those into Sheets errors, fixture-verified in P3.3). Building
241    /// therefore never fails.
242    pub fn build(workbook: &Workbook) -> Self {
243        let folder = CaseMapperBorrowed::new();
244        // The sheet list, folded once for the whole build (issue #952).
245        // Resolving a reference's target sheet used to be
246        // `workbook.sheet(name)` — a linear scan that case-folded every sheet
247        // name it passed — performed once per cross-sheet reference, so graph
248        // build carried the same `O(refs × sheets × name-length)` term the
249        // recalc path did.
250        let sheets = SheetIndex::build(workbook);
251
252        // Resolve named-range targets first (the name → target indirection
253        // layer). A name whose ref names a missing sheet, or is itself
254        // malformed, simply has no target and contributes no transitive edge.
255        let mut name_targets: HashMap<String, NameTarget> = HashMap::new();
256        for nr in workbook.names() {
257            let folded = simple_fold(&folder, &nr.name);
258            if let Some(target) = resolve_name_ref(&nr.r#ref, &sheets) {
259                name_targets.insert(folded, target);
260            }
261        }
262
263        let mut graph = DependencyGraph {
264            precedents: BTreeMap::new(),
265            cell_dependents: HashMap::new(),
266            range_dependents: Vec::new(),
267            name_dependents: HashMap::new(),
268            name_targets,
269            formula_rows: HashMap::new(),
270        };
271        // Stable index from a range node to its slot in `range_dependents`, so
272        // repeated references to the same range share one compressed node.
273        let mut range_slots: HashMap<RangeRef, usize> = HashMap::new();
274
275        for (i, sheet) in workbook.sheets().iter().enumerate() {
276            let sheet_folded = sheets.folded_name(i).to_owned();
277            for (addr, cell) in sheet.iter() {
278                let Some(formula) = cell.formula() else {
279                    continue;
280                };
281                let from = CellRef::new(sheet_folded.clone(), addr);
282
283                // Parsed without an `Engine`: parsing is flavor-independent
284                // and never reads the function registry, so building one per
285                // graph build was pure waste (issue #900).
286                let refs = match truecalc_core::parse_formula(formula) {
287                    Ok(expr) => truecalc_core::extract_refs(&expr),
288                    // An unparseable formula has one self-describing precedent
289                    // and no edges — the recalc engine reports the parse error.
290                    Err(_) => {
291                        graph.precedents.insert(
292                            from.clone(),
293                            vec![Precedent::Unresolved(formula.to_owned())],
294                        );
295                        continue;
296                    }
297                };
298
299                let mut seen: HashSet<Precedent> = HashSet::new();
300                let mut resolved: Vec<Precedent> = Vec::new();
301                for r in &refs {
302                    let prec = resolve_ref(r, &from.sheet, from.addr, &folder, &sheets, workbook);
303                    if seen.insert(prec.clone()) {
304                        resolved.push(prec);
305                    }
306                }
307
308                // Record reverse edges for each resolved precedent.
309                for prec in &resolved {
310                    match prec {
311                        Precedent::Cell(target) => {
312                            graph
313                                .cell_dependents
314                                .entry(target.clone())
315                                .or_default()
316                                .insert(from.clone());
317                        }
318                        Precedent::Range(range) => {
319                            let slot = *range_slots.entry(range.clone()).or_insert_with(|| {
320                                graph
321                                    .range_dependents
322                                    .push((range.clone(), BTreeSet::new()));
323                                graph.range_dependents.len() - 1
324                            });
325                            graph.range_dependents[slot].1.insert(from.clone());
326                        }
327                        Precedent::Name(name) => {
328                            graph
329                                .name_dependents
330                                .entry(name.clone())
331                                .or_default()
332                                .insert(from.clone());
333                        }
334                        Precedent::Unresolved(_) => {}
335                    }
336                }
337
338                graph.precedents.insert(from, resolved);
339            }
340        }
341
342        // Index the formula cells by sheet and row (issue #908). Walking the
343        // precedents map yields cells in canonical (sheet, row, column) order,
344        // so each row's vector comes out in column order for free, and only
345        // occupied rows become keys.
346        for cell in graph.precedents.keys() {
347            graph
348                .formula_rows
349                .entry(cell.sheet.clone())
350                .or_default()
351                .entry(cell.addr.row)
352                .or_default()
353                .push(cell.addr);
354        }
355
356        graph
357    }
358
359    /// The resolved precedents of `cell` in formula order, or `None` if `cell`
360    /// is not a formula cell (a literal or an empty cell has no precedents).
361    pub fn precedents_of(&self, cell: &CellRef) -> Option<&[Precedent]> {
362        self.precedents.get(cell).map(Vec::as_slice)
363    }
364
365    /// Whether `cell` is a formula cell tracked by the graph.
366    pub fn is_formula(&self, cell: &CellRef) -> bool {
367        self.precedents.contains_key(cell)
368    }
369
370    /// Every formula cell in the graph, in canonical (sheet, address) order.
371    pub fn formula_cells(&self) -> impl Iterator<Item = &CellRef> {
372        self.precedents.keys()
373    }
374
375    /// The formula cells that read `cell` **directly** — through a single-cell
376    /// reference, through a range that contains `cell`, or through a named
377    /// range whose target contains `cell`.
378    ///
379    /// This is the dirty-propagation primitive the incremental recalc engine
380    /// (P3.3) walks transitively: when `cell` changes, every cell returned here
381    /// is dirty, and the walk repeats from each of them. It deliberately
382    /// composes all three edge kinds so callers never reason about
383    /// range-node compression or name indirection themselves.
384    ///
385    /// Returned in canonical (sheet, address) order; the set is deduplicated
386    /// even when a formula reaches `cell` by more than one path.
387    pub fn direct_dependents_of(&self, cell: &CellRef) -> BTreeSet<CellRef> {
388        let mut out = BTreeSet::new();
389        if let Some(direct) = self.cell_dependents.get(cell) {
390            out.extend(direct.iter().cloned());
391        }
392        for (range, deps) in &self.range_dependents {
393            if range.contains(cell) {
394                out.extend(deps.iter().cloned());
395            }
396        }
397        // Name indirection: a write inside a name's target dirties the name's
398        // dependents.
399        for (name, target) in &self.name_targets {
400            let hit = match target {
401                NameTarget::Cell(c) => c == cell,
402                NameTarget::Range(r) => r.contains(cell),
403            };
404            if hit {
405                if let Some(deps) = self.name_dependents.get(name) {
406                    out.extend(deps.iter().cloned());
407                }
408            }
409        }
410        out
411    }
412
413    /// The formula cells that depend on the named range `name` (any case),
414    /// i.e. would be dirtied by retargeting it (P3.4).
415    ///
416    /// Retargeting a name changes what its dependents read without changing
417    /// their formulas, so the recalc engine dirties exactly this set (the
418    /// name → target indirection promised by issue #534).
419    pub fn name_dependents_of(&self, name: &str) -> BTreeSet<CellRef> {
420        let folder = CaseMapperBorrowed::new();
421        let folded = simple_fold(&folder, name);
422        self.name_dependents
423            .get(&folded)
424            .cloned()
425            .unwrap_or_default()
426    }
427
428    /// The current target of the named range `name` (any case): the cell or
429    /// range its dependents actually read, or `None` when the name is not
430    /// defined in this workbook or its reference does not resolve.
431    ///
432    /// The forward half of the name → target indirection whose reverse half is
433    /// [`name_dependents_of`](Self::name_dependents_of). A caller walking a
434    /// formula's precedents needs it to report what a [`Precedent::Name`]
435    /// actually points at; [`NameTarget`] is the two-variant type for exactly
436    /// that answer, so the signature itself rules out a name or an
437    /// unresolved reference coming back — no doc caveat required.
438    pub fn name_target_of(&self, name: &str) -> Option<NameTarget> {
439        let folder = CaseMapperBorrowed::new();
440        let folded = simple_fold(&folder, name);
441        self.name_targets.get(&folded).cloned()
442    }
443
444    /// A topological order of the formula cells: every cell appears after all
445    /// the formula cells it (transitively) reads, so evaluating in this order
446    /// visits each cell only once with its precedents already current.
447    ///
448    /// Returns `Ok(order)` when the formula-cell subgraph is acyclic, or
449    /// `Err(cycle_cells)` listing every formula cell that lies on a cycle (the
450    /// set [`cycle_cells`](Self::cycle_cells) returns). Only edges *between
451    /// formula cells* participate: a formula that reads a literal cell has
452    /// nothing to wait for. This is the ordering primitive P3.3 evaluates in;
453    /// the Sheets circular-dependency error semantics for the cells on a cycle
454    /// are applied by the recalc engine (fixture-verified there), not here.
455    pub fn topological_order(&self) -> Result<Vec<CellRef>, BTreeSet<CellRef>> {
456        let edges = self.formula_edges();
457        match edges.topological_order() {
458            Some(order) => Ok(order),
459            // Some cells never reached in-degree 0: they are on or downstream
460            // of a cycle. Report exactly the cells *on* a cycle.
461            None => Err(edges.cycle_members()),
462        }
463    }
464
465    /// The evaluation order and the cycle set together, from **one** pass over
466    /// the formula-cell edges.
467    ///
468    /// A recalculation needs both: the order to evaluate in, and the cells to
469    /// mark with the circular-dependency error. Asking for them separately
470    /// ([`cycle_cells`](Self::cycle_cells) then
471    /// [`topological_order`](Self::topological_order)) derives the same
472    /// formula-cell adjacency from the precedent lists twice and throws it away
473    /// twice. This builds it once. Nothing is cached and nothing has to be
474    /// invalidated; it is the same work, done once instead of twice.
475    ///
476    /// The order is [`topological_order`](Self::topological_order)'s when the
477    /// graph is acyclic (and the cycle set is then empty), and
478    /// [`acyclic_order_excluding`](Self::acyclic_order_excluding)'s over the
479    /// acyclic remainder when it is not.
480    pub fn evaluation_order(&self) -> (Vec<CellRef>, BTreeSet<CellRef>) {
481        let edges = self.formula_edges();
482        match edges.topological_order() {
483            Some(order) => (order, BTreeSet::new()),
484            None => {
485                // The graph has a cycle. Order the acyclic remainder, so cells
486                // that do not touch the cycle still evaluate in dependency
487                // order; the rest take the circular error.
488                let cycle = edges.cycle_members();
489                let order = edges.order_excluding(&cycle);
490                (order, cycle)
491            }
492        }
493    }
494
495    /// A topological order over the formula cells **not** on a cycle, for the
496    /// cyclic-graph case (P3.3): cells that do not transitively read the cycle
497    /// still evaluate in dependency order; cells on or downstream of the cycle
498    /// are omitted (the recalc engine gives them the circular error). When the
499    /// graph is acyclic this equals [`topological_order`](Self::topological_order).
500    ///
501    /// `cycle` must be the cycle set returned by
502    /// [`cycle_cells`](Self::cycle_cells) (passed in so the caller computes it
503    /// once). The order is deterministic (canonical tie-breaking), matching
504    /// `topological_order`'s discipline.
505    pub fn acyclic_order_excluding(&self, cycle: &BTreeSet<CellRef>) -> Vec<CellRef> {
506        self.formula_edges().order_excluding(cycle)
507    }
508
509    /// Every formula cell that lies on a dependency cycle (a strongly connected
510    /// component of size > 1, or a self-referential cell).
511    ///
512    /// This is the set P3.3 marks with the Sheets circular-dependency error.
513    /// Empty iff the formula-cell subgraph is acyclic. Computed independently
514    /// of [`topological_order`](Self::topological_order) so it can be queried
515    /// directly.
516    pub fn cycle_cells(&self) -> BTreeSet<CellRef> {
517        self.formula_edges().cycle_members()
518    }
519
520    /// The formula-cell-only adjacency the ordering and cycle passes both run
521    /// over: precedent formula cell → dependent formula cell, with the nodes in
522    /// canonical order so every traversal over it is deterministic.
523    ///
524    /// Deriving it means expanding every precedent to the formula cells it
525    /// covers, which is the expensive part of ordering a graph; the callers
526    /// that need both an order and the cycle set take it once
527    /// ([`evaluation_order`](Self::evaluation_order)) rather than twice.
528    fn formula_edges(&self) -> FormulaEdges<'_> {
529        let nodes: Vec<&CellRef> = self.precedents.keys().collect();
530        let index_of: HashMap<&CellRef, usize> =
531            nodes.iter().enumerate().map(|(i, n)| (*n, i)).collect();
532
533        // `BTreeSet` keeps each node's successors in canonical order and dedups
534        // parallel edges (a formula reading the same cell twice).
535        let mut succ: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); nodes.len()];
536        for (i, cell) in nodes.iter().enumerate() {
537            for prec in &self.precedents[*cell] {
538                for fp in self.formula_precedent_cells(prec) {
539                    // Edge fp(j) → cell(i): cell i depends on fp.
540                    if let Some(&j) = index_of.get(&fp) {
541                        succ[j].insert(i);
542                    }
543                }
544            }
545        }
546
547        FormulaEdges { nodes, succ }
548    }
549
550    /// Maps a precedent to the *formula* cells it covers (its intersection with
551    /// the graph's formula-cell set), following name indirection. Literal and
552    /// empty cells are not yielded — only edges between formula cells matter for
553    /// ordering and cycles.
554    ///
555    /// This is the "what do I walk next" primitive of a precedent traversal: a
556    /// [`Precedent::Cell`] yields that cell iff it carries a formula, a
557    /// [`Precedent::Range`] yields the formula cells inside it (range-node
558    /// compression is expanded only here, never in the stored edges), a
559    /// [`Precedent::Name`] yields the formula cells its current target covers,
560    /// and a [`Precedent::Unresolved`] yields nothing. Returned in canonical
561    /// (sheet, address) order.
562    ///
563    /// Cost is `O(1)` for a cell precedent and, for a range or range-targeted
564    /// name, `O(formula cells on the rows the range spans)` — the rows are
565    /// indexed and only *occupied* rows are keyed, so neither the empty rows a
566    /// tall reference spans nor the formula cells elsewhere in the workbook are
567    /// visited (issue #908). Still an upper bound rather than a contract:
568    /// callers should not rely on it staying this expensive or this cheap.
569    pub fn formula_precedent_cells(&self, prec: &Precedent) -> Vec<CellRef> {
570        self.formula_precedent_cells_examined(prec).0
571    }
572
573    /// [`formula_precedent_cells`](Self::formula_precedent_cells), plus how
574    /// many candidate formula cells the lookup examined to produce it.
575    ///
576    /// Instrumentation, not a feature: the range index of issue #908 is a
577    /// change in *how much is examined*, and wall-clock is too
578    /// machine-dependent to pin it. Both values come out of the one lookup, so
579    /// the count cannot drift from what the lookup actually does. Hidden from
580    /// the docs because callers want
581    /// [`formula_precedent_cells`](Self::formula_precedent_cells).
582    #[doc(hidden)]
583    pub fn formula_precedent_cells_examined(&self, prec: &Precedent) -> (Vec<CellRef>, usize) {
584        match prec {
585            Precedent::Cell(c) => {
586                if self.precedents.contains_key(c) {
587                    (vec![c.clone()], 1)
588                } else {
589                    (Vec::new(), 1)
590                }
591            }
592            Precedent::Range(r) => self.formula_cells_in_range(r),
593            Precedent::Name(name) => match self.name_targets.get(name) {
594                Some(NameTarget::Cell(c)) if self.precedents.contains_key(c) => {
595                    (vec![c.clone()], 1)
596                }
597                Some(NameTarget::Cell(_)) => (Vec::new(), 1),
598                None => (Vec::new(), 0),
599                Some(NameTarget::Range(r)) => self.formula_cells_in_range(r),
600            },
601            Precedent::Unresolved(_) => (Vec::new(), 0),
602        }
603    }
604
605    /// The formula cells inside `range`, in canonical order, and the number of
606    /// formula cells examined to find them.
607    ///
608    /// Walks only the *occupied* rows the range spans (`formula_rows` is keyed
609    /// by row, so a `BTreeMap` range query skips every row that holds no
610    /// formula), then filters those rows' cells by column. The cells examined
611    /// are therefore the formula cells on the rows the range covers — not the
612    /// formula cells of the whole workbook, which is what the scan this
613    /// replaced cost (issue #908).
614    fn formula_cells_in_range(&self, range: &RangeRef) -> (Vec<CellRef>, usize) {
615        let mut out = Vec::new();
616        let mut examined = 0usize;
617        // Ranges resolved from formulas and names are top-left-first, but
618        // `RangeRef`'s fields are public and this is reached from a public
619        // entry point, so the corners can arrive the wrong way round.
620        // [`RangeRef::contains`] answers "no" to every cell for such a range;
621        // answer the same, rather than handing `BTreeMap::range` an inverted
622        // bound (which panics).
623        if range.start.row > range.end.row || range.start.column > range.end.column {
624            return (out, examined);
625        }
626        let Some(rows) = self.formula_rows.get(&range.sheet) else {
627            return (out, examined);
628        };
629        for addrs in rows.range(range.start.row..=range.end.row).map(|(_, a)| a) {
630            examined += addrs.len();
631            out.extend(
632                addrs
633                    .iter()
634                    .filter(|a| a.column >= range.start.column && a.column <= range.end.column)
635                    .map(|a| CellRef::new(range.sheet.clone(), *a)),
636            );
637        }
638        (out, examined)
639    }
640}
641
642/// The formula-cell-only adjacency of a [`DependencyGraph`]: the nodes in
643/// canonical order and, for each, the formula cells that read it.
644///
645/// Built by [`DependencyGraph::formula_edges`] and consumed by the ordering and
646/// cycle passes, which used to derive it independently from the precedent lists
647/// — the same work, done twice per recalculation.
648struct FormulaEdges<'a> {
649    /// Every formula cell, in canonical (sheet, address) order. Indices into
650    /// this vector are the node ids of `succ`.
651    nodes: Vec<&'a CellRef>,
652    /// `succ[j]` holds the nodes that read node `j` (edge `j → i` meaning
653    /// "`i` depends on `j`"), deduplicated and in canonical order.
654    succ: Vec<BTreeSet<usize>>,
655}
656
657impl FormulaEdges<'_> {
658    /// Kahn's algorithm over the whole node set, or `None` if a cycle prevents
659    /// every node from being placed.
660    ///
661    /// The ready set is a `BTreeSet` of indices, which — because `nodes` is in
662    /// canonical order — pops in canonical order, so the order is itself
663    /// deterministic.
664    fn topological_order(&self) -> Option<Vec<CellRef>> {
665        let mut indeg: Vec<usize> = vec![0; self.nodes.len()];
666        for deps in &self.succ {
667            for &i in deps {
668                indeg[i] += 1;
669            }
670        }
671
672        let mut ready: BTreeSet<usize> = (0..self.nodes.len()).filter(|&i| indeg[i] == 0).collect();
673        let mut order: Vec<CellRef> = Vec::with_capacity(self.nodes.len());
674        while let Some(&node) = ready.iter().next() {
675            ready.remove(&node);
676            order.push(self.nodes[node].clone());
677            for &dep in &self.succ[node] {
678                indeg[dep] -= 1;
679                if indeg[dep] == 0 {
680                    ready.insert(dep);
681                }
682            }
683        }
684
685        (order.len() == self.nodes.len()).then_some(order)
686    }
687
688    /// Kahn's algorithm over the nodes **not** in `cycle`, additionally
689    /// dropping every node that reads a cycle node (it takes the circular
690    /// error, and so does everything downstream of it).
691    fn order_excluding(&self, cycle: &BTreeSet<CellRef>) -> Vec<CellRef> {
692        // Surviving nodes, in canonical order; `slot` maps a node id to its
693        // position among them (`None` for a cycle node).
694        let kept: Vec<usize> = (0..self.nodes.len())
695            .filter(|&i| !cycle.contains(self.nodes[i]))
696            .collect();
697        let mut slot: Vec<Option<usize>> = vec![None; self.nodes.len()];
698        for (k, &i) in kept.iter().enumerate() {
699            slot[i] = Some(k);
700        }
701
702        let mut indeg: Vec<usize> = vec![0; kept.len()];
703        // A node that reads the cycle must be excluded from the order even
704        // though it has in-degree 0 over the surviving edges.
705        let mut tainted: Vec<bool> = vec![false; kept.len()];
706        for (j, deps) in self.succ.iter().enumerate() {
707            let from_cycle = slot[j].is_none();
708            for &i in deps {
709                let Some(k) = slot[i] else { continue };
710                if from_cycle {
711                    tainted[k] = true;
712                } else {
713                    indeg[k] += 1;
714                }
715            }
716        }
717
718        let mut ready: BTreeSet<usize> = (0..kept.len())
719            .filter(|&k| indeg[k] == 0 && !tainted[k])
720            .collect();
721        let mut order: Vec<CellRef> = Vec::new();
722        while let Some(&k) = ready.iter().next() {
723            ready.remove(&k);
724            order.push(self.nodes[kept[k]].clone());
725            for &i in &self.succ[kept[k]] {
726                let Some(dep) = slot[i] else { continue };
727                indeg[dep] -= 1;
728                if indeg[dep] == 0 && !tainted[dep] {
729                    ready.insert(dep);
730                }
731            }
732        }
733        order
734    }
735
736    /// Every node on a cycle: Tarjan's SCC, keeping components of size > 1 and
737    /// self loops.
738    fn cycle_members(&self) -> BTreeSet<CellRef> {
739        TarjanScc::new(&self.succ).cycle_members(&self.nodes)
740    }
741}
742
743/// Resolves a single parsed [`Ref`] against the workbook, relative to the
744/// formula's own (folded) sheet for bare references. `own_addr` is the
745/// formula cell's own address (unfolded), needed only by [`Ref::Table`] to
746/// infer which table an unqualified `[@column]` belongs to by containment —
747/// the same role `recalc.rs`'s `GridResolver.current_cell` plays for real
748/// value resolution.
749fn resolve_ref(
750    r: &Ref,
751    own_sheet: &str,
752    own_addr: Address,
753    folder: &CaseMapperBorrowed<'static>,
754    sheets: &SheetIndex,
755    workbook: &Workbook,
756) -> Precedent {
757    match r {
758        Ref::Cell { sheet, addr } => {
759            let sheet_folded = match sheet {
760                None => own_sheet.to_owned(),
761                Some(name) => match sheets.folded_of_name(name) {
762                    Some(folded) => folded.to_owned(),
763                    // `relative_display` (not `to_string`) so a missing-sheet
764                    // reference reached via `$A$1` dedupes with one reached
765                    // via `A1` — `$` anchors don't change what's unresolved.
766                    None => return Precedent::Unresolved(r.relative_display()),
767                },
768            };
769            match to_address(addr) {
770                Some(a) => Precedent::Cell(CellRef::new(sheet_folded, a)),
771                None => Precedent::Unresolved(r.relative_display()),
772            }
773        }
774        Ref::Range { sheet, start, end } => {
775            let sheet_folded = match sheet {
776                None => own_sheet.to_owned(),
777                Some(name) => match sheets.folded_of_name(name) {
778                    Some(folded) => folded.to_owned(),
779                    None => return Precedent::Unresolved(r.relative_display()),
780                },
781            };
782            match normalize_range(start, end) {
783                Some((s, e)) => Precedent::Range(RangeRef {
784                    sheet: sheet_folded,
785                    start: s,
786                    end: e,
787                }),
788                None => Precedent::Unresolved(r.relative_display()),
789            }
790        }
791        Ref::Name(name) => {
792            let folded = simple_fold(folder, name);
793            // A name is a precedent only if the workbook actually defines it;
794            // an unknown bare identifier is an unresolved reference (a `#NAME?`
795            // in Sheets), not a phantom name node.
796            if workbook
797                .names()
798                .iter()
799                .any(|nr| simple_fold(folder, &nr.name) == folded)
800            {
801                Precedent::Name(folded)
802            } else {
803                Precedent::Unresolved(name.clone())
804            }
805        }
806        Ref::Table {
807            table,
808            column,
809            this_row,
810        } => resolve_table_precedent(
811            table.as_deref(),
812            column,
813            *this_row,
814            own_sheet,
815            own_addr,
816            folder,
817            sheets,
818            workbook,
819        ),
820    }
821}
822
823/// Resolves a `Ref::Table` to its precedent.
824///
825/// Whole-column (`this_row: false`) precedents are a single
826/// [`Precedent::Range`] over just the resolved column (header row through
827/// last data row), not the whole table rectangle. The whole-table version
828/// used to make an in-table formula that reads a *different* column of its
829/// own table (e.g. `=[@qty]/SUM(T[qty])`, a common percentage-of-total
830/// pattern) a precedent of itself, since its own cell always falls inside
831/// the table's full rectangle — the same false-self-cycle class the
832/// current-row branch below already guards against, just previously
833/// uncaught for this branch (truecalc/core#861 final review). The header
834/// row is kept in the range (not narrowed to just the data rows): editing a
835/// header cell can change which column a name resolves to, so it should
836/// still dirty dependents — conservative for dirtying purposes while
837/// eliminating the false cross-column cycle. A formula in the *same* column
838/// that reads its own column remains correctly circular, since its own cell
839/// is still inside the narrowed range.
840///
841/// Current-row (`this_row: true`) precedents are a single [`Precedent::Cell`]
842/// at `(own_addr.row, resolved column)` instead: using the whole-table range
843/// here would make every in-table `[@col]` formula a *precedent of itself*
844/// (its own cell is always inside the table rectangle it reads from), a false
845/// self-cycle that would wrongly flag the extremely common "compute a column
846/// from sibling columns in the same row" pattern (e.g. `=[@qty]*[@price]`) as
847/// circular. A precise single-cell precedent matches exactly what
848/// `recalc.rs`'s `GridResolver::resolve_table_ref` actually reads for
849/// current-row, so it never over- or under-dirties.
850///
851/// An unqualified reference (`table: None`) infers the table from
852/// `own_addr`'s containment within a table's *data* rows (excluding the
853/// header row) on `own_sheet` — the same containment test
854/// `recalc.rs`'s `GridResolver::resolve_table_ref` uses via its
855/// `current_cell`, so an unqualified `[@column]` picks the same table here as
856/// it does for real value resolution. A qualified reference looks the table
857/// up by name directly.
858// Eight arguments: the sheet index joined an already-long parameter list
859// (issue #952). Same rationale as `recalc.rs`'s `eval_formula_cell` — these are
860// the inputs one reference resolution needs, and bundling them into a struct
861// would add a type whose only purpose is to satisfy the lint.
862#[allow(clippy::too_many_arguments)]
863fn resolve_table_precedent(
864    table: Option<&str>,
865    column: &str,
866    this_row: bool,
867    own_sheet: &str,
868    own_addr: Address,
869    folder: &CaseMapperBorrowed<'static>,
870    sheets: &SheetIndex,
871    workbook: &Workbook,
872) -> Precedent {
873    let target = match table {
874        Some(name) => {
875            let folded_name = simple_fold(folder, name);
876            workbook
877                .tables()
878                .iter()
879                .find(|t| simple_fold(folder, &t.name) == folded_name)
880        }
881        None => workbook.tables().iter().find(|t| {
882            named_ref::parse_canonical_ref(&t.r#ref)
883                .ok()
884                .and_then(|parsed| crate::table_ref::parsed_range_bounds(&t.r#ref, &parsed))
885                .is_some_and(|b| {
886                    simple_fold(folder, &b.sheet) == own_sheet
887                        && b.row_start < own_addr.row
888                        && own_addr.row <= b.row_end
889                        && b.col_start <= own_addr.column
890                        && own_addr.column <= b.col_end
891                })
892        }),
893    };
894    let Some(t) = target else {
895        return Precedent::Unresolved(format!(
896            "{}[{}{}]",
897            table.unwrap_or(""),
898            if this_row { "@" } else { "" },
899            column
900        ));
901    };
902    let Ok(parsed) = named_ref::parse_canonical_ref(&t.r#ref) else {
903        return Precedent::Unresolved(t.r#ref.clone());
904    };
905    let Some(bounds) = crate::table_ref::parsed_range_bounds(&t.r#ref, &parsed) else {
906        return Precedent::Unresolved(t.r#ref.clone());
907    };
908    // One index probe answers both "which tab?" and "what is its folded
909    // name?", where this used to fold `bounds.sheet` and then scan-and-fold the
910    // whole sheet list a second time (issue #952).
911    let sheet_idx = sheets.index_of_name(&bounds.sheet);
912    let sheet_folded = match sheet_idx {
913        Some(i) => sheets.folded_name(i).to_owned(),
914        // No such tab: no header can match, so the lookup below finds nothing
915        // and the reference resolves as unresolved — exactly what the missing
916        // sheet produced before.
917        None => simple_fold(folder, &bounds.sheet),
918    };
919
920    // Column-index-by-header lookup, same pattern as `recalc.rs`'s
921    // `GridResolver::resolve_table_ref`: a column that isn't actually in the
922    // table's header row produces no precedent (it's not a real dependency,
923    // the formula will error at recalc time regardless of what changes).
924    let column_folded = simple_fold(folder, column);
925    let sheet = sheet_idx.map(|i| &workbook.sheets()[i]);
926    let mut found = None;
927    for c in bounds.col_start..=bounds.col_end {
928        let Some(header_addr) = Address::new(bounds.row_start, c) else {
929            continue;
930        };
931        if let Some(Value::Text(header)) = sheet.and_then(|s| s.get(header_addr)).map(|c| c.value())
932        {
933            if simple_fold(folder, header) == column_folded {
934                found = Some(c);
935                break;
936            }
937        }
938    }
939    let Some(col) = found else {
940        return Precedent::Unresolved(t.r#ref.clone());
941    };
942
943    if this_row {
944        // Precise single-cell precedent (see the function doc comment for
945        // why the whole-table range would be wrong here): only valid if the
946        // formula's own cell is actually inside this table's data rows.
947        if own_sheet != sheet_folded
948            || own_addr.row <= bounds.row_start
949            || own_addr.row > bounds.row_end
950        {
951            return Precedent::Unresolved(t.r#ref.clone());
952        }
953        return match Address::new(own_addr.row, col) {
954            Some(a) => Precedent::Cell(CellRef::new(sheet_folded, a)),
955            None => Precedent::Unresolved(t.r#ref.clone()),
956        };
957    }
958
959    Precedent::Range(RangeRef {
960        sheet: sheet_folded,
961        start: Address::new(bounds.row_start, col).unwrap(),
962        end: Address::new(bounds.row_end, col).unwrap(),
963    })
964}
965
966/// Resolves a named range's canonical `ref` string to its concrete target,
967/// requiring the target sheet to exist. Returns `None` when the ref is
968/// malformed or names a missing sheet (a dangling name has no target).
969fn resolve_name_ref(r: &str, sheets: &SheetIndex) -> Option<NameTarget> {
970    let parsed = named_ref::parse_canonical_ref(r).ok()?;
971    // The ref's sheet must exist; key the target by its folded name.
972    let sheet_folded = sheets.folded_of_name(&parsed.sheet)?.to_owned();
973
974    // Recover the A1 part (parse_canonical_ref already validated it).
975    let a1_part = r.rsplit_once('!').map(|(_, a)| a).unwrap_or(r);
976    match a1_part.split_once(':') {
977        None => {
978            let addr = Address::from_a1(a1_part)?;
979            Some(NameTarget::Cell(CellRef::new(sheet_folded, addr)))
980        }
981        Some((s, e)) => {
982            let start = Address::from_a1(s)?;
983            let end = Address::from_a1(e)?;
984            Some(NameTarget::Range(RangeRef {
985                sheet: sheet_folded,
986                start,
987                end,
988            }))
989        }
990    }
991}
992
993/// Converts a core [`CellAddr`] (no upper bound of its own) to a workbook
994/// [`Address`], enforcing the workbook's grid bounds. An out-of-bounds ref
995/// (legal to *parse*, but off the grid) resolves to `None` → `Unresolved`.
996fn to_address(addr: &CellAddr) -> Option<Address> {
997    Address::new(addr.row, addr.col)
998}
999
1000/// Normalizes a parsed range to top-left-first, in-bounds corners. Returns
1001/// `None` if either corner is off-grid.
1002fn normalize_range(start: &CellAddr, end: &CellAddr) -> Option<(Address, Address)> {
1003    let top = Address::new(start.row.min(end.row), start.col.min(end.col))?;
1004    let bottom = Address::new(start.row.max(end.row), start.col.max(end.col))?;
1005    Some((top, bottom))
1006}
1007
1008/// Tarjan's strongly-connected-components, specialized to return the set of
1009/// nodes that lie on a cycle (SCCs of size > 1, plus self loops). Iterative to
1010/// avoid recursion blowup on deep dependency chains (the plan's ≥10k-deep
1011/// benchmark).
1012struct TarjanScc<'a> {
1013    adj: &'a [BTreeSet<usize>],
1014    index: Vec<Option<usize>>,
1015    lowlink: Vec<usize>,
1016    on_stack: Vec<bool>,
1017    stack: Vec<usize>,
1018    next_index: usize,
1019    components: Vec<Vec<usize>>,
1020}
1021
1022impl<'a> TarjanScc<'a> {
1023    fn new(adj: &'a [BTreeSet<usize>]) -> Self {
1024        let n = adj.len();
1025        Self {
1026            adj,
1027            index: vec![None; n],
1028            lowlink: vec![0; n],
1029            on_stack: vec![false; n],
1030            stack: Vec::new(),
1031            next_index: 0,
1032            components: Vec::new(),
1033        }
1034    }
1035
1036    fn cycle_members(mut self, nodes: &[&CellRef]) -> BTreeSet<CellRef> {
1037        for v in 0..self.adj.len() {
1038            if self.index[v].is_none() {
1039                self.strongconnect(v);
1040            }
1041        }
1042        let mut out = BTreeSet::new();
1043        for comp in &self.components {
1044            let on_cycle = comp.len() > 1
1045                // A singleton SCC is on a cycle only via a self loop.
1046                || (comp.len() == 1 && self.adj[comp[0]].contains(&comp[0]));
1047            if on_cycle {
1048                for &i in comp {
1049                    out.insert(nodes[i].clone());
1050                }
1051            }
1052        }
1053        out
1054    }
1055
1056    fn strongconnect(&mut self, v: usize) {
1057        let mut call_stack: Vec<(usize, Vec<usize>)> =
1058            vec![(v, self.adj[v].iter().copied().collect())];
1059        self.index[v] = Some(self.next_index);
1060        self.lowlink[v] = self.next_index;
1061        self.next_index += 1;
1062        self.stack.push(v);
1063        self.on_stack[v] = true;
1064
1065        while let Some((node, successors)) = call_stack.last_mut() {
1066            let node = *node;
1067            if let Some(w) = successors.pop() {
1068                if self.index[w].is_none() {
1069                    self.index[w] = Some(self.next_index);
1070                    self.lowlink[w] = self.next_index;
1071                    self.next_index += 1;
1072                    self.stack.push(w);
1073                    self.on_stack[w] = true;
1074                    call_stack.push((w, self.adj[w].iter().copied().collect()));
1075                } else if self.on_stack[w] {
1076                    self.lowlink[node] = self.lowlink[node].min(self.index[w].unwrap());
1077                }
1078            } else {
1079                // All successors processed: finalize this node.
1080                if self.lowlink[node] == self.index[node].unwrap() {
1081                    let mut component = Vec::new();
1082                    loop {
1083                        let w = self.stack.pop().unwrap();
1084                        self.on_stack[w] = false;
1085                        component.push(w);
1086                        if w == node {
1087                            break;
1088                        }
1089                    }
1090                    self.components.push(component);
1091                }
1092                call_stack.pop();
1093                if let Some((parent, _)) = call_stack.last() {
1094                    let parent = *parent;
1095                    self.lowlink[parent] = self.lowlink[parent].min(self.lowlink[node]);
1096                }
1097            }
1098        }
1099    }
1100}