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. 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 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, IEEE special cases, and exact rational checks of ieee_rem. |
| 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.1"
With the reference interpreter:
[]
= { = "2.0.0-alpha.1", = ["interp"] }
Without the standard library:
[]
= { = "2.0.0-alpha.1", = 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::
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 |
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.