Grasm v1.0.0 Get started

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

StageCrateWhat it produces
Lexer and parsergrasm_syntaxTokens, then a tree. Also the formatter.
Checkergrasm_semaTypes, effect rows, ownership, exhaustiveness — and an Analysis the backend reads.
Interpretergrasm_interpThe normative answer.
Native backendgrasm_nativeCranelift IR, then an object file.
Runtimegrasm_rtReference-counted boxed values with copy-on-write.
Operating systemgrasm_sysThreads, thread-locals, locks, files, sockets, subprocesses, float maths. Raw syscalls and libc; no std.
Program ABIgrasm_rt_abiThe #[global_allocator] and #[panic_handler] a linked program needs, and nothing else.
Fibersgrasm_fiberThe stack switch a lightweight task runs on.
Wasmgrasm_wasmThe 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.