Viscosity’s internals, and how to learn compiler engineering from scratch
By Tariq Shams
Having found no project doing something which I expected to be deterministically possible, I created Viscosity, the world’s first automatic, ahead-of-time Array of Structures to Structures of Arrays compiler for any language, starting with JavaScript and TypeScript, with more languages to come. However, I didn’t just want to create documentation. I wanted others to be able to learn how to create something like this, regardless of their engineering background. So I wrote a book with the help of Opus 5.5 (2026 frontier LLMs are great for writing technical documentation, no surprise given the training corpus, and given that programming is uniquely suited to reinforcement learning with verifiable rewards) that takes someone with beginner-level programming experience, or someone who is already an expert engineer (it doesn’t matter), to being able to write their own programming language, a compiler, and Viscosity itself. You build it all in TypeScript first (which the book teaches) and then again in Rust (which the book also teaches), in the spirit of how Robert Nystrom wrote Crafting Interpreters (an excellent book which I wish I had found when I took my compilers class in undergrad, where I had to read a theoretical compiler book to tease out the implementation details).
Given that AI is rapidly extending the capabilities of us engineers, I didn’t want to just create Viscosity, but rather a community that learns from each other, with this book being a start.
Additionally, while you may command and direct LLMs and their agentic harnesses to do things for you, never outsource your understanding and your reasoning; the knowledge you gain will help you connect dots years from now.
After you read this book, I believe you will be able to make meaningful contributions to Viscosity and make software faster for humanity (and hopefully enjoy the journey as well).
Tariq
Contents
200 of 200 chapters written so far. Planned chapters are greyed out.
- Part I. JavaScript for engine builders Days 1–4Track A · TypeScript
Functions, arrays, objects, references, closures, classes, strings, errors, modules, TypeScript, testing, and the raw bytes behind typed arrays.
- 1Functions: naming a piece of work
- 2Arrays: a row of numbered boxes
- 3Objects: things with named parts
- 4References: two names, one object
- 5Functions as values
- 6Closures: functions that remember
- 7Classes, methods and this
- 8Map and Set
- 9Recursion and the call stack
- 10Strings up close
- 11Errors and exceptions
- 12Modules: import and export
- 13TypeScript I: types as labels
- 14TypeScript II: unions, narrowing and generics
- 15Testing your code
- 16Bits, bytes and typed arrays
- Part II. How machines run code Days 5–6
Memory, CPU caches and cache lines, prefetching, SIMD, how JavaScript stores numbers and objects, AoS vs SoA measured, and how to benchmark honestly.
- Part III. A tree-walking interpreter Days 7–11
Build Drip, a small language: grammars, scanning, recursive descent and Pratt parsing, evaluation, environments, closures, resolution, classes.
- 25A map of the territory
- 26Meet Drip
- 27Tokens and lexemes
- 28Writing a scanner
- 29Numbers, strings, names and keywords
- 30Grammars
- 31Syntax trees
- 32Recursive descent parsing
- 33Pratt parsing
- 34Syntax errors and recovery
- 35Evaluating expressions
- 36Statements and state
- 37Environments and scope
- 38Control flow
- 39Functions in Drip
- 40Return and the call stack
- 41Closures in Drip
- 42Resolving variables
- 43Classes and instances in Drip
- 44Inheritance, and measuring the tree-walker
- Part IV. A bytecode virtual machine Days 12–15
Compile Drip to bytecode and run it on a stack VM: hash tables, string interning, call frames, upvalues and a garbage collector.
- 45Why bytecode?
- 46Chunks and a disassembler
- 47A stack-based VM
- 48Compiling expressions
- 49Representing values
- 50Strings and heap objects
- 51Hash tables
- 52String interning
- 53Global variables in the VM
- 54Local variables and stack slots
- 55Jumps: if, while and for
- 56Calls and call frames
- 57Closures and upvalues
- 58Garbage collection: mark and sweep
- 59Classes and methods in the VM
- 60Making the VM fast
- Part V. Inside real JavaScript engines Days 16–18
V8’s tiers, shapes and inline caches, elements kinds, speculation and deoptimization, garbage collection, other engines, and the columnar-array research.
- Part VI. The compiler middle-end Days 19–22
Intermediate representations, control-flow graphs, dominators, SSA, dataflow analysis, classic optimizations, alias and escape analysis, and pass managers.
- Part VII. Building Viscosity Days 23–28
The plugin architecture, VIR, the JavaScript front-end, collection discovery, safety analysis, the cost model, SoA and AoSoA code generation, materialization, PGO, explain, and testing it all.
- 89The architecture of Viscosity
- 90The plugin API
- 91VIR: Viscosity’s IR
- 92Parsing real JavaScript
- 93Lowering JavaScript to VIR
- 94Emitting JavaScript
- 95Round-trip testing the front-end
- 96Finding collections
- 97Record shapes
- 98Access patterns
- 99Proving it safe
- 100What Viscosity refuses to touch
- 101The cost model
- 102Opting in with @viscosity
- 103Generating SoA storage
- 104Rewriting field accesses
- 105Growing collections
- 106Identity and materialization
- 107AoSoA code generation
- 108Hot/cold splitting
- 109Profile-guided optimization
- 110viscosity explain
- 111Benchmarking Viscosity end to end
- 112Differential testing at scale
- Part VIII. Beyond JavaScript Days 29–30
A Python front-end plugin, WebAssembly as a target, the research frontier, and shipping Viscosity.
- Part IX. Rust for TypeScript developers Days 31–36Track B · Rust
Ownership and borrowing drawn as pictures, structs, enums and match, traits and generics, lifetimes, testing, smart pointers, arenas, memory layout and WebAssembly.
- 121Why Rust? Hello, cargo
- 122Variables, types and mutability
- 123Functions and expressions
- 124Ownership
- 125Borrowing
- 126Slices and strings
- 127Structs and impl
- 128Enums and match
- 129Option and Result
- 130Vec and HashMap
- 131Traits
- 132Generics and trait bounds
- 133Lifetimes
- 134Iterators and closures in Rust
- 135Modules, crates and visibility
- 136Testing in Rust
- 137Box, Rc and RefCell
- 138Arenas and typed indices
- 139Memory layout in Rust
- 140Struct of arrays in Rust by hand
- 141Unsafe Rust
- 142Measuring Rust performance
- 143Rust and WebAssembly
- 144Error handling at scale
- Part X. Drip in Rust Days 37–42
Rebuild the Drip bytecode VM in Rust, with arenas and a garbage collector, and benchmark it against the TypeScript versions.
- 145Planning drip-rs
- 146Tokens as enums
- 147The scanner in Rust
- 148Chunks and opcodes in Rust
- 149Values in Rust
- 150The VM loop in Rust
- 151A Pratt compiler in Rust, part 1
- 152A Pratt compiler in Rust, part 2
- 153Heap objects in an arena
- 154Strings and interning in Rust
- 155Globals and locals in Rust
- 156Control flow in Rust
- 157Functions and call frames in Rust
- 158Closures and upvalues in Rust
- 159A garbage collector in Rust
- 160Classes and instances in Rust
- 161Methods and initializers in Rust
- 162Inheritance in Rust
- 163Errors and diagnostics in Rust
- 164Testing against the shared corpus
- 165Profiling the Rust VM
- 166Optimizing the Rust VM
- 167Benchmarks: three implementations
- 168What Rust bought us
- Part XI. Viscosity in Rust Days 43–49
Port the engine: VIR with typed indices, trait-based plugins, the oxc front-end, analyses, code generation, WebAssembly, and differential tests against the TypeScript engine.
- 169The Rust workspace
- 170VIR in Rust
- 171The plugin trait
- 172Parsing with oxc
- 173Lowering oxc’s AST to VIR
- 174Emitting JavaScript from Rust
- 175Round-trip tests across engines
- 176Dominators and SSA in Rust
- 177A generic dataflow framework
- 178Collection discovery in Rust
- 179Shape inference in Rust
- 180Escape and alias analysis in Rust
- 181Access patterns in Rust
- 182The cost model in Rust
- 183SoA code generation in Rust
- 184AoSoA and hot/cold in Rust
- 185explain in Rust
- 186PGO in Rust
- 187The Python plugin in Rust
- 188TypeScript vs Rust: differential tests
- 189Compiling the engine to WebAssembly
- 190The npm package
- 191The browser playground
- 192How fast is the compiler?
- 193Fuzzing the Rust engine
- 194Diagnostics
- 195Compiling in parallel
- 196Keeping it healthy
- Part XII. Shipping Days 50
The command-line interface, release builds, the TypeScript vs Rust benchmark matrix, and the capstone.