Documentation
XCX JIT Engine: Core Architecture and Pipeline
The XCX JIT Compiler uses Cranelift as its backend to dynamically compile VM functions (bytecode chunks) into native x86_64 machine code. Compilation is per-function ("method JIT"): a chunk is compiled once its call count reaches the warmup threshold (VM::jit_threshold, default 50), triggered from Executor::check_jit_warmup and the FFI entry warmup path in vm/core/jit_helpers.rs. Fibers execute through the interpreter; a former fiber-segment JIT and a trace (hot-loop) compiler existed in earlier revisions but were unreachable and have been removed.
Cranelift Module Integration
The JIT engine is represented by the JIT struct (src/jit/jit.rs). It acts as a wrapper around Cranelift's JITModule and compiler context to orchestrate code generation, thread-safe function definition, and FFI linking.
Method Compilation Pipeline
Compilation of one function is initiated by JIT::compile_method (implemented in src/jit/compiler_method.rs):
1. Clear Context: Instantiates/clears JIT function generation state (module.clear_context).
2. Signature Build & Declaration: Builds the target function's calling convention and declares the function.
3. Symbol Importation: Maps FFI symbols into the Cranelift internal function representation (SymbolRegistry::new + import_in_func).
4. Register Pre-Analysis: Sweeps the chunk's bytecode to build pointer tracking maps, type tag inference grids, and liveness regions (src/jit/analysis.rs). Parameter registers are seeded from the chunk's declared signature types (Chunk::param_types); remaining registers keep flat inference, and the seed never invents operand facts - an arithmetic op's result type is derived from its operands only.
5. Cranelift IR Builder Execution: Loops over bytecode, generating Cranelift IR for each operation.
6. Code Generation: Emits compiled machine code into executable memory using Cranelift's JIT infrastructure.
Two 4.4 properties of the pipeline:
- Determinism: analyses that collect register/global sets use ordered containers (
BTreeSet), and spill/reload emission sorts by index instead of iterating a hash map, so the same binary produces the same machine code on every run. - Failure reporting: code finalization failures report the chunk name and the real Cranelift/module error at all three compile sites (warmup, FFI-entry, and direct) instead of silently disabling the JIT.
Function ABI & Calling Conventions
Modern compilers optimize execution speed by defining custom ABIs for local paths while exposing standard APIs for FFI interaction. The JIT uses two distinct compilation strategies to reduce stack parameter unpacking overhead.
Parameters Map
A JIT-compiled function conforms to the following ABI:
out_ptr: Pointer to a 16-byte stack slot where the 64-bit bits and 64-bit type tag representing the returnedValueare written.locals_ptr: Pointer to the VM register/locals array.globals_ptr: Pointer to the VM globals array.consts_ptr: Pointer to the constants list.vm_ptr: Pointer to the activeVMcontext.executor_ptr: Pointer to the currentExecutorcontroller.shutdown_ptr: Pointer to the VM shutdown state flag.
JIT-to-JIT Direct Call & Local Call Optimizations
To minimize interpreter re-entry and FFI overhead on function calls, the JIT implements two optimization routes:
#### 1. Local Recursive Calls (Wrapper vs. Inner Model)
For internal self-recursive or local segment calls, the compiler generates two nested Cranelift functions:
- Inner Function (
Linkage::Local): Receives parameters directly via register-based CPU ABIs (pairs of(bits_reg, tag_reg)) instead of reading them from thelocals_ptrstack frame. - Outer Wrapper (
Linkage::Export): Serves as the FFI bridge for the interpreter. It unpacks arguments fromlocals_ptr, executes the local inner function, records the resultingValueback intoout_ptr, and returns the frame status.
Recursion depth guard: before performing a local recursive call, the compiler emits a native comparison of the call-depth counter (stored on the Executor) against a fixed limit of 800, using IntCC::SignedGreaterThanOrEqual. On overflow, execution branches to a dedicated block that invokes the xcx_jit_check_recursion FFI helper to report the failure and returns a halt status - the failure path itself is not inlined at each call site, keeping the guard's generated code compact (registers are spilled ahead of the guard on some paths, not inside the overflow block). On the non-overflow path the depth counter is incremented before the recursive call and restored to its prior value afterwards. Because HIR inlining flattens self-calls, the depth counter measures physical (post-inlining) frames, so equivalent programs may recurse deeper than in 4.3.
#### 2. Cross-Function Direct Call Dispatch
When calling another function (func_idx != ctx.self_func_idx), the compiler audits the callee's JIT status:
- Fast-Path (Direct Call): If the target function has already been JIT-compiled (callee's
jit_ptris non-null), the compiler emits a Craneliftcall_indirectinstruction directly to that memory location. The compiler incrementscall_depthcheck-bounds, moves the VM register stack pointer (stack_ptr) forward bycallee_chunk.max_localsto frame local scopes, and restores them on return. Global variables are reloaded from memory after the call, narrowed to the callee's transitive global-slot write set - computed as a fixpoint over the call graph (recursion-safe). Callees that write no globals add zero reload cost; the oldercallee_uses_heapgate is superseded. - Slow-Path Fallback: If the callee is uncompiled (
jit_ptris null), processing falls back to the interpreter interface via thexcx_jit_call_recursiveFFI helper.
Symbols Registry
To let JIT-compiled code interact with complex runtime subsystems, a comprehensive static mapping is maintained inside src/jit/symbols/mod.rs. The SymbolRegistry declares and registers external Rust symbols prefixed with xcx_jit_* with Cranelift.
Through this registration, JIT-generated Cranelift IR calls external routines for:
- Reference counting management (
xcx_jit_inc_ref,xcx_jit_dec_ref). - Memory allocation and initialization of
Array,Map,Set, andTableinstances. - Core FFI library lookups (string manipulation, JSON serialization, date parsing).
- I/O and network layers (
net_call,net_request,terminal_move,terminal_cursor). - Deep recursion checks (
xcx_jit_check_recursion,xcx_jit_dec_recursion). - Bounds-checked collection fallbacks (
xcx_jit_array_get_booland equivalents), invoked when a fast-path bounds check onArray/BoolArrayelement access fails - seecompiler/jit/jit_codegen.md.
Macro-driven Declarations (src/jit/symbol_macros.rs - removed 2026-09-07)
A declare_jit_symbols! macro existed that could generate (1) the fields of the SymbolRegistry struct, (2) the fields of the ImportedSymbols struct, (3) the linkage declarations inside SymbolRegistry::new, and (4) the per-function imports in SymbolRegistry::import_in_func - but it was never wired into the module tree: src/jit/symbols/mod.rs hand-writes the registry. The dead file was deleted on 2026-09-07 (verified: cargo did not recompile on its removal).