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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
//! Resource limits — the single source of truth for the structural caps of
//! the scope ADR (`2026-06-07-workbook-v1-scope.md`, Decision 5).
//!
//! These are **library constants, not schema constants** (schema spec §1):
//! a limit can rise without a schema version bump, and a document is always
//! validated against the limits of the library version that loads it. Raising
//! a limit is non-breaking; lowering one is not.
//!
//! Structural limits are enforced at [`Workbook::from_json`](crate::Workbook::from_json);
//! the serialized byte cap is enforced at serialize/deserialize time only
//! (computing canonical byte length per mutation would be O(document) per
//! edit — ADR Decision 5).
//!
//! # Two of these limits are enforced on `wasm32` only
//!
//! [`MAX_CELLS_PER_WORKBOOK`] and [`MAX_SERIALIZED_BYTES`] exist because of a
//! property of the 32-bit WebAssembly target, not because a workbook that
//! large is meaningless. Both constants keep their values on every target — a
//! 64-bit tool can still ask "would a browser load this?" **by comparing
//! against the constants directly.** The crate-internal checks that actually
//! reject, `exceeds_cell_cap` and `exceeds_serialized_cap`, only do so when
//! `target_arch = "wasm32"` — off `wasm32` they always return `false`, so they
//! answer a different, narrower question ("does *this build* reject it?") and
//! are not exported for that reason (see their doc comments).
//!
//! The `wasm32` constraint is the **address space**. A wasm32 linear memory is
//! indexed by a 32-bit pointer and so cannot exceed 4,294,967,296 bytes; a
//! growth request past that byte fails identically in Node and in desktop
//! browsers.
//!
//! # Measured cost per formula cell (the dependency-graph cache)
//!
//! Since the dependency-graph cache (`graph_cache` module), a formula cell's
//! **retained** cost on `wasm32` is no longer just the document — it is the
//! document *plus* the cached graph, kept alive between recalculations
//! instead of being rebuilt and dropped every time:
//!
//! | | wasm32 | native |
//! |---|---:|---:|
//! | document, retained | 106.9 B/cell | 183.4 |
//! | cache, retained | 545.2 | 855.7 |
//! | **idle total, retained** | **≈652 B/cell** | ≈1,039 |
//! | peak, mid-recalc | 856.7 | — |
//!
//! (Measured with a counting global allocator compiled for
//! `wasm32-unknown-unknown` and run in Node, at 160,000 formula cells.)
//!
//! The important nuance: **the peak was already ≈857 B/cell before this
//! cache existed**, because `DependencyGraph::build` allocated the same graph
//! transiently on every `recalc`/`recalc_incremental` call and dropped it
//! again at the end. So a *single* workbook's mid-recalc high-water mark is
//! unchanged — still bounded by the peak, not the idle floor — and the real
//! ceiling for one workbook actively recalculating at the cap is
//! 4,294,967,296 / 857 ≈ 5.0 million formula cells, the same before and after
//! this cache. (Before this cache, the previously-documented estimate here
//! was "a stored cell costs about 108 bytes as a number and about 149 as a
//! formula, roughly 29–40 million cells" — that was the *retained*, not the
//! *peak*, cost, and so was always an undercount of the true wall a
//! recalculating workbook hits; this cache didn't create that gap, it just
//! made the retained number closer to the peak one, which is why the section
//! below now matters.)
//!
//! What changed is the **idle** floor: 107 → 652 B/cell. A workbook sitting
//! in memory between edits, not currently recalculating, now costs roughly
//! 6× what it did, because the graph it built for its last recalculation is
//! still attached to it. [`Workbook::drop_derived_state`](crate::Workbook::drop_derived_state)
//! releases that cache (at the cost of a rebuild on the next recalc/query)
//! for a host that wants the old idle floor back for a workbook it isn't
//! actively using.
//!
//! What makes that wall worth a cap, rather than something to let fail on its
//! own, is *how* it fails. It is not a refusal a caller can catch and report:
//! the allocation failure aborts the entire wasm module. Because one wasm
//! instance normally backs every workbook a host has open, a single oversized
//! workbook destroys the unrelated ones alongside it. These caps turn that into
//! an ordinary [`WorkbookError`](crate::WorkbookError) raised before any of the
//! memory is asked for, and are currently the only thing that does.
//!
//! # Multi-workbook consequence of the idle floor
//!
//! The scenario the cap exists for (previous paragraph) is one wasm instance
//! holding several open workbooks, at most a few of them recalculating at
//! once. At the enforced 1,000,000-cell cap, with one workbook actively
//! recalculating (paying the ≈857 B/cell peak) and the rest idle:
//!
//! - **Before this cache**: an idle workbook retained ≈107 B/cell (document
//! only), so `(4,294,967,296 − 857,000,000) / 107,000,000 ≈ 32` more idle
//! workbooks fit alongside the recalculating one — **W ≈ 32** open
//! workbooks (rounding down) before the 4 GiB wall.
//! - **After this cache**: an idle workbook retains ≈652 B/cell (document
//! *and* cache), so `(4,294,967,296 − 857,000,000) / 652,000,000 ≈ 5` more
//! idle workbooks fit — **W ≈ 6** open workbooks.
//!
//! Whether that consequence justifies lowering [`MAX_CELLS_PER_WORKBOOK`], or
//! documenting the multi-workbook budget more explicitly to hosts, is a
//! decision for later — this paragraph is the number it should be made from,
//! not a call this module makes on its own.
//!
//! No other target has that wall. A 64-bit build is bounded by machine memory,
//! which is both far larger and operator-controlled, so a wasm-shaped cap there
//! only refuses documents the engine handles comfortably. (Exhausting machine
//! memory is still an abort rather than an error — but it is a host-level
//! failure at a height the operator chooses, not a fixed ceiling an ordinary
//! spreadsheet reaches.)
//!
//! This stays consistent with "library constants, not schema constants": off
//! `wasm32` these limits are effectively *raised*, which is non-breaking, and
//! the `wasm32` behaviour is unchanged, so nothing is lowered anywhere. A
//! document a 64-bit build accepts may of course exceed what a `wasm32` build
//! will load, which is the same version-dependent validation described above.
/// Maximum populated cells across the whole workbook. Spilled/materialized
/// cells count toward this cap (ADR Decision 5).
///
/// **Enforced on `wasm32` only** — see the module docs. This constant itself
/// keeps its value on every target; the crate-internal `exceeds_cell_cap`
/// predicate every enforcement site uses is what varies by build.
pub const MAX_CELLS_PER_WORKBOOK: usize = 1_000_000;
/// Maximum length of a `text` value, in Unicode scalar values.
pub const MAX_TEXT_LEN: usize = 50_000;
/// Maximum element count (`m × n`) of a single `array` value.
pub const MAX_ARRAY_ELEMENTS: usize = 1_000_000;
/// Maximum worksheets per workbook.
pub const MAX_SHEETS: usize = 256;
/// Maximum length of a sheet name, in Unicode scalar values (schema spec §3).
pub const MAX_SHEET_NAME_LEN: usize = 100;
/// Maximum row index, 1-based (schema spec §3 / ADR Decision 5).
pub const MAX_ROW: u32 = 10_000_000;
/// Maximum column index, 1-based: `ZZZ` = 18,278 (schema spec §3 / ADR Decision 5).
pub const MAX_COLUMN: u32 = 18_278;
/// Maximum verbatim formula length, in bytes.
pub const MAX_FORMULA_LEN: usize = 32 * 1024;
/// Maximum workbook-scoped named ranges.
pub const MAX_NAMED_RANGES: usize = 10_000;
/// Maximum workbook-scoped tables (structured-references spec §4,
/// truecalc/core#861) — the same magnitude as [`MAX_NAMED_RANGES`], since a
/// table costs the same order of workbook-scoped bookkeeping. This also
/// bounds the cost of the O(tables) scans a table incurs on every mutation
/// (`overlapping_table`, `expand_table_on_append` — the latter runs on every
/// `Workbook::set()`).
pub const MAX_TABLES: usize = 10_000;
/// Maximum serialized canonical JSON document size, in bytes (100 MiB).
/// A workbook exceeding this cannot be serialized, and `from_json` rejects
/// oversized inputs (ADR Decision 5).
///
/// **Enforced on `wasm32` only** — see the module docs. This constant itself
/// keeps its value on every target; the crate-internal
/// `exceeds_serialized_cap` predicate every enforcement site uses is what
/// varies by build. This cap is gated together with
/// [`MAX_CELLS_PER_WORKBOOK`] because the two were deliberately matched:
/// canonical JSON costs roughly 53 bytes per numeric cell and ~81 per formula
/// cell, so one million formula cells is about 81 MiB — 81% of this.
/// Relaxing the cell cap alone would only move the same address-space
/// failure from `set` to `to_json`.
pub const MAX_SERIALIZED_BYTES: usize = 100 * 1024 * 1024;
/// Whether a workbook holding `cells` populated cells breaches
/// [`MAX_CELLS_PER_WORKBOOK`] on a target that enforces it.
///
/// Always `false` off `wasm32`: the cap tracks the 32-bit address-space wall
/// described in the module docs, and no other target has one.
///
/// **Internal enforcement policy, not a public API.** This answers "does
/// *this build* reject it?", not "would a browser load this?" — the two
/// questions differ off `wasm32`, where this always returns `false`. A host
/// asking the browser-load question should compare against
/// [`MAX_CELLS_PER_WORKBOOK`] directly. `#[doc(hidden)]` rather than
/// `pub(crate)` because the gating integration test in `tests/` is a separate
/// crate and needs to reach this (CLAUDE.md §9 forbids inline `#[cfg(test)]`
/// in production sources); the precedent for that shape is
/// `DepGraph::formula_precedent_cells_examined`. Being `const fn` does not
/// mean "same result on every target" here — it means "evaluable at compile
/// time on whichever target is compiling", so a downstream `const` built
/// against this can bake in a native `false` without warning, and rustdoc
/// renders the host (x86_64) body, so docs.rs would otherwise show this
/// always returning `false` right next to a constants page that still says
/// 1,000,000.
pub const
/// Whether a serialized document of `bytes` bytes breaches
/// [`MAX_SERIALIZED_BYTES`] on a target that enforces it.
///
/// Always `false` off `wasm32`, for the reason given on
/// [`MAX_SERIALIZED_BYTES`].
///
/// **Internal enforcement policy, not a public API** — see
/// [`exceeds_cell_cap`]'s doc comment, which this mirrors: it answers "does
/// *this build* reject it?", a host asking "would a browser load this?"
/// should compare against [`MAX_SERIALIZED_BYTES`] directly, and `const fn`
/// here means "evaluable at compile time on whichever target is compiling",
/// not "same result everywhere".
pub const