jit-lang 0.2.0

Lower IR to machine code in executable memory and run it now.
Documentation

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

[dependencies]
jit-lang = "0.2"
ir-lang = "1"

Or from the terminal:

cargo add jit-lang ir-lang

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 jit_lang::compile;
use ir_lang::{Builder, BinOp, Type};

// Build `fn add(a: int, b: int) -> int { a + b }` with the IR builder.
let mut b = Builder::new("add", &[Type::Int, Type::Int], Type::Int);
let a = b.block_params(b.entry())[0];
let c = b.block_params(b.entry())[1];
let sum = b.bin(BinOp::Add, a, c);
b.ret(Some(sum));

// Compile it to native machine code.
let f = compile(&b.finish()).expect("add is well-formed");
assert_eq!(f.name(), "add");

// 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(i64, i64) -> i64 = unsafe { f.entry() };
assert_eq!(add(19, 23), 42);

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. A malformed function is rejected with JitError::InvalidIr, not miscompiled.
  2. 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.
  3. 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 jit_lang::compile;
use ir_lang::{Builder, BinOp, Type};

// fn sum_to(n: int) -> int { let mut acc = 0; while n > 0 { acc += n; n -= 1 } acc }
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.
let sum_to: extern "C" fn(i64) -> i64 = unsafe { f.entry() };
assert_eq!(sum_to(100), 5050);

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, with name, params, ret, code_len, as_ptr, and the unsafe entry.
  • 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.
cargo run --example jit_and_call

# Compile and run a branch (abs) and a loop (sum_to).
cargo run --example control_flow

# Compile and run floating-point and boolean-returning functions.
cargo run --example floats

Testing

# Unit, integration (workflow, properties, error paths), and doc tests
cargo test

# Property tests only
cargo test --test properties

# Lints and formatting
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings

# Benchmarks (Criterion)
cargo bench --bench bench

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.