LeJIT

LeJIT is Cruft's just-in-time compiler: it turns hot JavaScript functions into native machine code so they stop paying the interpreter's per-instruction cost. This page explains its single baseline tier, the six levels that govern how far it specializes a piece of code, and the mechanisms (hidden classes, inline caches, on-stack replacement, deoptimization) that let it specialize without changing what a program computes.

LeJIT is Cruft's just-in-time compiler: the part of Cruft Core that turns hot JavaScript functions into native machine code so they stop paying Exegesis's per-instruction cost. This page explains how it is structured, what governs how far it specializes a given piece of code, and the mechanisms (hidden classes, inline caches, on-stack replacement, deoptimization) that let it specialize without changing what a program computes. It follows Cruft Core and the Exegesis.

One execution tier, several specialization levels

There are two different things "tier" can mean for a JIT, and Cruft sits at opposite ends of them.

As an execution ladder, Cruft has one compiler. V8 runs a function through a series of engines as it gets hotter: the Ignition interpreter, then the Sparkplug baseline compiler, then Maglev, then the TurboFan optimizing compiler. A function can be recompiled several times, climbing the ladder for more speed and dropping back down when an assumption fails. Cruft does not do this. It has Exegesis, which runs everything first, and LeJIT, a single baseline compiler that hot functions are handed to. There is no optimizing tier above LeJIT: a function is either interpreted or compiled once, and nothing recompiles it more aggressively later. That keeps the compiler small enough to read and audit, and it means a function that is already fast will not be silently recompiled into different behavior.

Inside that one compiler, how far LeJIT specializes a given region is governed by a ladder of conditions. This is where "single-tier" stops being the whole story. Compiling x + y into a raw machine integer add, or obj.x into a direct memory read, is only correct when specific facts about that code have been established first. LeJIT organizes those facts into six levels, labeled A through F. Each level licenses a more aggressive specialization, and LeJIT takes that step only when the level's condition holds; when it cannot establish the condition, it declines the specialization and, in the limit, leaves the region for Exegesis to run.

LevelWhat it establishesWhat that allows
A. Admissionevery operation in the region is one LeJIT knows how to compilecompiling the region at all
B. Value domaina value is provably in the type it is about to be treated as (a number, say)dropping the runtime type tag and using a raw machine i64/f64
C. Runtime boundarya call into a runtime helper or native function will not disturb the values the fast path is specializing oncalling across that boundary without re-checking afterward
D. Deopt / resumethere is a correct way to fall back to Exegesis if a guess turns out wrongspecializing a site that is genuinely dynamic, on a guess
E. Shapes / inline cachesthe recorded history of a property-access site (which object layouts it has seen)compiling the access as a direct slot read for those layouts
F. Compile policythat a function or loop is hot enough to be worth compilingstarting compilation, and choosing where to enter

Levels A through E are about whether a specialization is safe. Level F is a different axis: it is about when to compile, the hotness thresholds and entry points, and it carries no correctness meaning on its own.

The single discipline that ties them together: F must never run ahead of B, C, and D. Deciding a loop is hot (F) does not license dropping a value's type tag (B) or crossing a helper boundary (C) unless those facts are actually established. When the licensing fact is missing, LeJIT withholds the faster path rather than emitting it and hoping. A specialization that ran fast on an unproven assumption, and therefore sometimes computed the wrong answer, is the exact failure this structure exists to prevent.

Level A in practice: the verifier

Before generating any code, LeJIT runs a verifier over the function's bytecode. A function it cannot confirm is well-formed and built entirely from operations it knows how to compile is left in Exegesis untouched. This is why turning the compiler on cannot make a program wrong: the worst outcome for a function that fails admission is that it does not get faster, not that it runs incorrectly.

Level B in practice: integer promotion

Numbers in Exegesis are always 64-bit floats (f64). When LeJIT can prove a function's arithmetic is integer-shaped, loop counters, array indices, and the common cases, it rewrites those generic number operations into real 64-bit integer operations, so the math compiles to machine integers instead of routing through floating point. The rewrite happens only when the values are provably in the integer domain; that proof is the Level B condition, and without it the tag stays on and the generic path is used.

Level E in practice: hidden classes and inline caches

JavaScript objects behave like dictionaries, but real programs build them in repetitive ways, and LeJIT specializes property access on that regularity through shapes, Cruft's name for hidden classes (V8 calls them Maps).

  • Objects that gain the same properties in the same order share one shape, and the shape holds the map from each property name to a storage slot.
  • Adding a property moves an object to a child shape with one more slot, and the same addition always leads to the same child, so objects with the same history converge on the same shape.
  • Reading obj.x becomes: look at the object's shape, find x's slot, read that slot, with no per-object dictionary walk.

An inline cache sits at a property-access site and remembers the shape it saw last, together with the slot that shape put the property in. If the next object has the same shape, the access is a couple of instructions: check the shape, read the slot. If the shape differs, the cache records the miss and falls back to the general lookup; after enough different shapes at one site (Cruft's cutoff is eight, against V8's observed four to five) the site stops trying to specialize and uses the general path for good.

