WebAssembly
Almide compiles to two backends from one intermediate representation: native code (through Rust) and WebAssembly. The wasm target is not a reduced dialect or a separate implementation — it is the same compiler, the same standard library, and the same program semantics.
almide run app.almd --target wasm # compile and execute on wasmtimealmide build app.almd --target wasm # emit app.wasmrun --target wasm needs the wasmtime CLI on your
PATH; build does not.
The guarantee
Section titled “The guarantee”A program’s observable behaviour is identical on both targets. Observable means stdout, stderr, and the exit code. Native is the oracle: where the two disagree, the wasm side is wrong, and it is a compiler bug rather than a “platform difference” you are expected to work around.
fn main() -> Unit = {
let xs = [3, 1, 4, 1, 5, 9, 2, 6]
println(xs |> list.sort |> list.map(int.to_string) |> list.join(", "))
println(float.to_string(0.1 + 0.2))
} That sample prints the same bytes three ways: almide run natively, almide run --target wasm under wasmtime, and the Run button above — which compiles it
in your browser and executes it under a WASI shim. Float formatting is part of the promise too, and the second line is where that
bites. 0.1 + 0.2 is a different double from 0.3 — == says so — and
float.to_string prints the shortest decimal that round-trips back to the exact
value it was handed. 0.30000000000000004 is therefore the faithful rendering
rather than a verbose one: 0.3 would name a value the program does not hold.
When you do want 0.3, ask for it with float.to_fixed(x, 1).
That line is in the sample because it was once a real divergence. The wasm
backend formatted at fixed precision and printed 0.3 here while native printed
the full expansion — and it was closed by implementing exact big-integer Dragon4
in the wasm runtime, not by relaxing the guarantee. It is contract C-023, and
spec/wasm_cross/float_shortest_roundtrip.almd opens with this exact
expression.
Where the guarantee is named, it is enforced rather than asserted. The compiler
repository carries a behaviour contract ledger — 180+ named contracts, each
traceable to executable evidence, and 300+ cross-target fixtures that must
produce byte-identical output on both legs. A change to observable behaviour has
to update the ledger in the same commit. The count grows most weeks;
docs/contracts/README.md in the compiler repo carries the current figure.
The ledger is a ratchet, not a proof of universal equivalence. Behaviour a contract names is pinned by a fixture; behaviour outside the named set rests on the rule above without a machine check behind it. Divergences do still surface there — a handful were found by hand while these pages were being written — and they get fixed as compiler bugs rather than written up as target differences. That triage rule, not the fixture count, is the part of the promise that binds.
What the browser cannot do
Section titled “What the browser cannot do”Two different limits are easy to confuse.
Not available on the wasm target at all — a program using these refuses to build for wasm with a diagnosed wall:
http.*— no socketsprocess.*— no subprocesses
Available under a WASI host like wasmtime, but not in a browser:
env.args()— a page has no command linefs.*— only whatever the embedder mounts into the WASI filesystem. The playground, for example, mounts its data-file tabs, sofs.read_text("data.csv")works there.
Verified output
Section titled “Verified output”The wasm module is produced by the v1 trust-spine renderer, and what it renders
is what ships — the bytes are not post-processed. --wasm-opt runs
wasm-opt -Oz over the result for a smaller module, but that is an explicit
opt-in which deliberately leaves the verified envelope: the shipped bytes are
then no longer the exact bytes the trust-spine rendered.
almide build app.almd --target wasm # verbatim, verifiedalmide build app.almd --target wasm --wasm-opt # smaller, no longer verbatimIf the renderer cannot verify a program it refuses it with a diagnosed wall instead of silently falling back to unverified codegen. A wall is a bug report worth filing: it means a program shape the verified path does not cover yet.
Running it in a page
Section titled “Running it in a page”The playground is the reference embedding: the compiler itself is compiled to wasm, so both compilation and execution happen in the reader’s browser with no server involved. Every runnable sample in these docs is that same mechanism.
For shipping your own Almide code to the web as a library, see almide-wasm-bindgen, which generates the JavaScript/TypeScript bindings around a built module.
Next steps
Section titled “Next steps”- CLI Usage — every build and run flag
- Architecture — how one IR reaches two backends