diff --git a/quest/m2/wasm/README.md b/quest/m2/wasm/README.md index 88d5d86f2..456fb33e6 100644 --- a/quest/m2/wasm/README.md +++ b/quest/m2/wasm/README.md @@ -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 @@ -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 diff --git a/quest/m2/wasm/backend.md b/quest/m2/wasm/backend.md index 670ba6042..59343ad8f 100644 --- a/quest/m2/wasm/backend.md +++ b/quest/m2/wasm/backend.md @@ -1,11 +1,14 @@ -# [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 @@ -13,6 +16,11 @@ 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, @@ -28,8 +36,9 @@ 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` @@ -37,28 +46,36 @@ measurement on [the benchmark](/quest/m2/wasm/routes.md): 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 diff --git a/quest/m2/wasm/routes.md b/quest/m2/wasm/routes.md index 5d93b075f..605214215 100644 --- a/quest/m2/wasm/routes.md +++ b/quest/m2/wasm/routes.md @@ -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 diff --git a/quest/m3/README.md b/quest/m3/README.md index 98857a6fd..90ed91e88 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -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 @@ -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 diff --git a/quest/m3/node.md b/quest/m3/node.md index 772cd0101..1f97bc79f 100644 --- a/quest/m3/node.md +++ b/quest/m3/node.md @@ -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 diff --git a/quest/m3/react-native.md b/quest/m3/react-native.md index 44d5d383e..59127ef87 100644 --- a/quest/m3/react-native.md +++ b/quest/m3/react-native.md @@ -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 diff --git a/quest/m3/wasi.md b/quest/m3/wasi.md new file mode 100644 index 000000000..9eb48c017 --- /dev/null +++ b/quest/m3/wasi.md @@ -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