# WebAssembly

> Compile Brine programs to WebAssembly with --target wasm (a WASI module, run with Node.js or any WASI runtime) and --target web (a folder a browser opens), set up wasi-sdk, use the brine.js loader, and know the limits.

Source: https://docs.brinelang.org/tooling/webassembly/

Brine programs compile to WebAssembly as well as to native executables, with the same checks and the same results. `--target wasm` makes a `.wasm` module for WASI runtimes, and `brine run` and `brine test` run it with Node.js. `--target web` makes a folder a browser opens: a page, a small loader (`brine.js`) and the module. Both need [wasi-sdk](#setup). WebAssembly runs on one thread, so `par` constructs run in order (with the same result bits), and there are no processes, sockets or GPU.

```sh
brine run --target wasm collatz.br           # build a .wasm module and run it with Node.js
brine build --target wasm collatz.br         # collatz.wasm, for any WASI runtime
brine build --target web collatz.br -o site  # site/: index.html, brine.js, program.wasm
```

Note

WebAssembly support is on Brine’s main branch and not in a release yet: Brine 0.4.0 has no `--target`. To use it now, [build Brine from source](/getting-started/installation/#build-from-source).

## Setup

[Section titled “Setup”](#setup)

WebAssembly builds compile the generated C with the clang and C library of [wasi-sdk](https://github.com/WebAssembly/wasi-sdk/releases), not with your system C compiler. Download the archive for your platform, unpack it, and set `WASI_SDK_PATH` to its folder:

```sh
export WASI_SDK_PATH=~/tools/wasi-sdk-34.0-x86_64-linux
```

Brine looks for `$WASI_SDK_PATH/bin/clang`, then for `/opt/wasi-sdk/bin/clang`, so unpacking it to `/opt/wasi-sdk` also works. Without either, a WebAssembly build stops with [E0702](/reference/errors/#e0702):

```text
error[E0702]: no WebAssembly C toolchain found
  = note: brine builds WebAssembly with the clang and C library of wasi-sdk
  = help: download wasi-sdk from https://github.com/WebAssembly/wasi-sdk/releases and set WASI_SDK_PATH to its folder
  = run `brine explain E0702` for more about this error
error: could not compile due to 1 error
```

To run modules, `brine run --target wasm` and `brine test --target wasm` also need [Node.js](https://nodejs.org) on your `PATH` (Brine’s CI tests with Node 24), or another WASI runtime in [`BRINE_WASM_RUNNER`](#other-wasi-runtimes). `brine doctor` shows what it found on its last line:

```text
  ✓ WebAssembly      C:/Users/you/tools/wasi-sdk-34.0-x86_64-windows\bin\clang.exe
```

Without wasi-sdk it reads:

```text
  - WebAssembly      no wasi-sdk (optional, for `--target wasm`; set WASI_SDK_PATH)
```

WebAssembly is optional, so a missing wasi-sdk doesn’t make `brine doctor` fail.

## Run a program with –target wasm

[Section titled “Run a program with –target wasm”](#run-a-program-with-target-wasm)

This program uses `par` for a map and two reductions:

collatz.br

```brine
fn collatz_len(n: int) -> int:
    var x = n
    var steps = 0
    while x != 1:
        x = x / 2 if x % 2 == 0 else 3 * x + 1
        steps += 1
    return steps


fn main():
    let a = args()
    let n = int(a[0]) if len(a) > 0 else 1000
    let lens = par [collatz_len(i) for i in 1..=n]
    let total = par sum(float(l) for l in lens)
    let longest = par max(l for l in lens)
    print(f"{os.name()} {os.arch()}, n = {n}")
    print(f"average steps: {total / float(n):.2f}, longest: {longest}")
```

```sh
brine run --target wasm collatz.br -- 1000000
```

```text
     Built collatz [dev, wasm] in 0.15s
   Running C:/Users/you/AppData/Local/Temp/brine-cache/07c4ee958925d33a/dev-wasm/collatz.wasm
wasi wasm32, n = 1000000
average steps: 131.43, longest: 524
```

The native build prints the same numbers (`windows x86_64, n = 1000000` on the first line). `os.name()` is `"wasi"` and `os.arch()` is `"wasm32"` in a WebAssembly build.

`brine run` runs the module with a small script around Node’s `node:wasi` module. The program gets its arguments and the environment, and the current folder is opened for it, so relative paths (`read_file("data.txt")`, `write_file("out.txt", ...)`) work as they do natively. Nothing outside the current folder is reachable: `read_file("/etc/hosts")` returns `Err("cannot read /etc/hosts: No such file or directory")`. The exit code is the program’s, `101` after a panic, as for a [native run](/tooling/cli/#exit-code):

```text
     Built panic [dev, wasm] in 0.11s
   Running C:/Users/you/AppData/Local/Temp/brine-cache/b2e1b9ea98c70c0b/dev-wasm/panic.wasm


panic: index 5 is out of bounds for an array of length 3
  --> panic.br:4:11
```

A crash of the module itself (a WebAssembly trap) prints `WebAssembly trap: ...` and exits with `134`. See [Deep recursion](#deep-recursion) for the common case.

### Tests

[Section titled “Tests”](#tests)

`brine test --target wasm` builds the tests as one module and runs each test in its own Node.js process, as a native `brine test` does with executables. The output is the same:

```text
     Built tests [dev, wasm] in 0.15s
test double doubles ... ok
test double of zero ... FAILED


---- double of zero ----


panic: assertion failed: double(0) == 1
  --> t.br:8:5


test result: FAILED. 1 passed; 1 failed; finished in 0.04s
```

### Other WASI runtimes

[Section titled “Other WASI runtimes”](#other-wasi-runtimes)

`brine build --target wasm` writes a module for WASI preview 1 (`wasm32-wasip1`), so any WASI runtime can run it. To have `brine run` and `brine test` use one instead of Node.js, set `BRINE_WASM_RUNNER` to its command; Brine adds the module’s path and the program’s arguments after it:

```sh
BRINE_WASM_RUNNER="wasmtime run --dir=." brine run --target wasm collatz.br -- 1000000
```

The value is split at spaces (no quoting). Give the runtime access to the current folder (`--dir=.` for wasmtime) if the program reads or writes files. Without Node.js and without `BRINE_WASM_RUNNER`, `brine run --target wasm` builds the module and then stops:

```text
error: running WebAssembly needs Node.js (https://nodejs.org) or a WASI runtime in BRINE_WASM_RUNNER (say `wasmtime run --dir=.`)
```

## Build for the browser with –target web

[Section titled “Build for the browser with –target web”](#build-for-the-browser-with-target-web)

```sh
brine build --target web collatz.br -o site
```

```text
     Built collatz [dev, web] in 0.15s
    Output site


Open its index.html over HTTP, e.g. `python3 -m http.server -d site` and http://localhost:8000
```

The output is a folder you can put on any static host:

| File           | What it is                                                                                                |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| `program.wasm` | the program                                                                                               |
| `brine.js`     | the loader: the WASI calls a Brine program makes, for the browser (about 6 KB)                            |
| `index.html`   | a starter page that runs the program and shows what it prints, stderr in red, with a **Run again** button |

Serve the folder over HTTP: browsers don’t load WebAssembly from `file://` pages.

```sh
python3 -m http.server -d site     # then open http://localhost:8000
```

Where the folder goes:

| Build                | Output                                                          |
| -------------------- | --------------------------------------------------------------- |
| project, no `-o`     | `target/<profile>-web/<name>-web/` (`target/dev-web/demo-web/`) |
| single file, no `-o` | `<name>-web/` in the current directory (`collatz-web/`)         |
| `-o path`            | `path/`                                                         |

### Rebuilding keeps your page

[Section titled “Rebuilding keeps your page”](#rebuilding-keeps-your-page)

`index.html` is a starting point. Replace it with your own page: a rebuild writes `program.wasm` and `brine.js` again but leaves an existing `index.html` alone. Other files you add to the folder (scripts, styles, images) are left alone too. The build’s change stamp lives in the build cache, not in the folder, so the folder holds only what you deploy.

### The run function

[Section titled “The run function”](#the-run-function)

`brine.js` exports one function:

```js
import { run } from "./brine.js";


const code = await run("program.wasm", {
  args: ["1000000"],
  env: { MODE: "fast" },
  stdout: (text) => console.log(text),
  stderr: (text) => console.error(text),
});
```

| Argument         | Meaning                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| `wasm`           | the module: a URL (string or `URL`), a `Response`, the bytes, or a compiled `WebAssembly.Module` |
| `options.args`   | the program’s arguments, what `args()` returns (default: none)                                   |
| `options.env`    | an object of environment variables, what `getenv` sees (default: none)                           |
| `options.stdout` | called with text the program prints (default: `console.log`)                                     |
| `options.stderr` | called with text the program writes to stderr, panics included (default: `console.error`)        |

`run` resolves to the exit code: `0` when `main` returns normally, the code the program exits with (`exit(code)`, or the value an `int` `main` returns), `1` when `main` returns `Err`, and `101` after a panic. The panic message arrives through `stderr`. If the module traps (for example, it [runs out of stack](#deep-recursion)), the promise rejects with the engine’s error instead.

Output arrives as text whenever the program flushes it, so one call can hold several lines, or part of one. Append it to what you have; don’t treat each call as a line.

To run a program more than once, compile it once and pass the `WebAssembly.Module`. Each `run` starts a fresh instance:

```js
const module = await WebAssembly.compileStreaming(fetch("program.wasm"));
await run(module, { args: ["10"] });
await run(module, { args: ["20"] });
```

What the loader provides, and what it doesn’t:

* **No files.** No folder is opened for the program, so opening any file fails: `read_file("data.txt")` returns `Err("cannot read data.txt: No such file or directory")`, `write_file` fails the same way, and `file_exists` is `false`. Pass input through `args` or `env` instead.
* **Empty standard input.** Reading stdin gives end-of-file at once.
* **`os.sleep` spins.** The loader has no way to pause a thread, so a sleep busy-waits until its time is up. It keeps the CPU busy and, on the main thread, freezes the page.
* **Clocks** come from the browser’s `performance.now()` and `Date.now()`.

### Run it in a Web Worker

[Section titled “Run it in a Web Worker”](#run-it-in-a-web-worker)

`run` calls the module’s `_start` and returns only when the program has finished. Until then, the thread it runs on does nothing else. On a page’s main thread, that means no clicks, no scrolling and no repaints: in a test, a 10 ms timer didn’t fire once during a 0.5 s run. Anything that takes more than a moment, or sleeps, belongs in a Web Worker.

A worker that runs the program once per message and sends its output back:

site/worker.js

```js
import { run } from "./brine.js";


// Each message runs the program once: postMessage({ args: [...] })
self.onmessage = async (e) => {
  try {
    const code = await run("program.wasm", {
      args: e.data.args,
      stdout: (text) => self.postMessage({ stdout: text }),
      stderr: (text) => self.postMessage({ stderr: text }),
    });
    self.postMessage({ exit: code });
  } catch (err) {
    // a trap, such as running out of stack
    self.postMessage({ error: String(err) });
  }
};
```

A page that starts it. Save it as the folder’s `index.html`; rebuilds keep it:

site/index.html

```html
<!doctype html>
<meta charset="utf-8">
<pre id="out"></pre>
<script type="module">
  const out = document.getElementById("out");
  const worker = new Worker("worker.js", { type: "module" });
  worker.onmessage = (e) => {
    const m = e.data;
    if ("exit" in m) out.append(`[exit code ${m.exit}]\n`);
    else out.append(m.stdout ?? m.stderr ?? m.error);
  };
  worker.postMessage({ args: ["1000000"] });
</script>
```

With `collatz.br` built into `site/` and the folder served over HTTP, the page shows:

Output (Chrome)

```text
wasi wasm32, n = 1000000
average steps: 131.43, longest: 524
[exit code 0]
```

The worker is a module worker (`type: "module"`), because `brine.js` is an ES module. `"program.wasm"` resolves against the worker script’s URL, so keep `worker.js` in the same folder.

## Limits

[Section titled “Limits”](#limits)

The same source compiles for WebAssembly with no changes. Some things the platform doesn’t have, and each fails with a message that says so.

### One thread

[Section titled “One thread”](#one-thread)

WebAssembly builds run on one thread. `par for`, `par [...]`, `par` reductions and `par(...)` forks run in order on that thread. Because [reductions combine in a fixed order](/parallelism/deterministic-reductions/) that doesn’t depend on the thread count, the results are the same bits as a native run on any number of cores. Only the speed differs.

`os.cpu_count()` returns `1`. `--threads` and [`BRINE_THREADS`](/tooling/environment-variables/#brine_threads) have no effect.

### No GPU

[Section titled “No GPU”](#no-gpu)

WebAssembly builds contain no GPU code, so every `par` construct runs on the CPU. With [`BRINE_GPU_LOG=1`](/tooling/environment-variables/#brine_gpu_log) the reason is printed:

```text
brine gpu: gpu.br:3:14 par map n=1000000 -> cpu 0.734 ms (not GPU-eligible: WebAssembly has no GPU)
brine gpu: gpu.br:4:17 par reduction n=1000000 -> cpu 1.282 ms (not GPU-eligible: WebAssembly has no GPU)
```

### One world rank

[Section titled “One world rank”](#one-world-rank)

A [`world`](/parallelism/world/) program runs as a single rank: `world.size()` is `1`, and collectives such as `world.sum` return the rank’s own value. `--ranks` is refused:

```sh
brine run --target wasm --ranks 2 world.br
```

```text
error: `--ranks` needs native processes; WebAssembly programs run as one rank
```

A module started with `BRINE_WORLD_SIZE` above 1 stops with exit code `2`:

```text
brine: `world` programs can't run with more than one rank on WebAssembly
```

### No processes or sockets

[Section titled “No processes or sockets”](#no-processes-or-sockets)

`run`, `os.spawn` and `os.free_ports` return `Err` values, which the program can handle like any other failure:

```text
Err("cannot run git: WebAssembly programs can't start processes")
Err("WebAssembly programs have no network ports")
```

`os.pid()` returns `1`.

### Files

[Section titled “Files”](#files)

Under `brine run --target wasm` (Node.js), the program can read and write files in the current folder and below, and nothing else. In the browser, [there are no files](#the-run-function).

### Deep recursion

[Section titled “Deep recursion”](#deep-recursion)

The engine that runs WebAssembly (V8, in Node.js and Chrome) limits the call stack, and that limit is reached long before a native program’s. This function, called with 15,000 levels of recursion, traps under Node.js 24 where a native build doesn’t:

deep.br

```brine
fn depth(n: int) -> int:
    if n == 0:
        return 0
    return 1 + depth(n - 1)


fn main():
    print(depth(int(args()[0])))
```

```text
     Built deep [dev, wasm] in 0.11s
   Running C:/Users/you/AppData/Local/Temp/brine-cache/b3a8a1c856a7f000/dev-wasm/deep.wasm


WebAssembly trap: Maximum call stack size exceeded
```

The exit code is `134`. In the browser, `run` rejects with `RangeError: Maximum call stack size exceeded`. Rewrite deep recursion as a loop if it must run on WebAssembly.

### Commands

[Section titled “Commands”](#commands)

| Command                    | Error                                                                                               |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| `brine run --target web`   | ``a web build runs in a browser: build it with `brine build --target web` and open its index.html`` |
| `brine test --target web`  | ``tests run with `--target wasm` (Node.js), not in a browser``                                      |
| `brine build --target arm` | ``unknown target `arm` (use host, wasm or web)``                                                    |

`brine debug` has no `--target`: debugging is native only. Each of these exits with `2`.

## Profiles and size

[Section titled “Profiles and size”](#profiles-and-size)

WebAssembly builds always go through the [C backend](/tooling/builds-and-backends/#backends): the native x86-64 and AArch64 backends write machine code, not WebAssembly. The profiles keep their C flags:

| Profile | Command                   | C compile                                    | Output in a project               |
| ------- | ------------------------- | -------------------------------------------- | --------------------------------- |
| dev     | `--target wasm`           | `-O2`, split into units compiled in parallel | `target/dev-wasm/<name>.wasm`     |
| release | `--target wasm --release` | `-O3`, one unit                              | `target/release-wasm/<name>.wasm` |
| debug   | `--target wasm --debug`   | `-O0 -g`                                     | `target/debug-wasm/<name>.wasm`   |

`--target web` uses the same flags, with `-web` folders (`target/release-web/`). A single file without `-o` builds `<name>.wasm` in the current directory. With `--emit-c`, the generated C is written next to the output, as for a native build.

Because every profile goes through C, a dev build for WebAssembly is slower to build than a native dev build on x86-64 or AArch64, but still takes a fraction of a second for a small program once the runtime is cached.

A module carries the Brine runtime, so even a program that only prints `hi` is about 400 KB (about 130 KB with gzip, which static hosts usually apply). `collatz.br` is 428 KB in a dev build and 429 KB in a release build: release makes the code faster, not smaller. A debug build is about 640 KB.

## How it works

[Section titled “How it works”](#how-it-works)

* **Through C and wasi-sdk.** Brine generates the same C it generates for a native C build and compiles it, and the runtime (`brine_rt.c`), with wasi-sdk’s clang for `wasm32-wasip1`. The module gets an 8 MB stack for Brine code, like a native main thread. Everything before code generation is unchanged: the type checker and the prover see the same program, so a program that checks natively checks for WebAssembly.
* **The 64-bit memory layout on wasm32.** wasm32 has 4-byte pointers, but Brine’s generated code and runtime lay memory out as on a 64-bit machine. Each pointer, in a struct field or an array element, takes an 8-byte slot, with the pointer in its low half. So the IR, the prover and the native backends don’t change, and on 64-bit platforms the code compiles exactly as before. `int` is 64 bits everywhere, and `i128`/`u128` work as they do natively.
* **One thread in the runtime.** Locks do nothing, worker threads are never started, and the thread pool has one thread, so every `par` construct runs on the calling thread.
* **Two loaders.** `brine run` uses a short Node.js script around `node:wasi`, written to the build cache. `--target web` uses `brine.js`, which implements the WASI calls a Brine program makes for the browser. Calls it doesn’t implement (files, sockets) return an error code, which the program sees as an `Err`.

Threads on WebAssembly (wasi-threads and Web Workers, so `par` runs in parallel there too), files in the browser, and a playground page that runs the compiler itself in the browser are [planned](/reference/roadmap/#webassembly).

## Summary

[Section titled “Summary”](#summary)

* `--target wasm` builds a WASI module; `brine run` and `brine test` run it with Node.js, or with `BRINE_WASM_RUNNER`.
* `--target web` builds a folder with `index.html`, `brine.js` and `program.wasm`. Rebuilds keep your `index.html`.
* Both need wasi-sdk: set `WASI_SDK_PATH`.
* `run(wasm, { args, env, stdout, stderr })` resolves to the exit code (`101` after a panic). Run long programs in a Web Worker.
* One thread (`par` runs in order, same bits), one `world` rank, no processes, sockets or GPU, and no files in the browser.

## Related

[Section titled “Related”](#related)

* [The brine command](/tooling/cli/)
* [Builds, backends and performance](/tooling/builds-and-backends/)
* [Environment variables](/tooling/environment-variables/#webassembly)
* [Platforms](/reference/platforms/#webassembly)
* [Deterministic reductions](/parallelism/deterministic-reductions/)
