jit-lang 1.0.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 = "1"
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. 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.