Recalculation
Editing a workbook with set/get/clear, what recalc does, full versus incremental recomputation, and how a RecalcContext makes volatile functions like NOW and TODAY deterministic.
The previous chapter built the map: a
DependencyGraph that knows which cell feeds which, and in what order they
must be evaluated. Recalculation is what walks that map and fills in the
answers — it is the step that turns =B1*12 from an empty placeholder into
14400.
This chapter covers the two halves of a live workbook: editing it (writing and clearing cells) and recomputing it (running the formulas). And it covers the property that makes the whole thing trustworthy: determinism — the same workbook, recomputed the same way, always gives byte-for-byte the same result.
Editing a workbook
In the Workbooks section you built a workbook by inserting cells directly into
a sheet's grid. That is fine for constructing one from scratch, but a live
workbook needs an editing API that validates as it goes. That is set, get,
and clear:
use truecalc_workbook::{Workbook, Worksheet, EngineFlavor, CellInput, Value, Address};
let mut wb = Workbook::new(EngineFlavor::Sheets);
wb.add_sheet(Worksheet::new("Budget")).unwrap();
let a1: Address = "A1".parse().unwrap();
let b1: Address = "B1".parse().unwrap();
let b2: Address = "B2".parse().unwrap();
// A literal value.
wb.set("Budget", b1, CellInput::Literal(Value::Number(1200.0))).unwrap();
// A formula - stored verbatim, validated for syntax, but NOT yet evaluated.
wb.set("Budget", b2, CellInput::Formula("=B1*12".to_owned())).unwrap();Three things in that snippet are worth knowing:
- A write is one of two shapes.
CellInputhas exactly two variants:CellInput::Literal(value)for a typed value andCellInput::Formula(text)for a formula (stored verbatim, leading=and all). The surface layers (the browser build, the AI-agent MCP, the hosted API) decide how to turn a user's raw keystrokes into one of these — a leading=means a formula, anything else is a literal. setvalidates but does not evaluate. It checks the formula's syntax against the workbook's locked engine and enforces size limits, but it leaves the cell's valueEmptyuntil the next recalc. Writing a formula and reading it back before recalculating gives you the formula text and an empty value — exactly the honest behavior the Workbooks section warned about, now with the missing half (recalc) about to arrive.- Clearing means removing.
wb.clear("Budget", b1)deletes the cell's entry. There is no "write an empty value" — an empty box is simply an absent cell, which is whyCellInput::Literal(Value::Empty)is rejected. This keeps the sparse grid and the portability guarantee intact.
To read a cell back, get returns the authored cell (the literal or
formula you wrote), and resolved returns the effective value at any
address — which matters once arrays start to spill.
Named ranges have the same CRUD shape: define_name, redefine_name,
remove_name, and name to look one up. Defining a name validates that its
target sheet exists, so you can never create a dangling name.
Recomputing: full and incremental
Now the payoff. recalc walks the dependency graph in topological order and
writes each fresh result back into the grid:
use truecalc_workbook::RecalcContext;
// A deterministic context (explained below). Etc/GMT, a fixed instant, seed 0.
let ctx = RecalcContext::new(0, "Etc/GMT", 0).unwrap();
// Full recalc: evaluate every formula cell, precedents first.
let changes = wb.recalc(&ctx);
// Now B2 holds 14400.0 - B1 (1200) was read before B2 (=B1*12) evaluated.recalc returns a Vec<Change> — one Change per cell whose value actually
moved, each carrying the cell's sheet, addr, old value, and new value.
The list is returned in a pinned order (by sheet tab index, then row, then
column), so it is itself reproducible. Returning the changes as plain data —
rather than firing callbacks — keeps the workbook a value
object.
A full recalc redoes everything, which is wasteful when you have just edited one cell. So there is a second mode:
// You changed B1. Recompute only what that edit affects.
let changes = wb.recalc_incremental(&ctx, &[("Budget".to_owned(), b1)]);recalc_incremental takes the cells an edit touched and recomputes only their
transitive dependents — the cells that read the edit, and the cells that
read those, and so on — reusing every stored result outside that closure. If
you changed B1, it redoes B2 and B3 but not the unrelated Z9.
The two modes are guaranteed to agree. For the same workbook and the same
context, an incremental recalc produces exactly the subset of changes a full
recalc would — the crate's test suite asserts
recalc_incremental(edits) === recalc(). Incremental is just full recalc
restricted to the part that could have changed.
There is one deliberate exception to "only recompute the dependents": volatile cells are always recomputed, even by an incremental pass, no matter what you edited. That is the next section.
Determinism and the RecalcContext
You may have noticed both recalc calls take a RecalcContext. This little
struct is what makes recalculation deterministic: the same workbook plus
the same context always produces a byte-identical grid. That property is the
backbone of TrueCalc — it is what lets a workbook computed in the browser, on
a server, or inside an AI agent agree to the last digit.
The context exists because some functions are volatile — they do not
depend only on the cells they read, but on something outside the spreadsheet,
like the current time or a random number. NOW() and TODAY() read the
clock; RAND() rolls dice. If you let them read the real clock, two recalcs
a second apart would disagree, and the workbook would no longer be
reproducible.
The RecalcContext pins those outside influences to fixed inputs:
// timestamp_ms (UTC), an IANA timezone id, and an RNG seed.
let ctx = RecalcContext::new(
1_749_283_200_000, // a fixed UTC instant, in Unix milliseconds
"America/New_York", // the timezone NOW()/TODAY() are rendered into
42, // the seed for random draws
).unwrap();timestamp_msis the instantNOW()andTODAY()see, as milliseconds since the Unix epoch (UTC). Recalc converts it to a local spreadsheet serial number and hands that to the engine in place of the real clock. Same instant in, sameNOW()out.timezoneis the IANA zone the instant is localized into —"Etc/GMT","America/New_York", and so on. Crucially, this conversion runs against a vendored copy of the IANA timezone database that ships inside TrueCalc, never the host OS's tz tables. So the same instant in the same zone gives the same local date on every machine, regardless of how the operating system happens to be configured.RecalcContext::newreturnsNoneif the zone string is not a real IANA id.rng_seedkeys the random functions so a given cell draws the same "random" number every time.
Honest status of random functions. The rng_seed is carried by the
context today so the API is stable, but the core engine's RAND /
RANDBETWEEN / RANDARRAY still read the system clock directly and do not yet
accept a per-cell key — wiring the seed all the way through requires a change
in the core crate and is planned for a later phase. So right now recalc is
fully deterministic for every workbook that does not call a random function
(which is every conformance fixture). NOW() / TODAY() determinism, by
contrast, is wired up and working through the vendored timezone database.
Because dates are the part that is fully pinned, it is worth seeing that the
engine's date math itself is fixture-verified. The context just decides which
instant NOW() reports; the arithmetic on the resulting serial is the same
engine you met in Foundations:
# A date is a serial number; arithmetic on it is ordinary number math.
# Fixture: workbook.tsv - "date arithmetic across month boundary" (=DATE(2026,6,30)+1)
=DATE(2026,6,30)+1 // => 46204# The serial epoch the context localizes NOW()/TODAY() against.
# Fixture: workbook.tsv - "epoch anchor: serial zero formatted" (=TEXT(0,"yyyy-mm-dd"))
=TEXT(0,"yyyy-mm-dd") // => 1899-12-30(NOW() and TODAY() themselves are volatile, so they have no fixed
expected value and cannot appear in a conformance fixture — that is exactly why
they need a RecalcContext to be reproducible at all. What the fixtures pin is
the date-serial system the context feeds them into.)
What you learned
- Edit a workbook with
set(aCellInput::LiteralorCellInput::Formula),get(the authored cell), andclear(remove the entry);setvalidates syntax but does not evaluate. recalcrecomputes every formula in topological order and returns the ordered list ofChanges;recalc_incrementalrecomputes only an edit's transitive dependents and is guaranteed to agree with a full recalc.- A
RecalcContextmakes recalc deterministic: same workbook + same context = byte-identical grid. - The context pins volatiles —
NOW()/TODAY()to a fixed instant and timezone via a vendored IANA database; random functions carry a seed but full RNG determinism is still pending a core change (be aware of this).
Next: what happens when the graph loops back on itself — circular references.
Formulas that depend on other cells
How one cell's formula reads another, what a dependency graph is, why evaluation order matters, and how TrueCalc derives precedents and dependents from your formulas.
Circular references
What a circular reference is, why a spreadsheet cannot resolve one, how TrueCalc detects cycles instead of looping forever, and the error every cell on a cycle takes.