Reference
How it is built.
One language, four ways to run it, and a set of gates whose only job is to stop them disagreeing. The interpreter is the specification; the other three have to match it.
The pipeline
| Stage | Crate | What it produces |
|---|---|---|
| Lexer and parser | grasm_syntax | Tokens, then a tree. Also the formatter. |
| Checker | grasm_sema | Types, effect rows, ownership, exhaustiveness — and an Analysis the backend reads. |
| Interpreter | grasm_interp | The normative answer. |
| Native backend | grasm_native | Cranelift IR, then an object file. |
| Runtime | grasm_rt | Reference-counted boxed values with copy-on-write. |
| Operating system | grasm_sys | Threads, thread-locals, locks, files, sockets, subprocesses, float maths. Raw syscalls and libc; no std. |
| Program ABI | grasm_rt_abi | The #[global_allocator] and #[panic_handler] a linked program needs, and nothing else. |
| Fibers | grasm_fiber | The stack switch a lightweight task runs on. |
| Wasm | grasm_wasm | The interpreter, compiled for wasm32. |
Why a compiled program links no standard library
A hello-world was 351.1 KB, and about 135 KB of it was
Rust's own backtrace symbolizer — a DWARF reader, pulled in by
std's default panic hook, which a hello-world never calls. All four
crates a compiled program links are no_std now, which is what
grasm_sys exists for: every operating-system facility the runtime
needs, over raw syscalls and libc rather than over std. A
hello-world is 85.7 KB, and nm shows zero symbolizer
symbols.
Two consequences worth stating, because they are constraints rather than
wins. A #[panic_handler] returns !, so a
no_std crate cannot unwind — panic = "abort" is not a
preference here, it is the only option. And the operating-system layer is now
code this project owns and must get right: the conversion found nine defects,
listed on the scorecard, of which the gates on this
page would have caught none.
Why the checker hands types to the backend
It did not, once. The backend re-derived them — roughly 950 lines of a second type inference — and nine of twelve findings in one period traced to the two disagreeing. The checker now exports every expression’s resolved type; it answers 94% of the backend’s questions on real code and 87% on generated code. Where it answers, the two cannot disagree, because there is only one answer.
Nothing was deleted on the strength of that, and the reason is recorded: an arm the corpus never reaches is not an arm nothing reaches.
The memory model, plainly
Boxed values — strings, lists — are reference-counted with copy-on-write. The count is atomic so a value may cross a thread, and that cost was measured rather than assumed: 4.3% on the most reference-count-heavy program here, no movement at all on three others, about half a percent on the geometric mean.
Effects, capabilities, ownership modes and exhaustiveness are erased before
codegen and cost nothing at run time. ref T costs a generation
check. region is checked but does not yet free in bulk — the
specification says so in the clause itself rather than implying otherwise.
The gates
113 of them, each judged by its exit code and nothing else. That rule
exists because a gate once reported identical: 184 mismatched: 0,
which was true, while failing three lines further down.
Agreement
difftest, crosstest, wasmtest and agreetest run the corpus through every engine and require identical bytes.
Differential
Against things outside this project: OpenSSL for certificates, real zlib, git’s own object store, V8 for WebAssembly, clang’s assembler.
Generated
15 generators build programs nobody wrote, then require every engine to agree on them too.
Laws
205 properties that must hold. A law a degenerate implementation satisfies is not a law, and each one here is chosen so that it is not.
Record and replay
Every builtin that touches the world is journaled. A recorded run replays deterministically, and the gate requires each journaled builtin to refuse under an empty journal — so a replay cannot quietly reach the real world instead.