truecalc_workbook/graph_cache.rs
1//! The cross-recalculation dependency-graph cache.
2//!
3//! [`DependencyGraph::build`](crate::DependencyGraph::build) is a pure function
4//! of the workbook's *structure*, and it used to run from scratch on every
5//! [`recalc`](crate::Workbook::recalc) and every
6//! [`recalc_incremental`](crate::Workbook::recalc_incremental) — together with
7//! the [`evaluation_order`](crate::DependencyGraph::evaluation_order) derived
8//! from it, the single largest fixed cost of a recalculation on a large
9//! workbook, paid whether one cell changed or none did.
10//!
11//! This module holds that result on the workbook and hands it back until a
12//! mutation could have changed it.
13//!
14//! # What the graph is a function of
15//!
16//! Reading [`DependencyGraph::build`](crate::DependencyGraph::build) and every
17//! resolver it calls, the graph depends on exactly:
18//!
19//! 1. the sheet **name set** (a reference to a missing sheet resolves to
20//! `Unresolved`, and every node is keyed by folded sheet name);
21//! 2. every formula cell's `(sheet, address, formula text)`;
22//! 3. the workbook's **named ranges** — their names and their `ref`s;
23//! 4. the workbook's **table declarations** — their names and their `ref`s;
24//! 5. **the text values stored in a declared table's header row**, because a
25//! structured reference resolves its column by matching the header cell's
26//! stored `Value::Text`.
27//!
28//! Point 5 is the one that is easy to get wrong, and it is why "writing a
29//! literal cannot change the graph" is **false** in general: writing `"qty"`
30//! into a table's header cell moves what `T[qty]` reads. It is true only when
31//! the workbook declares no tables at all, since with no table declaration no
32//! cell value can reach the graph.
33//!
34//! It is *not* a function of any other stored value, of spill footprints, or of
35//! tab order: spill occupancy is judged against the grid at recalc time, and
36//! the graph keys sheets by name, never by tab index.
37//!
38//! # The invalidation contract
39//!
40//! The cache is invalidated by the mutation, not by a later check, so the
41//! invariant is: **if the entry is `Some`, it equals a build against the
42//! workbook as it is now.** Every `&mut self` method of
43//! [`Workbook`](crate::Workbook) either invalidates or is documented here as
44//! provably structure-preserving. The two exceptions are:
45//!
46//! * `Workbook::set` / `Workbook::clear` of a **literal over a non-formula
47//! cell in a workbook that declares no tables** — by points 2 and 5 above,
48//! that adds and removes no node, no edge, and no header text the graph can
49//! see;
50//! * recalc's own value write-back, which rewrites an existing formula cell's
51//! stored value while preserving its formula text — same reasoning, and
52//! likewise only while no table is declared.
53//!
54//! Everything else — any formula write, any clear of a formula, any name or
55//! table definition, any sheet operation, and **every** `&mut` accessor that
56//! hands out interior state ([`Workbook::sheets_mut`](crate::Workbook::sheets_mut),
57//! [`Workbook::sheet_mut`](crate::Workbook::sheet_mut),
58//! [`Workbook::names_mut`](crate::Workbook::names_mut),
59//! [`Workbook::tables_mut`](crate::Workbook::tables_mut)) — invalidates. The
60//! `&mut` accessors invalidate on the *borrow*: what a caller does with the
61//! borrow is unobservable from here, so the only sound assumption is the worst
62//! one.
63
64use std::collections::BTreeSet;
65use std::hash::{Hash, Hasher};
66use std::sync::Arc;
67
68use crate::depgraph::{CellRef, DependencyGraph};
69
70/// A built dependency graph together with the evaluation order derived from
71/// it. Immutable once constructed: invalidation replaces the whole entry, it
72/// never edits one in place, which is what makes sharing it across a
73/// [`Workbook`](crate::Workbook) clone sound.
74///
75/// `pub`, not `pub(crate)`: [`Workbook::cached_graph_entry`](crate::Workbook::cached_graph_entry)
76/// hands this out to any crate that only holds `&Workbook` and wants to reuse
77/// a warm graph without rebuilding one (the wasm `precedentsOf`/`dependentsOf`
78/// binding). Its fields stay `pub(crate)` — external callers reach the graph
79/// through [`graph`](Self::graph), not by construction or field access.
80#[derive(Debug)]
81pub struct CachedGraph {
82 pub(crate) graph: DependencyGraph,
83 /// [`DependencyGraph::evaluation_order`](crate::DependencyGraph::evaluation_order)'s
84 /// order, for this exact graph.
85 pub(crate) order: Vec<CellRef>,
86 /// The cycle set from that same pass.
87 pub(crate) cycle: BTreeSet<CellRef>,
88 /// Every formula cell whose formula text names a volatile function
89 /// (`Workbook::is_volatile`), for this exact graph — computed once at
90 /// build time so `recalc_incremental`'s seeding step never re-derives it
91 /// per call (issue #983).
92 pub(crate) volatile: BTreeSet<CellRef>,
93}
94
95impl CachedGraph {
96 /// The dependency graph this entry caches.
97 pub fn graph(&self) -> &DependencyGraph {
98 &self.graph
99 }
100}
101
102/// The workbook's dependency-graph cache slot.
103///
104/// A field of [`Workbook`](crate::Workbook), so it must not disturb the
105/// workbook's value-object contract: it is skipped by serde, compares equal to
106/// every other cache, and hashes to nothing. Two workbooks with the same
107/// content are still equal and still hash the same whether or not either has
108/// recalculated.
109#[derive(Debug, Clone, Default)]
110pub(crate) struct GraphCache {
111 entry: Option<Arc<CachedGraph>>,
112 /// How many graphs this workbook has built. Instrumentation for the
113 /// cache's own tests: "builds per recalc" is the exact-count metric behind
114 /// the cache, and wall clock is too machine-dependent to assert on. Kept
115 /// per workbook rather than in a global counter so tests running in
116 /// parallel cannot perturb each other's reading.
117 builds: u64,
118}
119
120impl GraphCache {
121 /// The cached entry, if warm.
122 pub(crate) fn get(&self) -> Option<Arc<CachedGraph>> {
123 self.entry.clone()
124 }
125
126 /// Stores a freshly built entry and counts the build.
127 pub(crate) fn store(&mut self, entry: Arc<CachedGraph>) {
128 self.entry = Some(entry);
129 self.builds += 1;
130 }
131
132 /// Drops the entry. Idempotent, and always sound: the worst a spurious
133 /// invalidation costs is a rebuild.
134 pub(crate) fn invalidate(&mut self) {
135 self.entry = None;
136 }
137
138 /// How many graphs this workbook has built.
139 pub(crate) fn builds(&self) -> u64 {
140 self.builds
141 }
142
143 /// Whether an entry is currently held.
144 pub(crate) fn is_warm(&self) -> bool {
145 self.entry.is_some()
146 }
147}
148
149/// Every cache compares equal to every other: the cache is derived state, so
150/// two workbooks that differ only in whether they have recalculated are the
151/// same workbook (schema spec §8 — the document is the value).
152impl PartialEq for GraphCache {
153 fn eq(&self, _other: &Self) -> bool {
154 true
155 }
156}
157
158/// Hashes to nothing, for the same reason [`PartialEq`] ignores it: `a == b`
159/// must imply `hash(a) == hash(b)`.
160impl Hash for GraphCache {
161 fn hash<H: Hasher>(&self, _state: &mut H) {}
162}