Arrays and spill
When one formula returns a whole grid of values, how that array spills from its anchor into a rectangle, what blocks a spill, and why spilled cells are never serialized.
Most formulas return a single value: =B1*12 is one number. But some formulas
return a whole grid — many rows and columns at once. When that happens, the
result has to go somewhere, and the somewhere is a rectangle of cells next to
the formula. That overflow is called spill, and it is the last piece of the
Phase 3 runtime.
A formula that returns more than one value
You have already seen array values in passing. A literal array like
{1,2,3} is a one-row grid; {1;2;3} is a one-column grid (the , separates
columns, the ; separates rows). Plenty of functions return arrays too —
filtering a list, transposing a block, generating a sequence.
When a function operates over an array, the engine handles the grid as a single value. The conformance fixtures verify this scalar behavior — for example, collapsing an array to one cell, or feeding an array result into another function:
# An array constrained to a single cell collapses to that scalar.
# Fixture: array.tsv - "constrain to single cell returns scalar"
=ARRAY_CONSTRAIN({10,20;30,40},1,1) // => 10# An array result flows straight into another function.
# Fixture: array.tsv - "constrain result used in SUM"
=SUM(ARRAY_CONSTRAIN({1,2,3;4,5,6},1,3)) // => 6That is the engine's view: an array is just a value, and a formula can produce one. The workbook's job is to decide where a multi-cell array goes when it is the result of a cell. That is spill.
The spill model: anchor plus rectangle
When a formula cell produces an m x n array, the cell it lives in becomes the
anchor, and the array fills the rectangle whose top-left corner is the
anchor:
col C col D
row 1 =SEQUENCE(2,2) . <- C1 is the anchor (authored formula)
+----------+----------+
| 1 | 2 | <- C1, D1
+----------+----------+
| 3 | 4 | <- C2, D2
+----------+----------+The cell C1 holds the formula and the full 2 x 2 array. The other three
cells — D1, C2, D2 — are spilled cells. They show values, but you did
not author them; they are a view materialized from the anchor's array.
This split between the authored anchor and the derived spilled cells is the key idea, and it has two important consequences.
Spilled cells are computed but never stored
You can read a spilled cell — its value is reconstructed from the anchor's array — but it is never serialized. When the workbook is saved to JSON, only the anchor's array goes on the wire. The spilled cells are rebuilt on load by a simple rule: for each stored array, lay it into the rectangle anchored at its cell.
This is why the workbook has two read methods, which the recalculation chapter mentioned:
// `get` returns only AUTHORED cells. A spilled cell has no authored
// entry, so get returns None for it.
let anchor_cell = wb.get("Sheet1", "C1".parse().unwrap()); // Some(...)
let spilled = wb.get("Sheet1", "D1".parse().unwrap()); // None
// `resolved` returns the EFFECTIVE value at any address, following spills,
// and tells you which anchor a spilled cell came from.
let r = wb.resolved("Sheet1", "D1".parse().unwrap()).unwrap();
// r.value == the array element at D1
// r.anchor == Some(C1) (None for an authored cell)Keeping spilled cells out of storage is what preserves the portability guarantee: there is exactly one way to write the workbook, and a million spilled cells never bloat the file.
A spill can be blocked
The rectangle a formula wants to spill into might not be empty. If any cell in the target rectangle (other than the anchor itself) already holds something — a literal you typed, another formula, or a cell already claimed by an earlier spill this recalc — the spill is blocked. When that happens:
- the anchor takes the blocked-spill error instead of an array, and
- no array is stored at all — nothing is partially written.
col C col D
row 1 =SEQUENCE(2,2) .
+----------+----------+
| #REF! | | <- C1 anchor: blocked, holds the error
+----------+----------+
| | "hi" | <- D2 was already occupied -> it blocked the spill
+----------+----------+A spill is also blocked if the rectangle would run off the edge of the sheet's address bounds. Either way the rule is all-or-nothing: a spill that cannot lay down its entire rectangle lays down none of it, and the anchor reports the error.
Because spills resolve in deterministic recalc order (the same topological, sheet-then-row-then-column order from earlier chapters), two arrays competing for the same cell resolve reproducibly: the earlier anchor wins, and the later one blocks. Same workbook, same context, same outcome — every time.
Honesty about the exact code. Google Sheets reports a blocked array
expansion as #REF!, and that is the code the workbook crate uses
(BLOCKED_SPILL_ERROR). As with circular references,
there is not yet a blocked-spill conformance fixture in truecalc/core —
the committed workbook fixtures cover cross-sheet refs, named ranges, and date
typing, not spill. So per this guide's
fixture-citation rule: the crate's behavior is verified by
its own tests (a blocked anchor takes the error, stores no array, and leaves
the target cells untouched), and the exact code comes from the workbook
JSON schema spec, to be re-confirmed against a real spreadsheet when a spill
fixture lands.
One edge still being pinned. Aggregating over a spill range (for example,
=SUM(C1#) summing a neighbor's spilled output, or a plain range that overlaps
spilled cells) is an area where the exact result is still being verified against
real-spreadsheet behavior. This guide does not yet teach a worked
sum-over-spill result, because we will not assert a number we have not pinned to
a fixture. Reading individual spilled cells with resolved is solid; treat
range math that leans on spilled cells as not-yet-documented until the fixtures
catch up.
What you learned
- A formula can return a whole array; the cell it lives in is the anchor, and the array spills into the rectangle below-and-right of it.
- The anchor is authored and stores the array; the other cells in the
rectangle are spilled — readable via
resolved, returningNonefromget, and never serialized (only the anchor's array is saved). - A spill is blocked (anchor takes a
#REF!-class error, no array stored) if any target cell is already occupied or the rectangle leaves the sheet; it is all-or-nothing and resolves in deterministic order. - The exact blocked-spill code is not yet fixture-pinned (no spill fixture in core) — behavior is test-verified, code from the schema spec.
- Sum-over-spill range aggregation is still being pinned and is deliberately not taught as a fixed result yet.
This closes the Advanced runtime arc: you now know how cells depend on each other, how a workbook recomputes deterministically, what happens when dependencies loop, and how a single formula fills many cells. From here the guide moves on to the Platforms — running TrueCalc in the browser, on Node, through MCP for AI agents, and via the hosted API.