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; a negative pow exponent is pinned as the E0006 error under every policy and signedness, in the interpreter and in the validator's check placement), 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.3"
With the reference interpreter:
[]
= { = "2.0.0-alpha.3", = ["interp"] }
Without the standard library:
[]
= { = "2.0.0-alpha.3", = 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.