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}