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
[]
= "0.2"
= "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. The CI matrix builds and tests on Linux, macOS, and Windows 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.