Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion quest/m2/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Decided with the maintainer on 2026-11-09, in the quest audit:
[the model epic](/quest/m2/ts-model/README.md)'s 2026-11-01 "the translated model is
moq's browser route". Rejected: JS-only staying the goal, with rust2-wasm only a
baseline; pausing rust2-ts until rust2-wasm is measured.
- ✅ Web and WASI are separate targets. [The web target](/quest/m2/wasm/backend.md) calls
web APIs directly, `performance.now` for its clock, and ships nothing of WASI;
[the WASI target](/quest/m3/wasi.md) serves hosts with no JavaScript, in m3 as the
other targets planned ahead of a consumer are. Rejected: one `wasm32-wasip1` module for
both, with the binding implementing its imports.
- ✅ m2, with [per-target export](/quest/m1/ffi-export/target.md) in m1. Rejected: all of
it in m1; all of it in m2.
- ✅ Until the gate reports, translator work is only what the gate needs: the model's
Expand Down Expand Up @@ -53,5 +58,5 @@ record the superseded decision; they are moq's tree's to update.

## Required

- [The exporter's wasm32 target](/quest/m2/wasm/backend.md) - the glue crate builds for wasm32 with a generated TypeScript binding, proven on the api fixture
- [The exporter's web target](/quest/m2/wasm/backend.md) - the glue crate builds for wasm32 with a generated TypeScript binding over web APIs, proven on the api fixture
- [Every route on the browser benchmark](/quest/m2/wasm/routes.md) - `just ts bench` runs rust2-wasm's export and a `wasm2js` control beside the translation
59 changes: 38 additions & 21 deletions quest/m2/wasm/backend.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
# [L] The exporter's wasm32 target
# [L] The exporter's web target

## Goal

`rust2-ffi --target wasm32-unknown-unknown` writes, beside the glue crate, a TypeScript
binding over the `.wasm` that `cargo build` makes of it: ES modules a bundler takes as
is, typed as the other languages' bindings are, with no wasm-bindgen and no tool run on
the `.wasm` afterwards. The `api` fixture's script runs through it in Node, Chrome, and
rust2-ffi's web target writes, beside the glue crate, a TypeScript binding over the
`.wasm` that `cargo build` makes of it: ES modules a bundler takes as is, typed as the
other languages' bindings are, with no wasm-bindgen and no tool run on the `.wasm`
afterwards. What the module needs of its platform comes from web APIs, called directly:
its clock is `performance.now` and `Date.now`, its randomness `crypto.getRandomValues`,
and its stderr `console.error`, with no WASI runtime or polyfill in the bundle. The `api`
fixture's script runs through it in Node, Chrome, and
Firefox, its transcript matching native Rust's line for line, with its futures checked as
the other languages' are. The fixture spawns a thread (`ffi/fixtures/api/src/lib.rs`),
which wasm32 cannot, so what needs one moves to a fixture of its own that the native
languages keep running.

## Plan

Decided with the maintainer on 2026-11-09: ✅ web and WASI are separate targets, and the
web's uses web APIs wherever one exists ("like performance.now()"). Rejected: one
`wasm32-wasip1` module for both, whose imports the binding satisfies, as this quest first
recommended. [The WASI target](/quest/m3/wasi.md) is its own quest.

Decided with the maintainer on 2026-11-09: planned in
[the rust2-wasm epic](/quest/m2/wasm/README.md). The rules every generator follows
([the exporter epic](/quest/m1/ffi-export/README.md)) apply: the contract check at load,
Expand All @@ -28,37 +36,46 @@ measurement on [the benchmark](/quest/m2/wasm/routes.md):
Rust returns or lends are an object of their pointer and length that makes its view
from the current `memory.buffer` at each read, and hands out a copy where a caller
wants a `Uint8Array` to keep. Test a callback that grows memory while it reads what it
was lent. Bytes passed in are copied once into memory the call frees. Text is one `TextDecoder` or `TextEncoder` call; JS string builtins do not help
UTF-8 held in linear memory.
was lent. Bytes passed in are copied once into memory the call frees. Text is one
`TextDecoder` or `TextEncoder` call; JS string builtins do not help UTF-8 held in
linear memory.
- A handle is an integer in a class with `[Symbol.dispose]`, and a `FinalizationRegistry`
frees one never disposed, as the other languages' garbage collectors do.
- A future is a `Promise`: the binding polls once at the call, and an imported `wake`
schedules the next poll on a microtask, never inside the wake. Cancelling frees the
future and loses nothing. JSPI is not needed, since no Rust code blocks on JS.
- One thread: no atomics, no shared memory, no COOP or COEP headers. A type that is not
`Send` exports with no check ([not-send](/quest/m1/ffi-export/not-send.md)).
- The runtime's `fail` writes its message through an import, since wasm32 has no stderr.
- The runtime's `fail` writes its message to `console.error`.
- A struct passed by value is passed indirectly in wasm's C ABI, which the binding must
match from the target's layouts, not the host's.

Open, for the maintainer once the implementer has the facts:

- What std's parts that wasm32-unknown-unknown lacks do. The file system, sockets, and
`thread::Builder::spawn` return errors there, which is the target's Rust and needs
nothing. `Instant::now` and `SystemTime::now` panic, and moq-net reaches them:
`Timestamp::now` reads kio's clock, whose fallback is `Instant::now`, so refusing the
call would refuse the gate's own consumer. Options: build for `wasm32-wasip1` instead,
whose std has a clock, randomness, and stderr through a handful of imports the
generated binding implements (`performance.now`, `crypto.getRandomValues`), a platform
boundary AGENTS.md allows; refuse at export, with the call's span, any exported item
whose body reaches a call that can only panic, as rust2-ts already reads every body's
MIR; or moq gives kio a wasm32 clock, a source change AGENTS.md calls a gap.
Recommended: the first, if its imports stay that few and its `.wasm` no larger;
measure both targets on the fixture before choosing.
- How a stock crate's `std::time::Instant::now` becomes `performance.now`. moq-net
reaches it: `Timestamp::now` reads kio's clock, whose fallback is `Instant::now`. std
for `wasm32-unknown-unknown` has no clock and no hook for one, so the call panics
there, and getrandom and uuid refuse that target until a binary picks a backend, whose
browser one is wasm-bindgen's (`ts/README.md`). The ways to a web clock:
- compile against a std that imports its platform, `wasm32-wasip1`'s, and have the
generated binding implement each import the module declares with the web API for it.
The import names are then a detail between the generated `.wasm` and its generated
binding, not a WASI contract, and nothing of WASI ships;
- a web std of rustc's own, should one exist: the audit's survey reported a tier-3
`wasm32-component-web` target among Rust's 2026 project goals, which this plan did
not verify; check before choosing;
- the crate calls `web-time` or js-sys itself, a source change AGENTS.md calls a gap, and
one that brings wasm-bindgen back;
- a patched std built with `-Zbuild-std`, which is hand-rolling what upstream maintains.
Recommended: the first, measured for size against `wasm32-unknown-unknown` on the
fixture. Whichever is chosen, what has no web API (the file system, sockets, the
environment, a process) is refused at export, at the span of the call that reaches it,
as rust2-ts already reads every body's MIR, rather than failing when the module loads
or the call runs.
- How a crate that itself imports browser APIs through wasm-bindgen (web-sys) exports.
Recommended: refuse it at the import's span until a consumer needs both.

Public API: the TypeScript binding's shape, and `--target wasm32-unknown-unknown`.
Public API: the TypeScript binding's shape, and the name of the web target.

## Required

Expand Down
2 changes: 1 addition & 1 deletion quest/m2/wasm/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ Public API: none.

## Required

- [The exporter's wasm32 target](/quest/m2/wasm/backend.md) - the binding the new route calls through
- [The exporter's web target](/quest/m2/wasm/backend.md) - the binding the new route calls through
5 changes: 3 additions & 2 deletions quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ Decided with the maintainer on 2026-11-09, in the quest audit:
supersedes, until then, the 2026-10-27 decision that rust2 aims to support all of std,
so a generic std gap was planned here even when moq never calls it. Rejected:
continuing as before; freezing the model's work too.
- ✅ C# and Unity, a Node-API addon, and React Native are planned here before any consumer
asks, since they are the targets a media SDK's users expect: LiveKit ships Python, Node,
- ✅ C# and Unity, a Node-API addon, React Native, and a WASI target are planned here
before any consumer asks, since they are the targets a media SDK's users expect: LiveKit ships Python, Node,
Unity, and React Native over its Rust core. Each waits on
[the exporter epic](/quest/m1/ffi-export/README.md), so it copies the settled C ABI,
types, and checks. Rejected: none until a consumer asks. Not planned: Java without
Expand Down Expand Up @@ -58,6 +58,7 @@ memory model for values whose address is reinterpreted, without a consumer.
- [C# and Unity bindings](/quest/m3/csharp.md) - a C# generator over the C ABI that runs under .NET and Unity's IL2CPP
- [A Node-API addon](/quest/m3/node.md) - a native Node binding, kept only if it beats rust2-wasm and `node:ffi`
- [React Native bindings](/quest/m3/react-native.md) - a JSI module over the generated C++ header
- [A WASI target](/quest/m3/wasi.md) - the exporter's API as a WASI module or component for hosts with no JavaScript
- [Retrial Cranelift on moq's tests](/quest/m3/cranelift-unwind.md) - waits on upstream: moq's tests pass under Cranelift, so its CI can weigh it again
- [Relink without rebuilding](/quest/m3/relink.md) - measure upstream's skip of dependency-only rebuilds on moq's edits once it reaches nightly
- [Waiting: drop the lang fixture's mul_add allowance](/quest/m3/ts-fma-allowance.md) - the differential fails on any `mul_add` difference again once wasm32's `fma` rounds exactly
Expand Down
2 changes: 1 addition & 1 deletion quest/m3/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ Public API: the generated TypeScript API and the `node` feature, if it lands.
## Required

- [Exporter](/quest/m1/ffi-export/README.md) - the C ABI, its lossless types, and its checks are settled before a new generator copies them
- [The exporter's wasm32 target](/quest/m2/wasm/backend.md) - the route the addon is measured against, whose TypeScript API it should match
- [The exporter's web target](/quest/m2/wasm/backend.md) - the route the addon is measured against, whose TypeScript API it should match
2 changes: 1 addition & 1 deletion quest/m3/react-native.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ Public API: the generated module's TypeScript API.
## Required

- [Exporter](/quest/m1/ffi-export/README.md) - the C ABI, its lossless types, and its checks are settled before a new generator copies them
- [The exporter's wasm32 target](/quest/m2/wasm/backend.md) - the TypeScript API this matches, and the route it is measured against
- [The exporter's web target](/quest/m2/wasm/backend.md) - the TypeScript API this matches, and the route it is measured against
45 changes: 45 additions & 0 deletions quest/m3/wasi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# [M] A WASI target

## Goal

A report, then a target if the report earns it: [rust2-ffi](/ffi/README.md)'s export of
the `api` fixture built for WASI and called from a host with no JavaScript, with the host
supplying the clock, randomness, files, and sockets. The report runs the fixture's script
under wasmtime two ways, as a core module over the exporter's C ABI and as a component
whose WIT world the exporter generates from the `pub` API, and says which a WASI target
should be, what each costs a call, and which of the exporter's rules each can keep.

## Plan

Decided with the maintainer on 2026-11-09: ✅ web and WASI are separate targets.
[The web target](/quest/m2/wasm/backend.md) calls web APIs directly; this one is for
hosts that are WASI runtimes. Planned in m3 before a consumer asks, as
[m3](/quest/m3/README.md#plan) records for the other targets. Rejected: one
`wasm32-wasip1` module serving both.

- Who would use it: a host language that embeds a WASI runtime instead of linking native
code, which is how [the pure-Go epic](/quest/m4/go/README.md) says to weigh
[wazero](https://wazero.io) before finishing a translation, and plugin hosts that load
untrusted modules.
- A core module keeps the C ABI as it is, so each host language needs a binding over its
runtime's API, as each native language has one over C today.
- A component replaces that with the Component Model's own interface: the exporter emits
WIT, and the host's bindings come from its runtime's generator. The audit's survey
found WASI 0.3 released on 2026-06-11 with native async, a sync call costing about 3.5
times a plain one for now, and no browser engine running components, which is why the
web target does not use them. Check what WIT cannot say that the C ABI does: a borrow
checked at run time, a lent closure, a future that loses nothing on cancel, and bytes
lent without a copy.
- What a WASI host lacks is refused at export, at the span of the call that reaches it,
as on every target: threads on `wasm32-wasip1`, and sockets there too.

Public API: the name of the WASI target and what it writes, if it lands.

## Required

- [Exporter](/quest/m1/ffi-export/README.md) - the C ABI, its lossless types, and its checks are settled before a new target copies them
- [The exporter's web target](/quest/m2/wasm/backend.md) - the wasm32 layouts, lent bytes, and single-thread rules this shares

## Related

- [moq in pure Go](/quest/m4/go/README.md) - parked until a consumer needs pure Go, and to weigh a WASI runtime first when one does