Overview
A compiler's middle end produces a function in SSA form; the backend turns that control-flow graph into a linear stream of instructions a machine can run. codegen-lang draws the line between the two.
Backend is the trait a target implements. Bytecode is the target shipped here: a small, register-based bytecode that is enough to inspect, serialize, and run generated code without pulling in a native code generator. A backend built on LLVM or Cranelift slots in behind the same trait, producing its own output type.
The input is an ir_lang::Function. Each IR value is given its own virtual register, each basic block becomes a label in a flat op stream, and a block's parameters are filled by move ops emitted on the edges that target it — the bytecode's stand-in for an SSA phi. The function is validated before lowering, so a backend only ever sees well-formed SSA.
Installation
[]
= "0.2"
= "1"
Or from the terminal:
Quick Start
use ;
use ;
// Build `fn double(x: int) -> int { x + x }` with the IR builder.
let mut b = new;
let x = b.block_params;
let sum = b.bin;
b.ret;
// Lower it to bytecode.
let program = compile.expect;
assert_eq!;
assert!;
assert!;
// The Display impl is a readable disassembly:
// double(r0) regs=2
// L0:
// r1 = add r0, r0
// ret r1
println!;
Lowering model
The bytecode is register-based rather than stack-based. The pieces map onto the IR directly:
| IR construct | Lowers to |
|---|---|
| A value (instruction result or block parameter) | A virtual Register, numbered by the value's index |
An instruction (iconst, bin, un, …) |
One Op writing its result register |
| A basic block | A Label at the op where the block begins |
| A block argument on an edge | A move into the target block's parameter register |
| A two-way branch | A jump_unless plus two exclusive arms |
Because the source and destination registers of an edge are disjoint in SSA, the argument copies never interfere — no parallel-move scheduling is required. The result is a flat &[Op] that an interpreter or a further pass can walk with no indirection.
A branch and a loop
use ;
use ;
// fn abs(x: int) -> int { if x < 0 { -x } else { x } }
let mut b = new;
let x = b.block_params;
let join = b.create_block;
let neg_blk = b.create_block;
let pos_blk = b.create_block;
let zero = b.iconst;
let is_neg = b.bin;
b.branch;
b.switch_to;
let negated = b.un;
b.jump;
b.switch_to;
b.jump;
b.switch_to;
let result = b.block_params;
b.ret;
let program = compile.expect;
// The branch became a conditional skip; the winner reaches the join via a move.
assert!;
assert!;
API Overview
For a complete reference with examples, see docs/API.md.
Backend— the trait a code generator implements; lowers aFunctionto a targetOutput.Bytecode— the reference backend, emitting a flatProgram.compile— the shortcut for lowering with the bytecode backend.Program— the lowered function: name, parameter registers, register count, op stream, and a disassemblyDisplay.Op— one bytecode instruction; the closed set the op stream is made of.Reg/Label— a register and a jump target.Const— a constant operand (Int/Float/Bool).CodegenError— the reason a function could not be lowered.BinOp/UnOp— re-exported fromir-langso the operations carried byOp::BinandOp::Uncan be matched without namingir-langdirectly.
Error handling
compile validates its input with Function::validate before lowering. If the function is not well-formed, it returns CodegenError::InvalidIr carrying the precise reason; a backend never emits a program that is wrong in a way the IR already forbids.
use ;
use ;
// A function whose entry block never receives a terminator is not well-formed.
let func = new.finish;
match compile
Performance
Performance is a hard constraint, not an afterthought (see REPS.md). Lowering is a single linear pass over the function: each value maps to one op, each edge adds a small fixed number of moves and a jump, and the op vector is preallocated. The cost is linear in function size.
Latest local Criterion means (cargo bench --bench bench --all-features, Windows x86_64 / WSL2, Rust stable, release build). compile includes the up-front validation pass:
| Function shape | Size | compile time |
|---|---|---|
| Straight-line arithmetic | 16 instructions | ~0.23 µs |
| Straight-line arithmetic | 256 instructions | ~2.2 µs |
| Straight-line arithmetic | 4096 instructions | ~31 µs |
| Diamond branches | 512 branches | ~121 µs |
| Countdown loop | header + back-edge | ~0.35 µs |
Numbers vary by CPU and environment; run the benches on your hardware for trends.
Configuration
Feature Flags
| Feature | Default | Description |
|---|---|---|
std |
on | Links the standard library. Without it the crate is #![no_std] and needs only alloc. |
serde |
off | Derives Serialize / Deserialize for Program and its parts, for caching or moving a compiled program between tools. |
# no_std build:
= { = "0.2", = false }
# with serialization:
= { = "0.2", = ["serde"] }
Testing
# Unit, integration (workflow), and doc tests
# Property tests only
# Lints and formatting
# Benchmarks (Criterion)
The integration suite in tests/workflow.rs compiles and runs representative functions — double, abs, max, a countdown loop — through a reference interpreter and checks the results. The property tests generate random straight-line functions and check that compiling and running matches an independent evaluation, and that every emitted program is structurally sound.
Cross-Platform Support
The crate is pure, dependency-light Rust with no platform-specific code, and is tested on Linux, macOS, and Windows (x86_64) through the CI matrix on stable and the 1.85 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.