How your code runs
Follows one file from cruft app.js to a result through each stage: loading and TypeScript erasure, the Parsimony parser, the Distil bytecode compiler, the Exegesis interpreter, LeJIT native compilation, and Trash Panda garbage collection. Explains what each stage settles before handing the rest to the next.
In a Node app, the path from source to result is a black box. V8 runs your JavaScript through an interpreter (Ignition) and an optimizing compiler (TurboFan), with deopts, hidden classes, and inline caches that decide your speed. You tune it from the outside with flags and never see it work.
Cruft runs the same shape of pipeline in one binary, and we built every stage to be inspectable. This page follows one file from cruft app.js to a result. Each stage settles what it can prove about the program and hands the rest to the next, so by the time code executes, the expensive decisions are already made.
Loading
Cruft runs JavaScript. A .js, .mjs, or .cjs file goes straight in, with no build step. A .ts, .mts, or .cts file is first stripped of its type annotations to plain JavaScript, the erase-only model Node uses for --strip-types. We do not type-check the program; that stays your editor's or tsc's job. The loader either erases cleanly to valid JavaScript or refuses with a clear error, and never half-erases into something that silently means the wrong thing. Once stripped, TypeScript is ordinary JavaScript, and nothing downstream can tell it apart from a file that arrived as .js.
For types that hold at run time instead of being erased, Cruft ships CruftScript, its own statically typed language with a sound type system whose guarantees are checked and enforced at run-time boundaries.
Parsing
Parsimony, Cruft's JavaScript parser, turns source text into an abstract syntax tree and enforces the grammar. That includes the spec's early errors, the cases like a duplicate let or a return outside a function that must be rejected before a single statement runs. It handles the whole modern language: modules and scripts, async and generators, classes with private fields, optional chaining, using declarations, and the rest.
Bytecode compilation
Distil, Cruft's bytecode compiler, lowers the syntax tree to bytecode, a flat instruction stream for a virtual machine. The decisions that are expensive to make repeatedly get settled here once: which variable lives in which slot, what each closure captures, the shape of the control flow, which instruction to select. The interpreter never re-derives them while the program runs.
The interpreter
Exegesis, Cruft's bytecode interpreter, is the execution core and the semantic source of truth for the whole runtime. It runs bytecode inside a Runtime that owns three things worth naming:
- Frames, one per active call, holding registers and lexical state, and, for generators and async functions, the suspended state that lets a call pause and resume.
- Completions, the spec's own model of control flow. Normal results, thrown errors, and
return/break/continueare each routed to the nearest construct that can handle them. - Jobs, the microtask machinery behind promises and
async/await, plus timers and the event loop. Cruft owns the event loop rather than delegating it to libuv.
One discipline runs through the whole interpreter: each bytecode instruction does the narrow thing it owns and hands the shared semantics (property access, calls, iteration, coercion) to common runtime helpers. The language rules live in one place instead of being copied across dozens of opcodes, which is much of why the engine holds the conformance it does.
Shapes, inline caches, and LeJIT
The mechanisms a V8 user knows by name do the same job here, correct first and fast second, with no change to observable behavior.
Hidden classes (shapes) describe an object's layout, and inline caches remember, per call site, which shape was seen last, so a repeated property access on same-shaped objects skips the general lookup. This is the reason "don't change an object's shape in a hot loop" is folklore.
LeJIT, Cruft's baseline JIT with a Cranelift backend, compiles hot functions to native code. Like V8 it is admission-gated and it deopts: a function is compiled only when the fast code provably behaves identically, and any violated assumption falls back mid-flight. Exegesis stays the authority. The JIT must match the interpreter, and a function the JIT declines simply runs in Exegesis, identical and only slower.
Garbage collection
Trash Panda, Cruft's garbage collector, is a tracing mark-and-sweep collector. Each worker runs its own heap and collects independently, so a pause in one does not stall the others, something a single shared V8 heap cannot promise. The trigger is adaptive, collecting once allocations reach roughly twice the live set, and it runs at safe points between jobs and between bytecode operations, which holds a long-running server or worker pool at a bounded resident size. One knob, CRUFT_GC_HEADROOM, trades memory for throughput.
The whole path, once
- LoadJavaScript enters directly; a .ts file is first erased to plain JavaScript.
- ParseParsimony builds an AST and surfaces early errors here.
- CompileThe AST is compiled to bytecode.
- InterpretExegesis runs the bytecode. Hot, provably-safe functions are compiled to native by LeJIT, which falls back to Exegesis the moment an assumption stops holding.