The model
- A
Functionis blocks of target instructions. Instructions name virtual registers (each typed by a register class), physical registers, stack slots, and other functions by symbol. Values cross edges as block parameters: a branch passes arguments, and the target's parameters receive them all at once. - A
Targetdescribes an ISA as data: its registers, its register classes, and its instructions, each with operand rules (definitions, uses, two-address ties, accepted shapes and immediate ranges) and the registers it reads, writes, and clobbers without naming them (idivreadsrdx:rax, a shift byclreadsrcx, flag producers define the flags register). - An
Abiis a calling convention: argument and return registers, callee-saved registers, stack alignment, shadow space, red zone, and stack probing.layout_frameturns a function's slots and saved registers into offsets and a prologue/epilogue plan. - The
MoveResolverturns any parallel copy into moves and swaps a machine can execute one at a time. - The
Emitterencodes physical-register functions through isa-lang, resolving every edge's parallel copy on the way.
What it guarantees, and how each guarantee is checked:
| Guarantee | How it is held |
|---|---|
| Parallel copies are exact: any permutation, cycles of every length, fan-out, registers and stack slots mixed, immediates. | Property tests run the resolver against a parallel-assignment reference over random copies built from explicit cycles, chains, fan-out, and immediates; every step is checked to be one x86-64 can execute; every location that is not a destination is checked unchanged. Cycles of every length from 2 to 14 registers and to 64 slots; a 100,000-slot permutation. |
| Moves are minimal under the resolver's rules. | For register-only copies, the step count is checked equal to the minimum: one per copy, k - 1 swaps per k-cycle (or k + 1 moves without swaps), nothing extra for a cycle whose value is also copied out of it. |
| Emitted edges perform their parallel copy. | Random jump and branch edges over all fourteen allocatable registers and up to six stack slots are emitted, executed by an x86-64 interpreter in the test suite, and checked location by location. |
| Every instruction row encodes, and its implicit operands agree with isa-lang. | 913 operand shapes over every opcode are emitted and decoded back; the flags each row declares as read and written are checked against isa-lang's flag effects, and fixed registers against isa-lang's forms. |
| Frames are aligned and disjoint, under SysV and Win64. | Property tests over random slots, saved registers, calls, and outgoing areas simulate each prologue: the stack is 16-aligned at calls, and slots are aligned, disjoint, and clear of the saved registers and the outgoing area. |
| MIR → isa-lang → object-lang → a running process. | Three hand-built programs (a Fibonacci loop with parallel-copy edges, recursive factorial across two functions, and a mix of the instruction table with an indirect call) are wrapped into static ELF executables and run on Linux x86-64 with their exit status checked, and run in the interpreter on every platform. |
| No panics on hostile construction. | Random opcodes with random valid, foreign, and ill-shaped operands are built, verified, printed, laid out, and emitted; every failure is an error value. |
Installation
[]
= "0.2"
Without the standard library:
[]
= { = "0.2", = false }
Quick start
emit is the one-call path: a function on physical registers in, x86-64 machine code out.
use ;
use ;
// fn double(x) -> x + x under SysV: x arrives in rdi, the result leaves in rax.
let mut f = new;
let entry = f.block?;
f.add_param?;
f.push?;
f.push?;
f.push_with?;
f.omit_frame_pointer;
let code = emit?;
// mov rax, rdi; add rax, rdi; ret
assert_eq!;
# Ok::
Edges are parallel copies
A loop whose back edge swaps its two values. The resolver sees the 2-cycle and emits one xchg; a cycle through memory would borrow a register instead:
use ;
use ;
use ;
let mut f = new;
f.omit_frame_pointer;
let = ;
f.add_param?;
f.add_param?;
let start = f.block_call?;
f.push?;
f.push?;
let again = f.block_call?; // swapped
let done = f.block_call?;
f.push?;
f.push_with?;
let code = emit?;
let text: = decode_all
.filter_map
.map
.collect;
assert!;
# Ok::
Calling conventions and frames
use ;
use ;
use ;
let mut locs = Vecnew;
SYSV.assign_args;
assert_eq!;
assert_eq!;
locs.clear;
let outgoing = WIN64.assign_args;
assert_eq!;
assert_eq!; // above the 32 bytes of shadow space
assert_eq!;
// A SysV leaf with a small frame keeps it in the red zone below rsp.
let mut leaf = new;
let slot = leaf.stack_slot?;
let frame = leaf.frame_layout?;
assert!;
assert!;
# Ok::
Examples
Runnable programs in examples/:
| Example | What it shows |
|---|---|
emit |
Build a loop in MIR, print it, emit it, and disassemble the result. cargo run --example emit |
moves |
The parallel-move resolver on a copy mixing a register rotation, fan-out to a slot, a slot swap, and an immediate, with and without a free register. cargo run --example moves |
Performance
Library-only numbers (no process start, no object file), measured with the benchmarks in benches/ on Windows x86_64, Rust stable, release profile:
| Benchmark | What it measures | Time |
|---|---|---|
mir_100k/build |
Build a 100,000-instruction function (2,000 blocks, each ending in a branch with two four-value edges). | ~4.3 ms |
mir_100k/verify |
Verify it. | ~1.3 ms |
mir_100k/emit |
All of emit: verify, lay out the frame, resolve every edge, encode, relax branches. |
~10.5 ms (~9.5 M instructions/s) |
mir_100k/print |
Its text form. | ~6.8 ms |
moves/permutation/4 |
A random permutation of four registers. | ~150 ns |
moves/permutation/100000 |
A random permutation of 14 registers and 99,986 slots. | ~11 ms |
moves/edges_100k_x4 |
100,000 four-move edges through one reused resolver. | ~17 ms |
Everything is linear except one sort when the resolver numbers a copy's locations (O(n log n)); a reused resolver allocates nothing once it has seen its largest copy.
Design notes
- Block parameters, not phi nodes. The same information as phis, kept on the edge where it is used: each edge's parallel copy is written down on that edge. ir-lang already uses block parameters, an SSA register allocator resolves each edge with the same resolver, and there is no phi list to keep in sync with predecessor order.
- Implicit operands are data. isa-lang 0.2 describes each form's explicit operands only. A register allocator needs every register an instruction touches, so this crate's instruction table records implicit uses, definitions, and clobbers per row, and calls and returns carry their ABI registers per instruction. The flags are a physical register: producers define it, consumers use it, and an instruction that may leave some flags unchanged (
inc, a shift by zero) also uses it, so a flag value never appears to come from an instruction that might not have produced it. - The stack pointer does not move in the body. Slots have fixed offsets from it; stack arguments are stores to
[rsp + offset];pushandpopbelong to prologues and epilogues only. - Edges split themselves. A conditional branch whose taken edge carries moves branches on the negated condition around them, so critical edges never need an extra block.
- No rented parts. Encoding is isa-lang's and object files are object-lang's; both are this family's own.
Testing
The integration tests carry a small x86-64 interpreter (tests/common) that executes the bytes the emitter produces, written against isa-lang's decoder alone, so emitted programs and edges are checked on every platform; on Linux x86-64, tests/executable.rs also runs them as real processes. Every rust example in this README and in docs/API.md is compiled and run as a doctest.
Contributing
See REPS.md for the engineering standards every change is held to, dev/DIRECTIVES.md for the definition of done, and dev/ROADMAP.md for the roadmap. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.