The model
- A
Moduledeclares functions (with aSignatureand a calling-convention tag, a linkage, and attributes), globals with initializers and relocations, and aggregates (structs, packed structs, arrays) with C layout. - A
Functionbody is blocks of instructions ended by one terminator. Values cross edges as block arguments. - A
Builderlowers into a body and rewrites it. Every instruction is type-checked as it is added. Mutable locals lower through variables (declare_var/def_var/use_var/seal_block); GC references can be used freely andinsert_statepointsrelocates them. Module::validatechecks everything;Displayandparseare the textual form.interp(featureinterp) runs it.
What it guarantees, and how each guarantee is checked:
| Guarantee | How it is held |
|---|---|
| A well-typed program built with the builder validates. | Property tests lower thousands of random programs (loops, calls, invokes, tail calls, memory, globals, checked arithmetic, exceptions) and validate each. |
| Dominance and the safepoint rule are decided exactly. | Property tests compare the validator with brute-force references on random control-flow graphs (loops, unreachable blocks, block parameters): dominance by path removal, reference liveness by path search. |
| Validation is total: it accepts or reports, never panics. | A sweep corrupts internal state thousands of times; property tests validate mutated text and the results of hostile rewrites. |
Operations follow specs/OPS.md (v2) exactly, on every type and policy. |
The interpreter is compared against independent reference arithmetic on the OPS edge values of every integer type and policy (integer pow and shift = saturate included), IEEE special cases, and exact rational checks of ieee_rem; float pow (ls_pow) matches all 122 vectors of specs/ops-vectors/pow.md bit for bit. |
| SSA construction from mutable variables preserves meaning and leaves no trivial block parameter. | Random programs with mutable variables, loops, invokes, and checked arithmetic are lowered through def_var/use_var, sealing blocks eagerly and lazily, and run against the independent evaluator; every result is checked for trivial parameters. |
| Statepoint insertion makes reference-carrying code valid without changing it. | Random programs keep references in variables across allocations, polls, safepoints, and invokes; after insertion they validate (the validator is the oracle), a second insertion changes nothing, and the interpreter's moving-collector simulation computes what an independent evaluator does. |
| Lowered programs mean what the source means. | Differential tests run each random program in an independent evaluator and, lowered, in the interpreter, before and after a print/parse round trip; results, traps, and exceptions agree exactly. |
| The textual form round-trips exactly. | parse(print(m)) prints identically and keeps every handle, over random programs and a module containing every instruction form. |
| Rewrites keep IR valid and meaning intact. | Random sequences of rewrites (split, duplicate-and-forward, remove dead code, swap operands, compact) are validated after every step and the result run against the evaluator. |
| GC references are never live across a safepoint without relocation. | The validator enforces it; the interpreter starts a new GC epoch at every safepoint and reports any stale reference. |
| Hostile input is bounded. | The parser caps input size and entity ids; the interpreter caps fuel, depth, and memory; every walk is iterative. Fuzzed with random and mutated text. |
Installation
[]
= "2.0.0-alpha.2"
With the reference interpreter:
[]
= { = "2.0.0-alpha.2", = ["interp"] }
Without the standard library:
[]
= { = "2.0.0-alpha.2", = false }
Quick start
Build, validate, print, and parse back fn add1(x: i64) -> i64 { x + 1 }:
use ;
let mut m = new;
let f = m.declare_function?;
let mut b = m.build?;
let x = b.param?;
let one = b.iconst?;
let y = b.add?;
b.ret?;
m.validate?;
assert_eq!;
assert_eq!;
# Ok::
Errors and exceptions are edges
An operation whose policy is error is a check terminator with an error edge carrying the OPS error code; a call that may throw is an invoke with a landing pad:
use ;
let mut m = new;
let may_throw = m.declare_function?;
let f = m.declare_function?;
let mut b = m.build?;
let x = b.param?;
let got = b.create_block?;
let pad = b.create_block?;
b.invoke?;
b.switch_to?;
let y = b.block_param?;
let sum = b.create_block?;
let overflow = b.create_block?;
b.check?;
b.switch_to?;
let s = b.block_param?;
b.ret?;
b.switch_to?;
b.trap?;
b.switch_to?;
let exn = b.block_param?;
b.resume?;
m.validate?;
# Ok::
Mutable locals and GC references
A front end names its mutable locals with variables and lets the builder place the block parameters; it keeps GC references in ordinary values across calls and lets insert_statepoints relocate them:
use ;
// fn keep(r: ref, n: i64) -> ref { let mut x = r; let mut i = 0; while i < n { poll(); i += 1 } x }
let mut m = new;
let poll = m.declare_function?;
let f = m.declare_function?;
let mut b = m.build?;
let = ;
let = ;
b.def_var?;
let zero = b.iconst?;
b.def_var?;
let = ;
b.jump?;
b.switch_to?;
let iv = b.use_var?;
let more = b.compare?;
b.branch?;
b.switch_to?;
b.seal_block?;
b.call?; // a safepoint; `x` is live across it
let iv = b.use_var?;
let one = b.iconst?;
let i2 = b.add?;
b.def_var?;
b.jump?;
b.seal_block?;
b.switch_to?;
b.seal_block?;
let xv = b.use_var?;
b.ret?;
b.seal_all_blocks?;
assert!; // `x` is used after the poll without relocation
let stats = b.insert_statepoints?;
assert_eq!;
m.validate?;
# Ok::
Running it
With the interp feature, the reference interpreter runs a module with the exact operation semantics:
#
# Ok::
More in examples/: lower (a front end's tree into IR), rewrite (a folding pass with the rewrite API), run (the interpreter).
Performance
Measured with cargo bench (criterion) on the development machine (Windows, x86-64), library only. Throughput scales linearly from 100k to 1M instructions.
| Operation (1M instructions) | Time | Throughput |
|---|---|---|
| Build, straight line | 49 ms | 20 M instructions/s |
| Build, chain of diamonds (250k blocks) | 134 ms | 7.5 M/s |
| Validate, straight line | 34 ms | 30 M/s |
| Validate, diamonds | 120 ms | 8.3 M/s |
| Print, diamonds | 244 ms | 4.1 M/s |
| Parse, diamonds | 652 ms | 1.5 M/s |
| Compact, diamonds | 144 ms | 7.0 M/s |
| Validate 100k safepoints relocating a reference | 12 ms | 8.1 M/s |
New in 2.0.0-alpha.2, measured while other builds shared the machine (the unchanged safepoint validation above varied between 11 and 40 ms in the same session, so read these as upper bounds):
| Operation | Time | Throughput |
|---|---|---|
| SSA construction, 100k instructions of diamonds through variables | 59–112 ms | about 1.3 M instructions/s |
| SSA construction, 1M instructions | 0.75–1.24 s | about 1.1 M/s |
insert_statepoints, 100k instructions (33k safepoints, two references live across each, 66k join parameters) |
165–229 ms | about 0.5 M/s |
ls_pow, 10,000 calls |
4.6–5.4 ms | about 2 M calls/s |
The textual form is for tests and debugging; nothing on the compile path parses text.
Contributing
See dev/DIRECTIVES.md for engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.