Exegesis

Exegesis is Cruft's bytecode interpreter and execution core: it runs compiled bytecode, owns the call frames and the completion machinery that models control flow, drives the job queues behind promises and async/await, and defines the boundary LeJIT accelerates and deoptimizes back to. This page walks each of those.

Exegesis is the runtime's interpreter, and its execution core: it runs compiled bytecode, owns the frame and completion machinery, drives the job queues behind promises and async execution, and defines the boundary LeJIT accelerates. It lives in a single file, interp.rs, whose roughly 144,000 lines of code make it the largest source file in the engine. This page assumes Cruft Core and Distil.

The Runtime and its frames

Execution runs through a Runtime holding a chain of frames: one per active function call. A frame carries registers, lexical state, the realm/module context, and, for suspendable functions, everything needed to pause and later resume.

Frames are full heap objects, and holding execution state that way is a deliberate architectural decision with three payoffs:

  • Generators and async functions suspend by keeping their frame. A yield or await doesn't unwind and reconstruct anything; the frame, with its registers, its position, its scope chain, simply stops being driven until a resume job picks it up.
  • Top-level await does the same at module granularity: a module body is a frame that can suspend mid-evaluation while the graph continues elsewhere.
  • Safepoints and rooting get a uniform answer: a suspended frame is a rooted object, so a promise held across a year-long await has its state known to the GC the same way any live object is.

Completions: how control flows

The spec models every evaluation step as producing a completion, normal, throw, return, break, or continue, and Exegesis implements control flow in exactly those terms. A thrown error or a control transfer is routed back through the frame chain to the nearest construct that can consume it: a catch clause, a finally block (which must run even while a break or return is in flight, and can override it), a loop head for continue, the caller for return.

Getting try/finally interactions right, a finally that runs during an in-flight break, a return inside a finally overriding the original completion, is a classic engine-correctness swamp, and modeling completions explicitly rather than piggybacking on native unwinding is what keeps it tractable and spec-checkable.

Dispatch discipline

Each bytecode instruction performs the narrow operation it owns and delegates shared semantics to the abstract-operation layer: property access, calls and construction, iteration protocol, private fields, BigInt arithmetic, typed-array indexed access, and module boundaries all flow through shared layers rather than being re-derived in opcode arms. (Why this rule exists and what it buys is covered in Cruft Core; Exegesis is its largest client.)

Two execution-loop details have outsized consequences:

  • Loop back-edges are checkpoints. Fuel accounting and the interrupt flag (compartment timeout_ms, cancellation) are checked on loop back-edges, so a tight loop with no calls and no try frame is still stoppable.
  • Safepoints are between operations and between jobs. Long-running execution, job turns, module-body resumes, and allocation-heavy paths are collection boundaries; the rooting contract from the engine core page is what makes crossing them safe.

The job engine: promises, ticks, and async resume

Exegesis's other half is the job queue: the machinery that decides what runs when the current stack finishes. This section sketches how it feeds the interpreter; Pumped Up Ticks, the event loop, is the exhaustive account of the three-phase driver, the drain order, and how a compartment bounds it.

Promise records and deferred reactions

A promise is an object whose internal kind stores its status, result, and reaction lists. The essential spec behavior: resolving never runs handlers synchronously. Resolution records the result and enqueues jobs; handlers run in queue order on a later turn. Promise.all/race/allSettled and friends are built over promise capabilities (promise + its resolve/reject pair), so combinator plumbing schedules reactions without losing promise identity.

Two lanes: microtasks and next ticks

The queue has separate lanes for standard microtasks and Node-shaped process.nextTick work, preserving the public ordering Node programs rely on (next ticks drain ahead of ordinary microtasks where the Node lane is used). This is a compatibility-critical detail: real packages observably depend on tick-vs-microtask interleaving.

Async suspend and resume

An await suspends the frame, roots the values that must survive, and registers a reaction; the resume job re-enters the frame where it left off. A rejection is thrown back at the suspension point, so a local try/catch around the await observes it before any coarser mechanism (module failure, unhandled-rejection reporting) does, the spec's semantics, achieved naturally because the frame is still intact to throw into.

Dynamic import settles through the same lane: import() waiters register against the module URL and settle when its evaluation completes.

The host at the seam

Host I/O (timers firing, sockets becoming readable) enters through polling hooks that run between job turns. The language state machine never blocks on the host and the host never runs JavaScript mid-turn; readiness is converted into enqueued jobs. Timers and EventTarget are host surfaces that feed this queue; Exegesis's contract is only the drain order.

The rooting rule, applied

The job engine is where the engine rooting contract does most of its work: every value a job can touch after the current stack unwinds, handlers, awaited values, namespaces, frame snapshots, rejection reasons, is enqueued through rooted helpers, never raw closures. Several of the engine's hardest-won historical bugs (and the CRUFT_GC_STRESS lane that hunts them) live at exactly this boundary.

The LeJIT boundary

Exegesis maintains inline-cache state on hot paths and hands sufficiently hot, provably-safe functions to LeJIT. The division of responsibility is strict:

  • LeJIT owns code generation and machine-code policy.
  • Exegesis owns fallback semantics: when a JIT assumption fails mid-flight, deoptimization reconstructs Exegesis frames and execution continues here, observably identically. Exegesis is the semantic ground truth; compiled code is an optimization that must be invisible.

The full JIT story, shapes, ICs, admission gating, deopt, is the next deep dive.