From 364c11a50148335d1c110581a835519be68afbfa Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:16:58 -0800 Subject: [PATCH 1/5] chore(quest): claim quest/m3/ts-wasm-bindgen/spike.md Quest-Claim: 54f667d07727f9fd From 4cf4f2a37e5a79fb3a06c8ab0e86781b4d7d38a8 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:22:55 -0800 Subject: [PATCH 2/5] quest: wasm-bindgen imports read the encoded program The spike's findings: translation reads each import's JS shape from the program wasm-bindgen's macro encodes in a link_section static, since source reading misses the macro_rules!-made imports on the encoder's path. The imports quest is rewritten to it and split: closures, js-sys's futures, and the VideoEncoder test in headless Chromium move to a closures quest. The epic records tokio::sync as a further gap for web-codecs. Co-Authored-By: Claude Opus 5.5 --- quest/m3/ts-wasm-bindgen/README.md | 24 ++++++--- quest/m3/ts-wasm-bindgen/closures.md | 38 +++++++++++++ quest/m3/ts-wasm-bindgen/imports.md | 79 +++++++++++++++++++++++----- quest/m3/ts-wasm-bindgen/spike.md | 37 ------------- 4 files changed, 120 insertions(+), 58 deletions(-) create mode 100644 quest/m3/ts-wasm-bindgen/closures.md delete mode 100644 quest/m3/ts-wasm-bindgen/spike.md diff --git a/quest/m3/ts-wasm-bindgen/README.md b/quest/m3/ts-wasm-bindgen/README.md index 155292db0..328d09ad1 100644 --- a/quest/m3/ts-wasm-bindgen/README.md +++ b/quest/m3/ts-wasm-bindgen/README.md @@ -28,23 +28,33 @@ Decided with the maintainer on 2026-11-06: imports now, with no consumer). At web-sys and js-sys 0.3.107 those attributes are `method`, `js_class`, `js_name`, `js_namespace`, `getter`, `setter`, `constructor`, `static_method_of`, `catch`, `extends`, `variadic`, `indexing_getter`, `indexing_setter`, - `indexing_deleter`, `typescript_type`, `is_type_of`, `final`, `structural`, and - `thread_local_v2`. + `indexing_deleter`, `typescript_type`, `is_type_of`, `final`, `structural`, + `thread_local_v2`, and `vendor_prefix`. js-sys's JSPI bridge also declares a + `suspending` import, which suspends the WASM stack on a promise. No JS call can, so it + is refused where reached, and no translated path reaches it (see + [closures](/quest/m3/ts-wasm-bindgen/closures.md)). - ✅ The wasm-bindgen crate is mapped into the runtime, as std is (rejected: translating it from source). Its real implementation is `unsafe` glue between WASM memory and a JS heap table, which means nothing when the caller is JS: `JsValue` is the JS value itself, `JsCast` checks with `instanceof` or the import's `is_type_of`, `Closure` is a JS - function, and `JsFuture` and `spawn_local` are a promise and a microtask. + function, and `JsFuture` and `spawn_local`, which js-sys now defines and + wasm-bindgen-futures re-exports, are a promise and a microtask. - ✅ web-rs's web-codecs is the first consumer, and the epic joins m3 (rejected: moq's clock first, staying in m2). moq calls no wasm-bindgen import at the [model fixture](/ts/fixtures/model/Cargo.toml)'s pin: kio's clock reads `std::time` and tokio, so `ts/moq/model.toml`'s clock binds stay with [the no-ts.toml quest](/quest/m3/ts-configless.md). Rejected: having moq give kio a wasm32 clock over js-sys first, a moq change made only for translation. -- ✅ The [spike](/quest/m3/ts-wasm-bindgen/spike.md) decides how translation reads each - import's JS shape, and its report rescopes the quests after it. +- ✅ Translation reads each import's JS shape from the program the macro encodes in a + `link_section` static, not from the `extern` blocks' source. The + [spike](https://github.com/moq-dev/rust2/pull/638) has the measurements: + `macro_rules!`-made imports, which source reading misses, are on the encoder's path. +- Open: web-codecs' `VideoEncoder` also hands its frames and errors through tokio's + `sync::mpsc` and `sync::watch`. Translation refuses those today, in their intrusive + lists and raw pointers into `UnsafeCell`s, about 50 refusals in the spike's census. So + web-codecs translating end to end needs them too, beyond the quests below. ## Required -- [Spike: how rust2-ts reads wasm-bindgen imports](/quest/m3/ts-wasm-bindgen/spike.md) - picks how translation reads each import's JS shape, and rescopes the imports quest -- [Translate wasm-bindgen imports](/quest/m3/ts-wasm-bindgen/imports.md) - web-sys, js-sys, and wasm-bindgen's runtime mapping, proven on web-codecs' encoder +- [Translate wasm-bindgen imports](/quest/m3/ts-wasm-bindgen/imports.md) - web-sys and js-sys imports read from the encoded program, with `JsValue` and imported types as JS values, proven on synchronous calls +- [wasm-bindgen closures and promises](/quest/m3/ts-wasm-bindgen/closures.md) - `Closure` and js-sys's futures mapped, proven on web-sys's `VideoEncoder` in headless Chromium diff --git a/quest/m3/ts-wasm-bindgen/closures.md b/quest/m3/ts-wasm-bindgen/closures.md new file mode 100644 index 000000000..8c3be2cbd --- /dev/null +++ b/quest/m3/ts-wasm-bindgen/closures.md @@ -0,0 +1,38 @@ +# [L] wasm-bindgen closures and promises, proven on WebCodecs' encoder + +## Goal + +wasm-bindgen's `Closure` and js-sys's futures (`JsFuture`, `spawn_local`, +`future_to_promise`, which wasm-bindgen-futures now re-exports) are mapped into the +[rust2-ts](/ts/README.md) runtime. A fixture that drives web-sys's `VideoEncoder` with +web-codecs' calls translates with no `ts.toml` binding: its configure, encode, and flush, +its output and error callbacks, and `isConfigSupported`'s promise. A differential test in +headless Chromium proves it against the same fixture's wasm-bindgen build. + +## Plan + +- `Closure` and `ScopedClosure` are a JS function over the closure it was made of, from + `Closure::wrap` of a `Box` or `Closure::new` of any closure, since + `dyn FnMut` already translates as the closure itself. It throws as wasm-bindgen's does + once the `Closure` drops, and when called again while running. `forget` and + `into_js_value` keep it alive. Its args from JS convert as an import's result does. The + census shows its glue refused today: `(usize, usize)` fat-pointer transmutes and fn + pointer casts in `convert/closures.rs`. +- `JsFuture` polls a promise's settlement as an `rs.Future` on the runtime's executor, and + `spawn_local` queues a microtask, as js-sys's `queue.rs` does. Mapping these stands in + for js-sys's JSPI bridge, which `spawn_local` checks first: with no export translated, + no JSPI context exists. `jspi_block_on_promise` is refused, as no JS blocks on a promise. +- The fixture keeps web-codecs' calls but not its tokio channels, which translation + refuses today (see the epic). It hands frames out through `Rc>`. +- Chromium decodes and encodes VP8 headless: Chrome 155 there gave a key chunk with a + `decoderConfig` and two deltas from three 64x64 frames. Only the bench shell carries + Chromium today, apart for its size. Run this test where it does: in the merge gate if + its run stays fast, or else nightly. Compare what the harness reports, such as chunk + types, timestamps, sizes, and bytes, with both builds in one browser. + +Public API: the translator's output for a crate calling wasm-bindgen closures and +futures. + +## Required + +- [Translate wasm-bindgen imports](/quest/m3/ts-wasm-bindgen/imports.md) - the shapes, the JS-represented types, and the boundary conversions this builds on diff --git a/quest/m3/ts-wasm-bindgen/imports.md b/quest/m3/ts-wasm-bindgen/imports.md index 8072f2318..d743162d3 100644 --- a/quest/m3/ts-wasm-bindgen/imports.md +++ b/quest/m3/ts-wasm-bindgen/imports.md @@ -2,24 +2,75 @@ ## Goal -[rust2-ts](/ts/README.md) translates every wasm-bindgen import web-sys and js-sys declare -into a direct JS call, and refuses any other wasm-bindgen attribute, intrinsic, or item at -translation with its span. wasm-bindgen, with wasm-bindgen-futures, is mapped into the -runtime. web-rs's web-codecs `VideoEncoder` translates with no `ts.toml` binding, proven by -a differential test against a native wasm32 build. +[rust2-ts](/ts/README.md) translates every wasm-bindgen import web-sys and js-sys declare, +and wasm-bindgen's own intrinsics, into a direct JS call. It reads each import's JS shape +from the program the `#[wasm_bindgen]` macro encodes. `JsValue` and every imported type +are the JS value itself. Any other wasm-bindgen import or item is refused where +translation reaches it, with its span. A differential test against a native wasm32 build +through the wasm-bindgen CLI proves it on synchronous js-sys and web-sys calls. Closures +and promises are [the next quest's](/quest/m3/ts-wasm-bindgen/closures.md). ## Plan -The epic's [Plan](/quest/m3/ts-wasm-bindgen/README.md#plan) holds the settled decisions: -the attribute set, the runtime mapping, and the first consumer. How translation reads each -import's JS shape is open until the [spike](/quest/m3/ts-wasm-bindgen/spike.md) reports. -Its PR rewrites this Plan, and splits the quest if the spike finds the work outgrows one PR. +The epic's [Plan](/quest/m3/ts-wasm-bindgen/README.md#plan) holds the settled decisions. +The [spike](https://github.com/moq-dev/rust2/pull/638) picked the mechanism and measured +what follows. + +- Shapes come from the encoded program (rejected: re-reading the `extern` blocks in the + dependency's source). Each `#[wasm_bindgen]` invocation on `wasm32-unknown-unknown` + emits a `#[link_section = "__wasm_bindgen_unstable"]` static, whose initializer every + crate's metadata keeps, as rustc keeps every static's. A wrapper such as + `VideoEncoder::encode_queue_size` calls one `extern "C"` shim in + `wasm_import_module = "__wbindgen_placeholder__"`. Its link name is the `shim` of an + `ImportFunction`, which gives the class, JS name, kind, namespace, `catch`, `variadic`, + and `structural`. Find a crate's statics by their `link_section` through + `CStore::num_def_ids_untracked`, since they sit in anonymous `const _` blocks. Read + them once per crate. Source reading would miss every import a `macro_rules!` makes: + js-sys's `arrays!` makes 308, for its 11 typed arrays, and the encoder path reaches + `Uint8Array`'s. It would also re-implement the macro's ~400 lines of naming rules. +- The decoding is positional and changes with `SCHEMA_VERSION`. That changed 8 times in + the 12 months to 0.2.130, each time adding or removing fields or variants. Decode with + one field list, wasm-bindgen-shared's `shared_api!`, with each field and variant gated + by the schemas that carry it. Support every schema from 0.2.128 on, moq's pin today. + Refuse a program of another schema at its crate, naming the supported range. + wasm-bindgen-shared can be depended on, but only at one version, since cargo unifies + 0.2.x. cli-support's decoder is private, so vendor its 181 lines (MIT or Apache-2.0) + with their source and revision. A new schema adds its gates. +- JS-represented types: `JsValue`, each `ImportKind::Type`, and their `ErasableGeneric` + instances, such as js-sys's `Promise`, are the JS value. The generated glue between + them is then the identity: the `obj` field, `transmute_copy`, and the + `&*(val as *const JsValue as *const Self)` cast, all refused today. `JsCast` translates + from MIR, over each type's `instanceof` shim, `x instanceof Class`, and its + `is_type_of`. Clone and drop are the identity and nothing, which replaces the heap-table + intrinsics (`__wbindgen_object_clone_ref`, `__wbindgen_object_drop_ref`). +- A wrapper is replaced whole, never translated from its ABI glue. Its arguments and + result convert as the wasm-bindgen CLI's JS glue converts them for each Rust type, and + `wbg_cast::` converts the same way. That covers numbers, 64-bit and 128-bit + integers, `bool`, `char`, strings, `Option`, slices and `Vec`s (a `&mut [u8]` copies + back), string enums by their `variant_values`, and a `Result` under `catch`. A + non-`structural` (`final`) method calls `Class.prototype.name.call(this, ...)`, and + `vendor_prefixes` try each prefixed global. +- Intrinsics: in wasm-bindgen 0.2.130, 54 `__wbindgen_*` intrinsics are ordinary imports + in the program (`raw_module = "__wbindgen_placeholder__"`). Give each one translation + reaches the JS body the CLI's intrinsic table gives it. Treat that module as + wasm-bindgen's own, not custom JS. +- Refusals are per reached import or item, at its span, never per program, since js-sys's + program holds its JSPI bridge (`suspending`, `raw_module`) beside everything else: a + `module`, `raw_module`, or `inline_js` import other than the placeholder, a + `suspending` import, a `generic_per_mono` import, and any export. +- Cargo config: none needed. rust2-ts runs cargo from the package's directory, so a + `.cargo/config.toml`'s `build.rustflags`, such as web-rs's + `--cfg=web_sys_unstable_apis`, apply beside the profile's. The spike's census compiled + web-sys's `VideoEncoder` that way. +- Differential test: a fixture whose harness calls js-sys (`Math`, `JSON`, `Reflect`, + `Array`, typed arrays) and web-sys's non-DOM globals (`TextEncoder`, `console`). Its + native side is the wasm32 build through `wasm-bindgen-cli_0_2_129`, which the flake's + nixpkgs has, with the fixture's lockfile pinned to 0.2.129. Its host is Bun, as + `tests/differential.ts` runs today. It covers each import kind, `catch`, the boundary + conversions' edge values (an `f64` past `u32`, `NaN`, lone surrogates), `JsCast` + failures, and refusals of each other module kind. Docs: [well-known crates](/ts/README.md#well-known-crates) gains wasm-bindgen's mapping -and the import attributes translation supports. +and the supported schemas. Public API: the translator's output for a crate calling wasm-bindgen imports. - -## Required - -- [Spike: how rust2-ts reads wasm-bindgen imports](/quest/m3/ts-wasm-bindgen/spike.md) - picks the mechanism and rescopes this quest diff --git a/quest/m3/ts-wasm-bindgen/spike.md b/quest/m3/ts-wasm-bindgen/spike.md deleted file mode 100644 index 287876e81..000000000 --- a/quest/m3/ts-wasm-bindgen/spike.md +++ /dev/null @@ -1,37 +0,0 @@ -# [M] Spike: how rust2-ts reads wasm-bindgen imports - -## Goal - -A report, in this quest's PR, that sizes translating web-rs's web-codecs `VideoEncoder` -through web-sys, js-sys, and wasm-bindgen, and rewrites -[the imports quest](/quest/m3/ts-wasm-bindgen/imports.md) to the mechanism it picks. No -translator change lands. - -## Plan - -After macro expansion the `#[wasm_bindgen]` attributes are gone. Decided with the -maintainer on 2026-11-06: the spike compares both sources of each import's JS shape (its -class, `js_name`, kind, `catch`) and picks one (rejected: choosing one now). - -- wasm-bindgen's encoded program, the `link_section` static the wasm-bindgen CLI reads back - out of the binary. It is exact, but its `SCHEMA_VERSION` changes with every release - (0.2.130 now): measure what decoding costs across the versions web-codecs' lockfile and - its dependents' pin, and whether `wasm-bindgen-shared` or the CLI's decoder is usable as - a library. -- The `extern` blocks in the dependency's source, read again with their attributes. They - are stable across versions, but miss imports a `macro_rules!` generates: count those in - web-sys and js-sys. - -It also answers, for the imports quest: - -- How a mapped `JsValue`, `JsCast`, and `Closure` meet the generic code around them, such - as `Closure::new` of a closure, which web-codecs' output and error callbacks need, given - that trait objects, `TypeId` included, already translate. -- How the differential test's native side runs a wasm-bindgen build: the wasm-bindgen CLI - in the dev shell at the fixture's pinned version, and a host with WebCodecs, which Bun - lacks, so likely a headless browser. -- Whether translation builds with the consumer's own cargo config: web-sys's - `VideoEncoder` is behind `--cfg=web_sys_unstable_apis`, which web-rs sets in its - `.cargo/config.toml` rustflags. - -Public API: none. From 388b6d4a492660e7eb260097058c685948aec68b Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 06:16:30 -0800 Subject: [PATCH 3/5] quest: bound wasm-bindgen schemas, test each, keep Fn closures reentrant Co-Authored-By: Claude Opus 5.5 --- quest/m3/ts-wasm-bindgen/closures.md | 4 +++- quest/m3/ts-wasm-bindgen/imports.md | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/quest/m3/ts-wasm-bindgen/closures.md b/quest/m3/ts-wasm-bindgen/closures.md index 8c3be2cbd..f45a9d647 100644 --- a/quest/m3/ts-wasm-bindgen/closures.md +++ b/quest/m3/ts-wasm-bindgen/closures.md @@ -14,7 +14,9 @@ headless Chromium proves it against the same fixture's wasm-bindgen build. - `Closure` and `ScopedClosure` are a JS function over the closure it was made of, from `Closure::wrap` of a `Box` or `Closure::new` of any closure, since `dyn FnMut` already translates as the closure itself. It throws as wasm-bindgen's does - once the `Closure` drops, and when called again while running. `forget` and + once the `Closure` drops and, for a `dyn FnMut` one (`WasmClosure::IS_MUT`), when + called again while running; a `dyn Fn` one stays reentrant, as the CLI's `makeClosure` + keeps it. `forget` and `into_js_value` keep it alive. Its args from JS convert as an import's result does. The census shows its glue refused today: `(usize, usize)` fat-pointer transmutes and fn pointer casts in `convert/closures.rs`. diff --git a/quest/m3/ts-wasm-bindgen/imports.md b/quest/m3/ts-wasm-bindgen/imports.md index d743162d3..a15b70200 100644 --- a/quest/m3/ts-wasm-bindgen/imports.md +++ b/quest/m3/ts-wasm-bindgen/imports.md @@ -31,11 +31,12 @@ what follows. - The decoding is positional and changes with `SCHEMA_VERSION`. That changed 8 times in the 12 months to 0.2.130, each time adding or removing fields or variants. Decode with one field list, wasm-bindgen-shared's `shared_api!`, with each field and variant gated - by the schemas that carry it. Support every schema from 0.2.128 on, moq's pin today. - Refuse a program of another schema at its crate, naming the supported range. + by the schemas that carry it. Support each schema from moq's pin (0.2.129, schema + `0.2.128`) through the newest when this lands, `0.2.130` at the spike. Refuse a program + of any other schema, older or newer, at its crate, naming the supported ones. wasm-bindgen-shared can be depended on, but only at one version, since cargo unifies 0.2.x. cli-support's decoder is private, so vendor its 181 lines (MIT or Apache-2.0) - with their source and revision. A new schema adds its gates. + with their source and revision. A new schema is supported once its gates are added. - JS-represented types: `JsValue`, each `ImportKind::Type`, and their `ErasableGeneric` instances, such as js-sys's `Promise`, are the JS value. The generated glue between them is then the identity: the `obj` field, `transmute_copy`, and the @@ -68,7 +69,10 @@ what follows. nixpkgs has, with the fixture's lockfile pinned to 0.2.129. Its host is Bun, as `tests/differential.ts` runs today. It covers each import kind, `catch`, the boundary conversions' edge values (an `f64` past `u32`, `NaN`, lone surrogates), `JsCast` - failures, and refusals of each other module kind. + failures, and refusals of each other module kind. The translated side runs once per + supported schema, each from its own lockfile, against that one native run, since the + harness's output does not depend on wasm-bindgen's version; that tests every schema's + gates without a CLI per schema. A unit test refuses a program of an unsupported schema. Docs: [well-known crates](/ts/README.md#well-known-crates) gains wasm-bindgen's mapping and the supported schemas. From a1d4c2bc69ea4876923d50146d094e92ff948990 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:01:25 -0800 Subject: [PATCH 4/5] quest: plan tokio's sync channels in rust2-ts; record that unsafe may stay refused Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 7 ++++++ quest/m3/ts-wasm-bindgen/README.md | 11 ++++++--- quest/m3/ts-wasm-bindgen/tokio-sync.md | 34 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 quest/m3/ts-wasm-bindgen/tokio-sync.md diff --git a/quest/m3/README.md b/quest/m3/README.md index 73adb9533..a92f9ed49 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -11,6 +11,13 @@ std the translators cover before a consumer calls them. Decided with the maintainer on 2026-10-27: rust2 aims to support all of std, so a generic std gap is planned here even when moq never calls it. +Decided with the maintainer on 2026-11-07: ✅ little time goes to unsafe Rust. A +translator may refuse unsafe code with its span, requiring safe Rust, and a common +library's unsafe internals may be replaced by a runtime mapping over its public API, as +`hashbrown`'s and tokio's channels are (rejected: planning each unsafe std gap that no +consumer reaches, such as `MaybeUninit::zeroed`, `Rc::new_uninit`, and +`ptr::write_bytes`). + ## Required - [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 diff --git a/quest/m3/ts-wasm-bindgen/README.md b/quest/m3/ts-wasm-bindgen/README.md index 328d09ad1..dfcbc30e5 100644 --- a/quest/m3/ts-wasm-bindgen/README.md +++ b/quest/m3/ts-wasm-bindgen/README.md @@ -49,12 +49,15 @@ Decided with the maintainer on 2026-11-06: `link_section` static, not from the `extern` blocks' source. The [spike](https://github.com/moq-dev/rust2/pull/638) has the measurements: `macro_rules!`-made imports, which source reading misses, are on the encoder's path. -- Open: web-codecs' `VideoEncoder` also hands its frames and errors through tokio's - `sync::mpsc` and `sync::watch`. Translation refuses those today, in their intrusive - lists and raw pointers into `UnsafeCell`s, about 50 refusals in the spike's census. So - web-codecs translating end to end needs them too, beyond the quests below. +- ✅ web-codecs' encoders and decoders also hand their frames and errors through tokio's + `sync::mpsc` and `sync::watch`, which translation refuses today (about 50 refusals in + the spike's census), so + [tokio's sync channels are mapped into the runtime](/quest/m3/ts-wasm-bindgen/tokio-sync.md) + (rejected: translating tokio's source, whose intrusive lists and raw pointers into + `UnsafeCell`s are unsafe code a JS caller has no use for). ## Required - [Translate wasm-bindgen imports](/quest/m3/ts-wasm-bindgen/imports.md) - web-sys and js-sys imports read from the encoded program, with `JsValue` and imported types as JS values, proven on synchronous calls - [wasm-bindgen closures and promises](/quest/m3/ts-wasm-bindgen/closures.md) - `Closure` and js-sys's futures mapped, proven on web-sys's `VideoEncoder` in headless Chromium +- [tokio's sync channels mapped into the runtime](/quest/m3/ts-wasm-bindgen/tokio-sync.md) - `mpsc`, `watch`, `oneshot`, and `select!`'s support, which web-codecs passes frames and errors through diff --git a/quest/m3/ts-wasm-bindgen/tokio-sync.md b/quest/m3/ts-wasm-bindgen/tokio-sync.md new file mode 100644 index 000000000..7f2915375 --- /dev/null +++ b/quest/m3/ts-wasm-bindgen/tokio-sync.md @@ -0,0 +1,34 @@ +# [M] tokio's sync channels mapped into the runtime + +## Goal + +tokio's `sync::mpsc` (bounded and unbounded), `sync::watch`, and `sync::oneshot`, and +`tokio::select!`'s support, are mapped into the [rust2-ts](/ts/README.md) runtime, as +`hashbrown` and `bytes` are, so a crate passing values through them translates with no +`ts.toml` binding. Differential tests against native Rust on tokio's current-thread +runtime prove it: values, ordering, closed and lagged errors, and when each future +becomes ready. + +## Plan + +Suggested on [#638](https://github.com/moq-dev/rust2/pull/638): web-rs's web-codecs hands +its encoders' and decoders' frames out through `mpsc::unbounded_channel`, and their errors +through `watch::channel`, read in a `tokio::select!` with `biased;`. Its spike's census +counts about 50 refusals in tokio's `sync`. + +Decided with the maintainer on 2026-11-07: ✅ map the channels into the runtime (rejected: +translating tokio's source, whose intrusive waiter lists, raw pointers into `UnsafeCell`s, +atomics, and `process::abort` mean nothing in single-threaded JS; and mapping only +web-codecs' methods, which the next consumer would reopen). Unsafe Rust may stay refused +where a wrapper over a common library does the job. + +- Each channel's public API maps, waiters kept in a queue the runtime wakes, as + `rs.Future`s on its executor. Methods that block a thread (`blocking_recv`, + `blocking_send`) are refused, as no JS thread blocks. +- `select!` expands to code that already translates, apart from its support module: + `thread_rng_n` picks the first branch polled, uniform as tokio's is, so a `select!` + without `biased;` maps too. +- `Notify`, `Mutex`, `RwLock`, and `Semaphore` stay refused where reached, until a + consumer needs them. + +Public API: the translator's output for a crate using tokio's sync channels. From b7f23c1a9e14973b61170b06927b1e7a495ef652 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:19:33 -0800 Subject: [PATCH 5/5] quest: adopt m3's unsafe policy; delete Vec::from_raw_parts Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 25 ++++++++++++++++++------ quest/m3/ts-unsafe/README.md | 21 +++++++++++--------- quest/m3/ts-unsafe/vec-from-raw-parts.md | 18 ----------------- 3 files changed, 31 insertions(+), 33 deletions(-) delete mode 100644 quest/m3/ts-unsafe/vec-from-raw-parts.md diff --git a/quest/m3/README.md b/quest/m3/README.md index a92f9ed49..7a1f39a4d 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -11,12 +11,25 @@ std the translators cover before a consumer calls them. Decided with the maintainer on 2026-10-27: rust2 aims to support all of std, so a generic std gap is planned here even when moq never calls it. -Decided with the maintainer on 2026-11-07: ✅ little time goes to unsafe Rust. A -translator may refuse unsafe code with its span, requiring safe Rust, and a common -library's unsafe internals may be replaced by a runtime mapping over its public API, as -`hashbrown`'s and tokio's channels are (rejected: planning each unsafe std gap that no -consumer reaches, such as `MaybeUninit::zeroed`, `Rc::new_uninit`, and -`ptr::write_bytes`). +Decided with the maintainer on 2026-11-07, the unsafe policy, to stop one-off fixes for +ever rarer `unsafe`: + +1. ✅ Safe Rust is the contract, and where translation effort goes. +2. ✅ `unsafe` inside std and common crates is wrapped at their safe API, as std, + `hashbrown`, `bytes`, and [tokio's channels](/quest/m3/ts-wasm-bindgen/tokio-sync.md) + are, rather than translated. +3. ✅ Other `unsafe` translates only where it fits the translator's existing pointer forms + (a place, an array element, a byte in a buffer). A construct needing a new pointer form + or pointer metadata is refused with its span, unless a consumer needs it and no library + wrapper covers it. +4. ✅ An `unsafe`-heavy crate a consumer needs is compiled to WASM behind its public API + rather than fixed construct by construct. Not planned until a consumer needs it. + +Rejected: an unsafe std gap with no consumer becoming its own quest, as +`MaybeUninit::zeroed`, `Rc::new_uninit`, and `ptr::write_bytes` would have +([#647](https://github.com/moq-dev/rust2/pull/647)), or as `Vec::from_raw_parts`'s adopted +storage did ([#635](https://github.com/moq-dev/rust2/pull/635)); and a second, byte-backed +memory model for values whose address is reinterpreted, without a consumer. ## Required diff --git a/quest/m3/ts-unsafe/README.md b/quest/m3/ts-unsafe/README.md index e54d29d86..51d81a0b5 100644 --- a/quest/m3/ts-unsafe/README.md +++ b/quest/m3/ts-unsafe/README.md @@ -2,9 +2,9 @@ ## Goal -[rust2-ts](/ts/README.md) translates every `unsafe` construct it can give exact semantics -in JS, each proven by differential tests against native Rust, so a stock crate's `unsafe` -never needs a hand binding or a rewrite in its consumer. +[rust2-ts](/ts/README.md) translates the `unsafe` constructs that fit its pointer forms +with exact semantics in JS, each proven by differential tests against native Rust, and +refuses the rest with a span, as [m3's unsafe policy](/quest/m3/README.md) decides. ## Plan @@ -37,8 +37,10 @@ none of its own `unsafe`. A union over a type parameter lays out each instance s - Decided with the maintainer on 2026-11-01, typed allocation: memory from `alloc` for a `Layout::array::(n)`, or an equal layout of `T`'s size and alignment, cast to `*mut T` is a fresh JS array of `T`s, which `realloc` copies and `dealloc` frees. Untyped and - byte allocation stay refused. smallvec spills to it, and - [`Vec::from_raw_parts` of it](/quest/m3/ts-unsafe/vec-from-raw-parts.md) is left. + byte allocation stay refused. smallvec spills to it. `Vec::from_raw_parts` of it stays + refused, deleted on 2026-11-07 under m3's unsafe policy: a `Vec` adopting that array + needs storage tracked apart from the pointer + ([#635](https://github.com/moq-dev/rust2/pull/635)). - Decided with the maintainer on 2026-11-02: a raw slice pointer is `rs.RawSlice`, its first element's pointer and its length, read through only where Rust reads, and an empty slice from a pointer into no array keeps that pointer through every part of it. @@ -46,14 +48,16 @@ none of its own `unsafe`. A union over a type parameter lays out each instance s Decided with the maintainer on 2026-11-06, splitting this quest into one child per construct still open at `b3df54b6c`, all in m3: -- ✅ Goal: every `unsafe` construct rust2-ts can make exact, one quest each. - Rejected: only the constructs moq's dependencies hit. +- Goal: every `unsafe` construct rust2-ts can make exact, one quest each. Rejected: only + the constructs moq's dependencies hit. Narrowed on 2026-11-07 by m3's unsafe policy to + the constructs that fit the existing pointer forms. - ✅ All children in m3. Rejected: spare capacity in m2; smallvec's constructs in m2. No child blocks moq: the model's only hits are smallvec's (7 of its 10 refusals), and moq drops or binds smallvec. - `Vec::from_raw_parts` of a typed allocation is its own child, restored on review of [#618](https://github.com/moq-dev/rust2/pull/618): the spare-capacity child fills an - existing `Vec`, not one adopting an allocation. + existing `Vec`, not one adopting an allocation. Deleted on 2026-11-07 under m3's unsafe + policy, above. - The 2026-11-01 clauses that let a pointer from elsewhere, a `Vec`'s spare capacity, and a byte pointer past its view fail loudly at run time are superseded by [Throw only where Rust panics](/quest/m3/ts-runtime-throws.md) (2026-11-04, #597): where @@ -73,7 +77,6 @@ Public API: the translator's output. ## Required - [A raw pointer behind a reference](/quest/m3/ts-unsafe/raw-behind-ref.md) - `&raw const *r` of a value JS copies points where the reference came from -- [Vec::from_raw_parts of a typed allocation](/quest/m3/ts-unsafe/vec-from-raw-parts.md) - a `Vec` adopts an `alloc`ed array of `T`s, with its length, capacity, and drops - [Vec::drain from std's MIR](/quest/m3/ts-unsafe/vec-drain-mir.md) - std's `Drain` translates, replacing the runtime's port - [BinaryHeap from std's MIR](/quest/m3/ts-unsafe/binary-heap-mir.md) - std's `BinaryHeap` and `PeekMut` translate, replacing the runtime's port - [Unchecked integer operations](/quest/m3/ts-unsafe/unchecked-ops.md) - `unchecked_*` and `NonZero::new_unchecked` translate, aborting on a violated precondition diff --git a/quest/m3/ts-unsafe/vec-from-raw-parts.md b/quest/m3/ts-unsafe/vec-from-raw-parts.md deleted file mode 100644 index 7e77b7ed5..000000000 --- a/quest/m3/ts-unsafe/vec-from-raw-parts.md +++ /dev/null @@ -1,18 +0,0 @@ -# [S] Vec::from_raw_parts of a typed allocation - -## Goal - -[rust2-ts](/ts/README.md) translates `Vec::from_raw_parts` of memory `alloc` gave for a -`Layout::array::(n)` (or an equal layout), so the `Vec` adopts that JS array of `T`s -with its length and capacity and drops its elements and frees it as std does, proven by -differential tests against native Rust that check length, capacity, and drops. Today it -is an unsupported call. - -## Plan - -Typed allocation is a fresh JS array of `T`s since the 2026-11-01 decision in -[the epic](/quest/m3/ts-unsafe/README.md), and `rs.Vec` holds std's capacity since -[#555](https://github.com/moq-dev/rust2/pull/555). smallvec spills its inline buffer this -way: 2 of its 10 model refusals at the moq pin, which moq drops or binds. - -Public API: the translator's output.