1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! The authored-cell-index cache (issue #991 fallback design).
//!
//! `seed_spill_sensitive` calls [`AuthoredCellIndex::build`] the first time it
//! examines a range precedent (issue #927), but that build used to be a fresh
//! `O(authored cells)` sweep **every incremental recalc call** that touched
//! any range precedent at all — the same shape of bug `anchor_rectangles()`
//! had before #984, just one level down: the *decision to build* was already
//! lazy, but the *build itself* was never memoized across calls. This module
//! holds that built index on the workbook and hands it back until a mutation
//! could have changed it, mirroring [`crate::spill_anchor_cache`]'s own
//! `SpillAnchorCache` pattern (which itself mirrors [`crate::graph_cache`]).
//!
//! # What the index is a function of
//!
//! [`AuthoredCellIndex::build`] depends on exactly: every sheet's folded name,
//! and which addresses are authored (formula or literal — an entry exists in
//! `Worksheet::iter()`) on each sheet. It is **not** a function of any cell's
//! *value*, so recalc's own value write-back does not invalidate it — see the
//! next section.
//!
//! # The invalidation contract
//!
//! Unlike the spill-anchor cache, this one *can* stay warm across an ordinary
//! recalc: `Workbook::apply_changes` (`crate::recalc`) only ever rewrites a
//! cell that **already has formula text** (it reads the formula off the
//! existing authored cell before writing back through
//! `sheets_mut_untracked()`), so it can change a cell's *value* but never adds
//! or removes an authored-cell *entry*. A spilled cell is reconstructed on
//! read and never separately authored (schema spec §5), so placing, resizing
//! or removing a spill does not touch the authored-cell set either. The
//! narrow, correct condition — checked at every site that can actually add or
//! remove an authored cell — is:
//!
//! - **`Workbook::set`**: invalidate only when the write introduces a
//! genuinely *new* cell (`introduces_new_cell`, already computed there for
//! the cell-count cap) — an overwrite of an already-authored cell changes no
//! entry.
//! - **`Workbook::clear`**: invalidate only when a cell was actually removed
//! (`prev.is_some()`).
//! - Every sheet-structure mutation that hands out unobserved write access, or
//! changes the folded-name key space the index is keyed by
//! (`sheets_mut`, `sheet_mut`, `insert_sheet`, `remove_sheet`,
//! `rename_sheet`) — the same worst-case reasoning the graph cache and the
//! spill-anchor cache already apply to those methods. `move_sheet` is
//! deliberately **not** included: reordering tabs changes neither the
//! folded-name keys nor any sheet's authored cells (the graph cache and the
//! spill-anchor cache already exempt it for the same reason). Declaring or
//! redefining a table is also **not** included: a table is metadata over a
//! range, and never itself adds or removes an authored cell.
use ;
use Arc;
use crateAuthoredCellIndex;
/// The workbook's authored-cell-index cache slot.
///
/// A field of [`Workbook`](crate::Workbook), so it must not disturb the
/// workbook's value-object contract: it is skipped by serde, compares equal to
/// every other cache, and hashes to nothing. Two workbooks with the same
/// content are still equal and still hash the same whether or not either has
/// recalculated.
pub
/// Hand-written rather than derived: [`AuthoredCellIndex`] does not implement
/// [`std::fmt::Debug`] (it holds no public fields worth printing), and a
/// derived impl would require it to. The cache's own build count and warmth
/// are what a debug print of a [`Workbook`](crate::Workbook) actually needs.
/// Every cache compares equal to every other: the cache is derived state, so
/// two workbooks that differ only in whether they have recalculated are the
/// same workbook (schema spec §8 — the document is the value).
/// Hashes to nothing, for the same reason [`PartialEq`] ignores it: `a == b`
/// must imply `hash(a) == hash(b)`.