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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Each project lives in its own top-level directory; the [quest tree](quest/README
# Required

- An unsupported construct or malformed input is a compile error with a source span, never a warning, a silent fallback, or a runtime failure. Generated code panics only where the Rust source does.
The one exception is an `unsafe` pointer from elsewhere whose origin translation can't see, such as a parameter's: a loud run-time check may stand in for the refusal, under m3's [unsafe policy](quest/m3/README.md), since refusing every such pointer would refuse pointer arithmetic on any parameter.
- Generated output must match the source's semantics exactly: integer widths and overflow, drop order, laziness, and error paths. Prove it with differential tests against native Rust, not by inspection.
- Everything is configless, with opinionated names and defaults. Translators take stock crates as they are: no annotations, and no config beyond true platform boundaries. Each config key or source change a consumer needs is a gap to close.
- A vanity rename, if ever supported, is opt-in and lives inline in source next to the item, never in a config file.
Expand Down
2 changes: 1 addition & 1 deletion quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ memory model for values whose address is reinterpreted, without a consumer.
- [Unsafe code the translator can model](/quest/m3/ts-unsafe/README.md) - a dependency using only `unsafe` the translator can model translates from source
- [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
- [Waiting: type-check in process with tsrs](/quest/m3/ts-tsrs.md) - rust2-ts checks bindings through tsrs instead of `tsc` once it is a stable library
- [Throw only where Rust panics](/quest/m3/ts-runtime-throws.md) - no runtime `rust2-ts:` error where native Rust goes on: an exact slower path, or a refusal at translation
- [Throw only where Rust panics](/quest/m3/ts-runtime-throws.md) - no runtime `rust2-ts:` error where native Rust goes on: exact, or refused at translation
- [rust2-ts with no ts.toml](/quest/m3/ts-configless.md) - a crate's whole `pub` API translates with no config and no bound items
- [Browser APIs through wasm-bindgen](/quest/m3/ts-wasm-bindgen/README.md) - web-sys and js-sys imports become direct JS calls, so idiomatic crates such as web-rs's web-codecs translate as stock crates
- [`if let` of a temporary whose rest drops](/quest/m3/ts-if-let-drop.md) - `if let Some(E::A(x)) = temp()` with a destructor in the rest of `temp` emits TypeScript that type-checks
Expand Down
98 changes: 43 additions & 55 deletions quest/m3/ts-runtime-throws.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,55 @@
# [L] Throw only where Rust panics
# [M] Throw only where Rust panics

## Goal

Code [rust2-ts](/ts/README.md) generates fails at run time only where the Rust source
does: no `rust2-ts:` error the runtime throws where native Rust would go on. Each such
construct translates exactly, through a slower path if it must, or is refused at
translation with a span. Proven by differential tests against native Rust for every
construct that now translates.
construct translates exactly, or is refused at translation with a span. Proven by
differential tests against native Rust for every construct that now translates.

## Plan

Decided with the maintainer on 2026-11-04: "we should not panic when Rust would not
panic", and runtime throws that copy Rust's behaviour are fine.

- ✅ A slower exact path, no flag: where one exists it is the translation, with the fast
path kept where the translator can prove it applies; refuse at translation only where
none exists. Rejected: refusing with no fallback; an inline opt-in.
- ✅ A throw that guards Rust UB (a read through a null or dangling pointer, `from_raw` of
a value already dropped, freeing memory `alloc` did not give) stays, as an abort worded
like std's `unsafe precondition(s) violated` check, since Rust defines nothing there.

The rest are gaps. At `main` they include a panic its hook cannot locate
([#556](https://github.com/moq-dev/rust2/pull/556)), a pointer cast to `()` that is not an
`Arc`'s or `Rc`'s (`rs.arcErase`, [#562](https://github.com/moq-dev/rust2/pull/562),
[#565](https://github.com/moq-dev/rust2/pull/565)), `offset_from` of one Rust allocation
JS keeps in two arrays, a pointer moved past the array JS keeps it in (`AtIndex.reach`, such
as one kept in a field), `Bytes::slice_ref` of other storage, `alloc`'s bytes read as
values, ranges stepped without their type, fmt widths written as a placeholder, a read
through a substring's `str::as_ptr` outside its own bytes but inside its text's
(`s[..1].as_ptr().add(1)`, `s[1..].as_ptr().wrapping_sub(1)`), and a raw or mutable slice
of a byte in a field (a struct's, a tuple's, a closure's capture, or one an async body keeps
across an `.await`), or a cast of one to `*const [u8; N]`: a field's byte has no buffer of
its own, unlike a `u8` local whose `&raw` the body takes, so the fix is a `Uint8Array(1)`
for each `u8` field whose address is taken. A `u8` local whose address only a call takes,
as `ptr::from_mut(&mut b)` does, has none either.
The substring reads are UB under Stacked Borrows and allowed under Tree Borrows, so they
throw until Rust settles its memory model (decided 2026-11-07 in
[#634](https://github.com/moq-dev/rust2/pull/634)); if Rust rules them defined, a
substring's pointer carries its text's encoding and offset.
`git grep 'rust2-ts: ' ts/runtime` lists them; classify each before fixing. The pointer
comparisons [#584](https://github.com/moq-dev/rust2/pull/584) refuses at translation, where
JS may make two `rs.Place`s for one place, are out of scope: addresses were their route, and
addresses stay refused (decided 2026-11-07 in [the model epic](/quest/m2/ts-model/README.md)).

✅ Decided with the maintainer on 2026-11-06 in
[moq-dev/rust2#608](https://github.com/moq-dev/rust2/pull/608): comparing `*const [T]` of
values JS copies moved here from rs.Slice's quest. The options
were to wait on rs.String ([moq-dev/rust2#574](https://github.com/moq-dev/rust2/pull/574)) and run only if `&str` shares
its bytes, or to move it here with the other comparisons left refused (picked). Its only
consumer in moq-net is form_urlencoded 1.2.2's `src/lib.rs:417`,
`debug_assert!(ptr::eq(s.as_bytes(), vec))`, of the `&str` that
`String::from_utf8_lossy(&vec)` borrowed. Under
[moq-dev/rust2#574](https://github.com/moq-dev/rust2/pull/574)'s decision a `&str` stays a
JS string, so `as_bytes` makes fresh bytes and today's refusal is exact; comparing by array
and offset would answer `false` where Rust answers `true`. Look for an exact slower path.

✅ Decided with the maintainer on 2026-11-07 in
[moq-dev/rust2#608](https://github.com/moq-dev/rust2/pull/608): `ptr::eq` of an array that
`as_chunks` split from one flat slice, as std lends it to a callback, moved here. Such an
array has no JS array of its own, so `rs.arraySliceItems` gives std a copy of it, and
`ptr::eq` with the array lent elsewhere answers `false` where Rust answers `true`. Arrays
that are each a JS array of their own keep their identity. Look for an exact slower path
with the `*const [T]` comparisons above.
panic", and runtime throws that copy Rust's behaviour are fine. A throw that guards Rust
UB stays, as an abort worded like std's `unsafe precondition(s) violated` check.

Decided with the maintainer on 2026-11-07, in
[moq-dev/rust2#661](https://github.com/moq-dev/rust2/pull/661), for the throws that fire
only because translation can't see where a pointer came from:

- ✅ Refuse at translation, with a span, where the body doesn't show the pointer fits:
`rs.arcErase` (done in #661) and `alloc`'s bytes.
- ✅ Keep as loud run-time checks: `ptrUnerase`, `offset_from` across arrays,
`AtIndex.reach`, raw slices of a field's byte, and `Bytes::slice_ref` of copied bytes,
safe code with no exact answer while a `&str` stays a JS string. AGENTS.md allows the
unsafe ones under m3's [unsafe policy](/quest/m3/README.md).
- Rejected: refusing all of them at translation, which refuses pointer arithmetic on any
pointer parameter; keeping all as run-time checks.

What remains:

- `alloc`'s bytes: a byte of memory from `alloc` read or written, or the memory cast to a
type other than the one it holds, throws in `rs/alloc.ts` when the byte pointer reached
there from elsewhere. Refuse at the `alloc` or `realloc` call whose byte pointer the
body doesn't show going only to a typed cast, `dealloc`, `realloc`, or the
`NonNull`/`Option` steps [`alloc_root`](/ts/src/lower/mir/alloc.rs) already follows, so
no untyped pointer leaves the body; and a `realloc` of memory the body cast from one type
whose result it casts to another. This needs a use analysis of that pointer, which the
translator doesn't have yet. The throws in `rs/alloc.ts` are then translator guards.
- `ptr::eq` of an array `as_chunks` split from one flat slice, which std's callback gets a
copy of, answers `false` where Rust answers `true`, silently (moved here 2026-11-07 in
[#608](https://github.com/moq-dev/rust2/pull/608)). A copy that remembers the slice and
offset it came from, compared through that, is exact; it builds on the `rs.ArraySlice`
[#662](https://github.com/moq-dev/rust2/pull/662) left, where `rs.arraySliceItems` still
copies.
- `bytes`' panics in a hook program: `BYTES` in [`caller.rs`](/ts/src/lower/caller.rs)
finds each by searching the source of the program's `bytes` for text, so a
`--remap-path-prefix` of a directory inside `bytes` fails translation of a hook program.
Take each panic's location from `bytes`' compiled code for the function that panics, as
[`tokio.rs`](/ts/src/lower/tokio.rs) does for tokio's oneshot `poll`, so rustc names the
file as native does under any remap, and delete the source search (decided with the
maintainer on 2026-11-07, from [#661](https://github.com/moq-dev/rust2/pull/661)'s
follow-ups).

Public API: the translator's output, and which crates translate.

Expand Down
4 changes: 3 additions & 1 deletion quest/m3/ts-unsafe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ construct still open at `b3df54b6c`, all in m3:
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
Rust defines the access it translates exactly or is refused with a span, and only a
throw guarding UB stays, as a precondition abort.
throw guarding UB stays, as a precondition abort, or a loud check of a pointer from
elsewhere whose origin translation can't see (2026-11-07,
[#661](https://github.com/moq-dev/rust2/pull/661)).
- Done and dropped: a byte pointer past its own `Uint8Array` view. Byte pointers index the
whole buffer since [#400](https://github.com/moq-dev/rust2/pull/400) and
[#576](https://github.com/moq-dev/rust2/pull/576), and a byte `Vec` keeps its
Expand Down
1 change: 1 addition & 0 deletions ts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading