Skip to main content

Crate jit_lang

Crate jit_lang 

Source
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:

  1. Validate. The function is checked with Function::validate, so only well-formed SSA is ever lowered.
  2. 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.
  3. 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.