Cruft Core

Cruft Core is the JavaScript engine inside Cruft, written in Rust: the parser, bytecode compiler, interpreter, baseline JIT, object model, and collector. This hub maps the pipeline and documents the contracts every stage shares, from how values are represented to how a realm's intrinsics are installed.

Cruft Core is the JavaScript engine inside Cruft: the part that takes your source text and actually runs it. It is an independent implementation in Rust, not a fork of V8 and not a wrapper around anyone else's engine, so every part below, the parser, the bytecode compiler, the interpreter, the baseline JIT, the object model, and the collector, is Cruft's own code.

This page is the engine hub. It maps the pipeline and links each stage's deep dive, then documents the contracts the whole engine stands on: how values are represented, where the language's semantics live, how lifetimes are kept safe, and how a realm's environment is installed.

The engine at a glance

Source becomes results through a fixed pipeline. Each stage has its own page; this is the overview, with the detail on those pages.

  1. Source to AST. A lexer and recursive-descent parser reads the text, with Script and Module as distinct parse goals and TypeScript erased to whitespace-padded JavaScript before parsing. See Parsimony.
  2. AST to bytecode. Distil, a single-pass compiler, settles scope, temporal-dead-zone slots, closure upvalues, and control flow, and emits a stack-based opcode set (not a register machine). See Distil.
  3. Bytecode to results. Exegesis, a loop { match op } dispatch loop, runs everything first, with a garbage-collection safepoint and a fuel tick per instruction, plus fused fast-paths for recurring sequences and a per-operation execution index for time-travel inspection. See Exegesis.
  4. Hot code to native. When a function is called enough, LeJIT compiles its bytecode through Cranelift: a verifier gate, integer promotion, inline caches, on-stack replacement, and deoptimization back to Exegesis. See LeJIT.
  5. Memory underneath. A per-realm mark-and-sweep collector reclaims dead objects at Exegesis's safepoints. See Trash Panda.

The rest of this page is what those stages share.

Values and the object model

A value in the engine is a plain tagged Rust enum, one arm per kind, with primitives carried immediately and objects carried as handles:

Value arms
UndefinedNullBooleanNumberStringBigIntSymbolObject