Inline caches sit at the boundary between Exegesis and LeJIT and serve both. Exegesis uses them on its own hot paths, and the record they build is what LeJIT reads when it compiles the surrounding function. A site that has only ever seen one shape is evidence the compiled code can specialize on that shape with a safe exit attached, because the record is a history, not a guarantee about the future, which is why the exit (Level D) has to exist.

The architecture: two code generators

Within that single compiler, LeJIT does not emit machine code directly for everything. It splits code generation between two producers.

  • Cranelift generates whole function bodies. Cranelift is a compiler backend written in Rust. It owns the parts of code generation that are the same for any language: choosing machine instructions, allocating registers, scheduling, and emitting the final bytes. LeJIT translates a function's bytecode into Cranelift's input and lets Cranelift do that work.
  • A dedicated emitter covers the spots Cranelift cannot express well. A few narrow cases need machine code shaped more specifically than a general backend produces: the inline-cache check on a property access, the tag check on a tagged value, and a fast path for very small functions where Cranelift's fixed setup cost would dominate. LeJIT emits those directly. Some of this path is on by default today (the tiny-function fast path defaults on, opt out with CRUFT_LEJIT_TB=0, and the shape-aware property fast path is likewise default-on), while the fully inlined self-patching cache stub is still being brought up.

Everything specific to JavaScript, which functions may be compiled and what each specialized path is allowed to assume, stays in Cruft's own Rust. Cranelift is used only for the language-neutral work of turning that into machine instructions.

Entering and leaving compiled code

Two transitions connect Exegesis and LeJIT, and they correspond to the Level F trigger and the Level D safety exit.

  • On-stack replacement (OSR) lets execution jump into compiled code in the middle of a running loop. A loop that only becomes hot part way through does not have to wait for the next call to benefit; Exegesis's state for that loop is transferred into the compiled version at a loop boundary. LeJIT has several specialized OSR entries for different loop shapes (floating point, typed arrays, packed arrays). OSR is a separate path from ordinary function-entry compilation.
  • Deoptimization is the reverse, and it is what makes speculative specialization safe. When a specialized path's assumption turns out wrong at run time, an object arrives with an unexpected shape, a value leaves the range the code assumed, the compiled frame is unwound back into Exegesis frames at the exact bytecode position it had reached, every live value is rebuilt, and execution continues in Exegesis as if the compiled code had never run. Because the fallback is always available and always correct, a specialized fast path does not need to defend itself with run-time type checks; the exit is the safety net.

Compiled code honors the same whole-runtime rules as Exegesis: compartment timeouts are enforced on compiled code (a bare loop under a timeout_ms is interrupted), and calls into runtime helpers follow the same garbage-collection rooting rules as everywhere else.

Inspecting what LeJIT does

Three environment variables expose LeJIT's behavior, useful for confirming whether a function was compiled and where time is going. Each produces the same program result with the compiler engaged or disabled.

VariableEffect
CRUFT_JIT_DISABLE=1Run everything in Exegesis, no function-entry compilation. Does not disable OSR (OSR is a separate path).
CRUFT_OSR_THRESHOLD=<large N>Raise the loop-hotness threshold so OSR effectively never fires; use this to isolate OSR from the rest of the compiler.
CRUFT_OSR_TRACE=1Print a line each time an OSR site compiles, fires, and transfers loop state, naming the site and the values moved.

A run with CRUFT_OSR_TRACE=1 shows entries like try_osr_compile site=90 ... OK and try_osr_invoke site=90 FIRED, the compile of a loop site and the jump into it. Because disabling the compiler leaves results unchanged, these are safe to turn on when diagnosing performance without risking a behavior difference.

Compiled code is never keyed by address alone

A JIT keeps caches mapping a function to its compiled result, and the tempting key is the memory address of the function or its bytecode. That is only safe if the cache also keeps the function alive, or re-checks on a hit that the address still belongs to the same function. A cache that does neither has a memory-safety hole: a function can be collected, its address reused by a different function, and a stale entry hand back the wrong function's compiled code, a use-after-free that surfaces as silent miscompilation rather than a clean crash.

LeJIT holds the function's handle in the cache and compares identity on every hit, so a reused address cannot alias a stale entry. No cache that is on by default keys compiled code by address alone.

Compiled code matches Exegesis

The question that matters for any JIT is whether compiled code computes the same answers as Exegesis. Cruft treats a disagreement as a first-order defect, not a performance detail, and checks it automatically. A differential test runs the classic miscompilation idioms, an array copy over mixed types, a side-effecting valueOf in a hot loop, integer arithmetic across the safe-integer boundary, a shape change mid-loop, and a mid-loop deoptimization, with the compiler on and off, and requires identical output.

That differential runs as a default integration gate rather than by hand. The side-effecting valueOf case failed before the fix that made the two paths agree.

Performance numbers are not quoted here. Cruft's standing rule for performance work is that a speed claim ships only with direct evidence (a trace, a counter, a deoptimization log, or an inspection of the generated code) and confirmation that the conformance and differential suites still pass. Concrete throughput therefore lives in the benchmark result records; this page describes how the compiler works.