Expand description
§jit_lang
Lower IR to machine code in executable memory and run it now.
jit-lang takes a function in the ir-lang intermediate representation,
compiles it to native machine code for the machine it is running on, places that
code in executable memory, and hands back a callable handle. It is the end of the
pipeline a front-end follows: parse, type-check, lower to IR, and then — instead of
writing an object file — run the function immediately.
The surface is small. Jit is the engine; Jit::compile turns an
ir_lang::Function into a Compiled; and Compiled::entry reinterprets the
compiled code as a function pointer you can call. For a single compile, the free
function compile does the same in one step.
§How it works
A compile runs three stages:
- Validate. The function is checked with
Function::validate, so only well-formed SSA is ever lowered. - Translate and generate. The IR 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 — and Cranelift generates optimized machine code for the host.
- Place. The emitted bytes are copied into a guard-flanked memory region from
pager-lang, which is then flipped from writable to read-execute. The functions compiled here are leaf functions with no outgoing calls, so the code is self-contained and needs no runtime relocation.
§Type and calling convention
The IR’s four machine types map onto the host C ABI: int is a 64-bit integer,
float is an f64, bool is a byte holding 0 or 1, and a unit return means
the function yields no value. A unit-typed parameter has no machine
representation and is refused. The compiled function uses the host’s C calling
convention, which is what Rust’s extern "C" denotes, so the two agree when you
call it.
§Example
Compile fn sum_to(n: int) -> int { let mut acc = 0; while n > 0 { acc += n; n -= 1 } acc }
— a loop with a back-edge carrying two values — and run it:
use jit_lang::compile;
use ir_lang::{Builder, BinOp, Type};
let mut b = Builder::new("sum_to", &[Type::Int], Type::Int);
let n0 = b.block_params(b.entry())[0];
let header = b.create_block(&[Type::Int, Type::Int]); // (n, acc)
let body = b.create_block(&[]);
let exit = b.create_block(&[]);
let zero = b.iconst(0);
b.jump(header, &[n0, zero]);
b.switch_to(header);
let n = b.block_params(header)[0];
let acc = b.block_params(header)[1];
let z = b.iconst(0);
let more = b.bin(BinOp::Gt, n, z);
b.branch(more, body, &[], exit, &[]);
b.switch_to(body);
let acc2 = b.bin(BinOp::Add, acc, n);
let one = b.iconst(1);
let n2 = b.bin(BinOp::Sub, n, one);
b.jump(header, &[n2, acc2]);
b.switch_to(exit);
b.ret(Some(acc));
let f = compile(&b.finish()).expect("sum_to is well-formed");
// SAFETY: the signature is `fn(int) -> int` and `f` outlives every call below.
let sum_to: extern "C" fn(i64) -> i64 = unsafe { f.entry() };
assert_eq!(sum_to(5), 15); // 5 + 4 + 3 + 2 + 1
assert_eq!(sum_to(0), 0);§Safety
Generating code and jumping into it cannot be checked by the compiler: the type you
transmute the entry point to has to match what was compiled, and the Compiled
that owns the code has to outlive every call. Those obligations are concentrated in
the single unsafe method Compiled::entry, whose contract spells them out.
Everything up to that point — building the IR, compiling it, inspecting the result
— is safe.
§Platforms
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 —
nothing to do on x86-64, where the caches are unified, and an instruction-cache
synchronization on ARM64, where they are not. The crate links the standard library
and reaches the operating system for memory; it is not no_std.
§Stability
The public surface is frozen and stable as of 1.0.0: it follows Semantic
Versioning, with no breaking changes before 2.0. JitError is
#[non_exhaustive], so a new failure variant is an additive, non-breaking change.
The full surface and the SemVer promise are catalogued in
docs/API.md.
Structs§
- Compiled
- A compiled function: native machine code living in executable memory, ready to run.
- Jit
- A just-in-time compiler for the host machine.
Enums§
- JitError
- The reason a function could not be compiled and made executable.
Functions§
- compile
- Compiles a function with a fresh host engine, for the one-off case.