Two facts follow, and both differ from an engine that packs values into machine words (V8's Smis, tagged HeapObject pointers, and NaN-boxing):

  • Numbers are always f64 in Exegesis. There is no separate Int32 value type at this tier; integer specialization is a JIT and bytecode concern (the typed 64-bit opcodes and the promotion pass), not a value-domain split Exegesis has to reconcile.
  • Objects are handles. An object value carries a small copyable id into heap storage the runtime owns, rather than embedding object data in the stack value. Object identity is handle equality.

The handle indirection is essential. Frames, jobs, module records, promise reactions, and worker structures all hold values that outlive any single stack; because they hold handles, object identity stays stable while the collector manages the underlying storage. JavaScript's pervasive identity semantics, a === b for objects, Map keys, prototype chains, reduce to handle equality over a heap the collector is free to administer.

Internal kinds

An object is ordinary properties plus an internal kind that marks what the object is beneath its properties: function, array, typed array, map, set, date, regexp, error, promise, module namespace, iterator, or a host-backed object. The kind is what the spec calls internal slots, made concrete: it is how Array.isArray answers, how a revoked proxy differs from a plain object, and how the collector knows which extra references a special object carries. That last point is a design obligation: every kind owes the collector a complete enumeration of its references, in every trace variant it participates in. A kind whose reference set is under-reported in even one trace path lets the collector reclaim a value that is still reachable, so the enumeration is part of each kind's contract.

Shapes (hidden classes)

Objects that gain the same properties in the same order share one Shape : a map from property name to storage slot. Adding a property walks a cached transition to a child Shape, and the same addition always returns the same child, so structurally identical objects converge. A Shape is what an inline cache caches (the (shape, slot) pair) and what LeJIT compiles a property access against. See LeJIT for the inline-cache and proof-object mechanics.

Abstract operations: semantics live in one place

The single most important architectural rule in the engine: neither bytecode instructions nor intrinsic functions implement language semantics. Both call shared abstract operations, the engine's direct rendering of the spec's own abstract operations (Get, Set, DefineOwnProperty, ToPrimitive, GetMethod, and the rest).

The abstract-operation layer owns descriptor conversion, receiver handling (the this seen by getters along a prototype chain), getter/setter dispatch, proxy trap lookup, private-field checks, and species/constructor lookups. So when the Exegesis executes obj.x and when JSON.stringify reads a property internally, both inherit exactly the same behavior: the same proxy traps fire, the same abrupt completions propagate.

The payoff is conformance at scale. The ECMAScript spec has thousands of observable corner cases, nearly all of which are compositions of a small set of abstract operations. Implement each operation once, correctly, and the compositions come out right everywhere; duplicate them per call site and every duplication is a divergence waiting for a test262 row. The engine's 99.9% zero-skip pass rate is, mechanically, this rule applied for years.

A layering note: pure-primitive helpers live in the abstract-ops layer with no access to the runtime, while operations that can call back into JavaScript (Object-to-primitive coercion, getters) are runtime-dispatching by construction. The naming convention marks the difference (see Names as coordinates), so a helper's signature tells you whether it can re-enter user code.

Numeric domains

Numbers (IEEE 754 doubles) and BigInts are strictly separate domains with runtime-owned arithmetic, comparison, conversion, and typeof behavior. The guard against accidental mixing is structural: there is no implicit cross-domain arithmetic path, because the spec says 1n + 1 is a TypeError, and the engine encodes that as the absence of a code path rather than a check that could be forgotten.

Rooting: the lifetime contract

The engine's one pervasive unsafety-shaped obligation: any value that survives a call boundary, queue turn, promise reaction, module await, or worker transfer must be rooted, registered as reachable, or represented through a safe transfer structure (SendIR, Tier-2 handles). The collector runs at safepoints between operations and jobs; an unrooted handle held across one is a use-after-free.

This contract is why the subsystems compose: the job queue roots pending reactions, module records root suspended top-level-await frames, worker transfer lowers to SendIR rather than holding foreign handles, and CRUFT_GC_STRESS exists to make any missed root deterministic instead of rare. When reading engine code, "who roots this across that await?" is the first review question.

Intrinsics: installing the world

A realm's observable environment, Object, Array, Promise, typed arrays, Temporal, Intl, iterator helpers, Web globals like TextEncoder and EventTarget, is installed into it at construction by intrinsic install functions. The division of labor mirrors the abstract-ops rule: install functions describe what the realm exposes (constructors, prototypes, descriptors, symbols, wrappers); the shared value and abstract-op helpers own what those functions do.

Every intrinsic has a declared compatibility posture: complete, partial, stubbed, Node-compatible, spec-pure, or intentionally divergent, and the documentation discipline is that pages must say which applies. Stubs are feature-detection-visible on purpose and documented as such.

Because compartments construct fresh realms, the install machinery is also the isolation machinery: a compartment realm is "whatever install functions ran for it, and nothing else." A fresh new Compartment() realm carries the standard intrinsics: Math, Date, Map, Set, Promise, and the value properties undefined/NaN/Infinity are all present in a bare compartment.

Names as coordinates

The source encodes its own strata in identifier conventions, so a name tells you where you are:

ConventionMeaning
nameuser-visible surface
__nameengine-internal sentinel (non-enumerable)
@@namewell-known Symbol property
_via suffixruntime-dispatching helper, can re-enter JavaScript
bare helper in abstract_opspure primitive, cannot re-enter
set_own_frozeninstalls {w:f, e:f, c:f} (namespace constants)
set_own_internalinstalls {w:t, e:f, c:t} (proto methods, sentinels)
set_owninstalls {w:t, e:t, c:t} (user-default)

The conventions self-check: an __name installed via plain set_own is a bug by inspection, before any test runs. For an engine with no upstream to diff against, making the invariants legible in the names is part of how correctness is maintained.

Coming from V8

If V8 is your reference, this table maps its parts to Cruft Core's. It is an aside, not the design: the sections above are the actual account.

In V8In Cruft CoreStatus
Ignition, the bytecode interpretera single-pass bytecode compiler feeding a stack-based dispatch looppresent, stack-based rather than register-based
Sparkplug and TurboFan, the JITsLeJIT, one baseline JIT over Craneliftpresent; a baseline tier, closer to Sparkplug than TurboFan
Maps (hidden classes)Shapes, transition-deduplicatedpresent, same idea
Torque and CSA, the builtin DSLsno DSL; builtins are plain Rust under the abstract-operations ruledeliberately absent
Slack tracking (in-object slot pre-sizing)nothingno analog
The WebAssembly compilation pipelineZoom, an MVP interpreterpresent, interpreter only, no WASM JIT
Orinoco and Oilpan, the collectorsTrash Panda, per-realm mark-and-sweeppresent, see Trash Panda
Smi / tagged-pointer / NaN-boxed valuesa plain tagged Rust enum; numbers are always f64present, simpler on purpose

What Cruft Core does not have yet

  • No builtin-authoring DSL. There is no Torque or CSA analog. Builtins are plain Rust installed by explicit install_* functions, with semantics in the shared abstract-operations layer. A few small macros remove accessor boilerplate, but there is no builtin language.
  • No slack tracking. An object's slot vector grows one slot per Shape transition, with no pre-allocation to trim. If you came looking for the slack-tracking knob, there is not one, by design.
  • LeJIT is one baseline tier, not a speculative optimizing compiler. There is no TurboFan-class tier above it. The LeJIT page is explicit about what that buys and what it costs.
  • The WebAssembly engine is an interpreter. Zoom is a self-contained MVP (1.0) interpreter in pure safe Rust; it parses and interprets but does not compile WASM to native code. The Zoom page has the parity matrix.
  • Biblia, the specification IR tier, is early. It expresses ECMA-262 builtins as an IR whose nodes are the specification's abstract operations, then lowers it to Rust, but today the sections are written into the IR directly rather than parsed from the specification, and the lowering emits Rust source as strings. It is a real and growing tier, not a finished pipeline. See Biblia.