120. Capstone: what you have built
Thirty days ago you wrote a function that doubled a number. Since then you have built two implementations of a language, read the insides of real engines, and written a compiler that rearranges memory. Here is all of it in one place, with the measurements, the rough edges, and what comes next.
- Trace one program through every stage of Viscosity, naming the chapter behind each stage.
- Read Track A’s committed measurements, including the ones that went the wrong way.
- List what is unfinished, and say why each item is hard or just undone.
- Describe what Track B rebuilds in Rust, and how it is checked against Track A.
Where you started
This was the first program in the book:
function double(n) {
return n * 2;
}
That chapter promised that everything later would be built from that one idea: name a piece of work, then build bigger work out of named pieces. Since then, those pieces have become scanners, parsers, an interpreter, a compiler, a virtual machine, a garbage collector, an SSA intermediate representation, a stack of analyses, and an optimizer that changes how a program’s data sits in memory, for two languages.
At the top of a long climb you turn around. The path you took is laid out below you, and you can see how each stretch led to the next: the steep bit that felt pointless was the one that got you over the ridge.
Eight parts, one machine
Nothing in that list stands alone. Typed arrays from Part I became Viscosity’s columns. The cache lines of Part II are why those columns win. The stack machine of Part IV came back as WebAssembly’s execution model. Shapes and deoptimization in Part V explain both why the research could transform arrays at run time and why Viscosity, which can’t deoptimize, has to prove things instead[2]. The SSA and dataflow of Part VI are what those proofs are made of.
One program, all the way down
Here is a particle simulation. Run it through the real compiler, then read the report with the whole book in mind:
The CLI says the same thing about it:
ps (line 1) {x, y, vx, vy, m} → struct
of arrays
· loop at line 7 in <module>() touches
4 of 5 fields (vy, y, vx, x)
· loop at line 13 in <module>() touches
1 of 5 fields (x)
· typed Float64Array columns for all 5
fields
· about 10000 elements
· 5 of 5 element accesses proven in
bounds
To get there, acorn parsed the source into an ESTree (chapter 92). The
front-end lowered it to VIR, in SSA form (chapters 93, 77–78).
Collection discovery found ps (96), the shape analysis saw five fields
in a stable order (97), and the access patterns found two hot loops
(98). The safety analysis found no escape (99), the cost model liked
10,000 elements (101), and the code generator replaced the objects with
a container of five Float64Arrays that doubles its capacity as it
grows (103–105). Every line of the report was written by the planner
as it decided (110). Behind all of it, the differential tests run every
corpus program both ways and compare the output (112).
What it measured
Two numbers summarize Track A: how much the bytecode VM beat the tree-walker, and how much Viscosity sped up whole programs. Both come from the committed result files, from one run each on a shared cloud machine:
Where these numbers come from
Drip: companion/ts/results/drip-bench-2026-10-31.json, Node v22.23.4, Intel(R) Xeon(R) Processor @ 2.10GHz. Best of N, each in a fresh process. Includes compile time. Shared cloud VM. Not shown: zoo_batch (runs for a fixed time) and string_equality (the VM failed to compile it).
Viscosity: engine/ts/packages/bench/results/e2e-node-2026-10-31.json, Node v22.23.4, V8 12.4.254.21-node.57, Intel(R) Xeon(R) Processor @ 2.10GHz. Median of N fresh-process runs of each program’s own timed section. Shared cloud VM.
Read the losses as carefully as the wins. The VM was slower than the
tree-walker on two programs. Part IV named the suspects: the VM runs
its own heap and garbage collector on top of V8’s, and its own hash
tables where the tree-walker borrows V8’s Map. One benchmark, string_equality,
doesn’t run on the VM at all: it fails with “Too many constants in one
chunk”, because a chunk’s constant index is one byte, so 256 constants
is the limit. And Viscosity’s smallest wins came on nbody, where
every body reads every field of the others, and on boids. The cost
model chose struct of arrays for those just as it did for the big
wins: it can’t yet tell them apart.
What isn’t done
An honest capstone lists the unfinished parts:
- Not on npm. Chapter 119 found and fixed three bugs between the
repository and
npm install, but nothing is published yet, and the nameviscositybelongs to someone else. - No WebAssembly back end (chapter 117). Every version of Viscosity emits JavaScript.
- Many ordinary programs are refused (chapter 118): sorting, classes, destructuring, varying shapes, exported arrays. Some of those are chores; some are research.
- The cost model is a heuristic, with a 64-element threshold and measurements from one machine.
A capstone is the piece of work at the end of a course that uses everything before it at once. Its point is less the thing itself than seeing how the parts fit, and being able to say exactly which parts are solid and which are not.
Track B: the same machine, in Rust
Track B, chapters 121 to 200, rebuilds the two biggest things you made
in a language where memory layout is yours to control. First the Rust
language itself, through ownership, borrowing, arenas and
struct-of-arrays by hand (Part IX). Then Drip’s VM again, as drip-rs
(Part X), which already exists in book/companion/rust and has
benchmark results committed beside it. Then Viscosity in Rust, parsing
with oxc and producing the same decisions as the TypeScript engine
(Part XI), and finally a command-line tool, release engineering and a
final benchmark matrix (Part XII).
“The same decisions” is checkable, because Track A left a contract
behind. For every corpus and benchmark program, in both bounds modes,
the TypeScript engine’s compiled code and explain report are stored as
files in engine/rust/tests/expected, and the Rust port has to
reproduce them byte for byte.
The Rust port is complete. Its workspace has five crates: vir,
core, frontend-js, a cli that builds a viscosity binary, and
wasm, which wraps the compiler for the browser. Its parity test
matches every stored fixture byte for byte, and the WebAssembly build
reproduces them too. Warm and in-process, it compiles the benchmark
programs about 5 to 8 times faster than the TypeScript engine
(engine/rust/results/compile-speed-2026-10-31.md). Track B is the
story of how it got there, one chapter at a time. Building a language twice, a
tree-walker and then a bytecode VM, and rebuilding it in a lower-level
language, is the shape this book borrowed from Crafting
Interpreters[1]. Track B takes the same idea one
level further, to the optimizer.
Your turn
The core exercise is Viscosity in miniature, done at run time the way the research does it[3]: check the shape, bail out if anything is off, otherwise build columns. The extra one is a whole bytecode VM in a dozen lines.
Write optimize(ps), a tiny run-time version of Viscosity. If ps is empty, or its records don’t all have the same fields in the same order as the first one, or any field value is not a number, bail out: return ps itself, unchanged. Otherwise return { length, cols }, where cols has one Float64Array per field holding that field of every record, in order.
function optimize(ps) {
return ps;
}Write run(code), a stack machine like Part IV’s. code is an array of instructions: 'push' followed by a number pushes that number; 'add', 'sub' and 'mul' pop two values and push the result (for sub, the value pushed first minus the value pushed second). When the code ends, return the top of the stack.
function run(code) {
const st = [];
for (const op of code) {
if (typeof op === 'number') {
st.push(op);
}
}
return st.pop();
}What you learned
- Track A built two implementations of Drip, read real engines, and built Viscosity: parse, lower to VIR, analyze, decide, generate columns, explain, and test differentially.
- One program passes through a dozen chapters on its way to SoA, and every step can be named.
- The measurements include losses: the VM on two programs, a 256-constant limit, small wins where every field is read.
- What’s unfinished is known: npm, WebAssembly, refused programs, and the cost model.
- Track B rebuilds the VM and the compiler in Rust, checked byte for byte against Track A’s stored outputs.
Check your understanding
0/6- 1.
Which Part I idea became Viscosity’s storage for numeric fields?
- 2.
In the particle program, which analysis decided that
psholds five fields in a stable order? - 3.
Why is
zoo_batchleft out of the Drip chart? - 4.
The Drip VM was slower than the tree-walker on two programs. What is the right conclusion?
- 5.
Which statement about the Rust port was true when this chapter was written?
- 6.
How can anyone check that the Rust Viscosity makes “the same decisions” as the TypeScript one?
References
- [1] Robert Nystrom (2021). Crafting Interpreters. Free web edition at craftinginterpreters.com (© 2015–2021); source, text and test suite at github.com/munificent/craftinginterpreters. github.com/munificent/craftinginterpreters (checked 2026-10-31)
- [2] Lukas Makor, Sebastian Kloibhofer, David Leopoldseder, Daniele Bonetta, Lukas Stadler, Hanspeter Mössenböck (2022). Automatic Array Transformation to Columnar Storage at Run Time. Proceedings of the 19th International Conference on Managed Programming Languages and Runtimes (MPLR ’22), ACM, pp. 16–28. doi:10.1145/3546918.3546919 (checked 2026-10-31)
- [3] Sebastian Kloibhofer, Lukas Makor, David Leopoldseder, Daniele Bonetta, Lukas Stadler, Hanspeter Mössenböck (2023). Control Flow Duplication for Columnar Arrays in a Dynamic Compiler. The Art, Science, and Engineering of Programming, vol. 7, issue 3, article 9. doi:10.22152/programming-journal.org/2023/7/9 (checked 2026-10-31)