Overview
A compiler's middle end produces a function in SSA form. The usual back end writes an object file; a JIT instead generates machine code in memory and jumps into it. jit-lang is that last step for the family: it takes an ir_lang::Function, compiles it for the machine it is running on, and returns a handle you can call.
The surface is three types and a function. Jit is the engine; Jit::compile turns a function into a Compiled; Compiled::entry reinterprets the compiled code as a function pointer you can call. The free function compile does all of that in one step for the one-off case. Failures are reported through JitError.
Under the hood, a validated function is translated to Cranelift IR — an almost one-to-one mapping, since both are SSA control-flow graphs whose values cross blocks as block parameters rather than through phi nodes — Cranelift generates optimized native code, and the bytes are copied into a guard-flanked, read-execute region from pager-lang.
Installation
[]
= "1"
= "1"
Or from the terminal:
You build the IR with ir-lang's Builder and hand the result to jit-lang, so both crates appear in your dependencies.
Quick Start
use compile;
use ;
// Build `fn add(a: int, b: int) -> int { a + b }` with the IR builder.
let mut b = new;
let a = b.block_params;
let c = b.block_params;
let sum = b.bin;
b.ret;
// Compile it to native machine code.
let f = compile.expect;
assert_eq!;
// Reinterpret the entry point as a function pointer and call it.
// SAFETY: the signature is `fn(int, int) -> int`, and `f` outlives the call.
let add: extern "C" fn = unsafe ;
assert_eq!;
How it works
A compile runs three stages:
- Validate. The function is checked with
Function::validate, so only well-formed SSA is ever lowered. A malformed function is rejected withJitError::InvalidIr, not miscompiled. - Translate and generate. The IR is translated to Cranelift IR and Cranelift generates optimized machine code for the host, with its native instruction-set features enabled.
- Place. The emitted bytes are copied into a guard-flanked memory region, which is then flipped from writable to read-execute — the write-xor-execute discipline. The functions compiled here are leaf functions with no outgoing calls, so the code is self-contained and needs no runtime relocation.
Types and calling convention
The IR's four machine types map onto the host C ABI:
| IR type | ABI | In Rust |
|---|---|---|
int |
64-bit integer | i64 |
float |
IEEE-754 double | f64 |
bool |
byte holding 0 or 1 |
bool / u8 |
unit (return) |
no return value | () |
A unit-typed parameter has no machine representation and is refused with JitError::Unsupported. The compiled function uses the host's C calling convention, which is exactly what Rust's extern "C" denotes, so the two agree when you call it.
A branch and a loop
use compile;
use ;
// fn sum_to(n: int) -> int { let mut acc = 0; while n > 0 { acc += n; n -= 1 } acc }
let mut b = new;
let n0 = b.block_params;
let header = b.create_block; // (n, acc)
let body = b.create_block;
let exit = b.create_block;
let zero = b.iconst;
b.jump;
b.switch_to;
let n = b.block_params;
let acc = b.block_params;
let z = b.iconst;
let more = b.bin;
b.branch;
b.switch_to;
let acc2 = b.bin;
let one = b.iconst;
let n2 = b.bin;
b.jump;
b.switch_to;
b.ret;
let f = compile.expect;
// SAFETY: the signature is `fn(int) -> int` and `f` outlives every call.
let sum_to: extern "C" fn = unsafe ;
assert_eq!;
API Overview
For a complete reference with examples, see docs/API.md.
Jit— the engine; holds a code generator for the host and compiles many functions.Jit::new/Jit::compile— build the engine, then compile a function to runnable code.compile— the one-call shortcut: build an engine and compile a single function.Compiled— the result: native code in executable memory, withname,params,ret,code_len,as_ptr, and theunsafeentry.JitError— the reason a compile failed: invalid IR, an unsupported feature or host, a code-generation failure, or executable memory being unavailable.
Safety
Generating code and jumping into it cannot be checked by the compiler. The two obligations — the function-pointer type must match what was compiled, and the Compiled that owns the code must outlive every call — are concentrated in the single unsafe method Compiled::entry. Building the IR, compiling it, and inspecting the result are all safe.
Performance
Performance is a hard constraint, not an afterthought (see REPS.md). The translation to Cranelift IR is a single linear pass over the function; the heavy lifting — instruction selection, register allocation, optimization — is Cranelift's, run at its speed optimization level with the host's native CPU features enabled. A compiled function is plain native code: calling it is an indirect call, with no interpreter loop or dispatch in between.
Latest local Criterion means (cargo bench --bench bench, Linux x86_64 / WSL2, Rust stable, release build). compile covers validation, translation, code generation, and placing the code in executable memory:
| Work | Shape | Time |
|---|---|---|
compile |
two-way branch (max) |
~27 µs |
compile |
loop with a back-edge (sum_to) |
~34 µs |
compile |
straight-line, 16 add steps |
~178 µs |
compile |
straight-line, 256 add steps |
~4.1 ms |
| call | invoke a compiled function | ~1.4 ns |
A call into compiled code is a plain indirect call — about a nanosecond, the cost of the jump and return. Compile time is Cranelift's, and grows with function size; the large straight-line case is register allocation over a single big block. Numbers vary by CPU and environment; run the benches on your hardware for trends.
Configuration
jit-lang has no feature flags: the JIT is the crate, and it links the standard library and reaches the operating system for executable memory, so it is not no_std.
Its dependencies are the engine it is built on:
| Dependency | Role |
|---|---|
ir-lang |
the IR a function is built in — the input to every compile |
cranelift-codegen / cranelift-frontend / cranelift-native |
native code generation for the host |
pager-lang |
executable, page-aligned memory with write-xor-execute control |
Examples
Three runnable examples live in examples/:
# Build `add`, compile it, and call it.
# Compile and run a branch (abs) and a loop (sum_to).
# Compile and run floating-point and boolean-returning functions.
Testing
# Unit, integration (workflow, properties, error paths), and doc tests
# Property tests only
# Lints and formatting
# Benchmarks (Criterion)
The integration suite in tests/workflow.rs compiles representative functions — double, abs, max, a countdown loop, float arithmetic, boolean logic, integer division, and a unit function — to native code and runs them, checking the results the CPU produced. The property tests in tests/properties.rs generate random functions, run the compiled code, and compare each result to an independent evaluation in Rust.
Cross-Platform Support
Code generation targets the host through Cranelift, and executable memory is managed by pager-lang, so the supported targets are their intersection: Linux, macOS, and Windows on x86-64 and ARM64. Freshly written code is made coherent before it runs — a no-op on x86-64, where the instruction and data caches are unified, and an instruction-cache synchronization on ARM64, where they are not. The CI matrix builds and tests on Linux, macOS, and Windows (macOS on ARM64, so the cache path is exercised) on stable and the 1.94 MSRV.
Contributing
Engineering standards for this crate are the Rust Efficiency & Performance Standards; the current scope and plan are in dev/ROADMAP.md. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.