Skip to content

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 wasmtime
almide build app.almd --target wasm # emit app.wasm

run --target wasm needs the wasmtime CLI on your PATH; build does not.

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.

Almide
main.almd
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))
}
Press Run to compile and execute this in your browser

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.

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 sockets
  • process.* — no subprocesses

Available under a WASI host like wasmtime, but not in a browser:

  • env.args() — a page has no command line
  • fs.* — only whatever the embedder mounts into the WASI filesystem. The playground, for example, mounts its data-file tabs, so fs.read_text("data.csv") works there.

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, verified
almide build app.almd --target wasm --wasm-opt # smaller, no longer verbatim

If 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.

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.