Viscosity
Review
Part VIII · Beyond JavaScript · Day 30

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.

About 15 minutes, including the exercises and quiz
By the end you will be able to
  • 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.

Intuition

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

Step throughTrack A, part by part
Eight boxes for Parts I to VIII of the book, with the highlighted part's chapters, what you built in it and where it lives in the repositoryPart Ich. 1–16Part IIch. 17–24Part IIIch. 25–44Part IVch. 45–60Part Vch. 61–72Part VIch. 73–88Part VIIch. 89–112Part VIIIch. 113–120Days 1 to 30WHAT YOU BUILTstep through to see each partWHERE IT LIVES
Thirty days, eight parts. Step through them: each one built something you can still find in the repository, and each one is used by the parts after it.
1/9

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:

PlaygroundThe whole pipeline

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:

Track A, measured
Speed-ups, longer is better; the dashed lines mark 1× (no change)
DRIP: BYTECODE VM vs TREE-WALKERzoo: tree-walker 8358 ms, VM 1749 mszoo4.78×properties: tree-walker 11853 ms, VM 2781 msproperties4.26×fib: tree-walker 23695 ms, VM 5685 msfib4.17×method_call: tree-walker 4106 ms, VM 1085 msmethod_call3.78×trees: tree-walker 53136 ms, VM 21327 mstrees2.49×binary_trees: tree-walker 19018 ms, VM 13078 msbinary_trees1.45×invocation: tree-walker 2962 ms, VM 2042 msinvocation1.45×equality: tree-walker 15533 ms, VM 16501 msequality0.94×instantiation: tree-walker 2742 ms, VM 3570 msinstantiation0.77×VISCOSITY: COMPILED vs ORIGINAL PROGRAMledger: 237 ms → 46 ms (tx=soa)ledger5.11×particles: 405 ms → 94 ms (ps=soa)particles4.3×heat: 364 ms → 110 ms (cells=soa)heat3.3×boids: 84 ms → 71 ms (fl=soa)boids1.18×nbody: 67 ms → 61 ms (bs=soa)nbody1.09×
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.

The VM won on most programs, and lost on equality and instantiation. Viscosity won big where a hot loop reads a few fields of a large array, and little on nbody and boids.

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 name viscosity belongs 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.
Definition

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.

Under the hood

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.

ExerciseViscosity in miniature

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;
}
Extra practiceA bytecode VM in a dozen lines

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. 1.

    Which Part I idea became Viscosity’s storage for numeric fields?

  2. 2.

    In the particle program, which analysis decided that ps holds five fields in a stable order?

  3. 3.

    Why is zoo_batch left out of the Drip chart?

  4. 4.

    The Drip VM was slower than the tree-walker on two programs. What is the right conclusion?

  5. 5.

    Which statement about the Rust port was true when this chapter was written?

  6. 6.

    How can anyone check that the Rust Viscosity makes “the same decisions” as the TypeScript one?

References

  1. [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. [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. [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)