Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d1695bf
chore(quest): claim quest/m3/ts-slice-object.md
kixelated Nov 6, 2026
4fd1b02
wip(ts): slice runs, checkpoint of the interrupted iterate session
kixelated Nov 7, 2026
fbc9480
fix(ts): restore cleaning the typecheck output
kixelated Nov 7, 2026
f602f75
Merge origin/main into quest/m3/ts-slice-object
kixelated Nov 7, 2026
5f9230f
style(ts): format
kixelated Nov 7, 2026
2901fb9
fix(ts): runs read their remaining arrays, copy objects a write lands…
kixelated Nov 7, 2026
db76517
fix(ts): runs in every form a slice of arrays takes, copied out by th…
kixelated Nov 7, 2026
17b5ba7
Merge origin/main into quest/m3/ts-slice-object
kixelated Nov 7, 2026
2c7ae6f
fix(ts): runs from an Option's mutable slice, a borrowed Vec's Cow, i…
kixelated Nov 7, 2026
08ba592
Merge remote-tracking branch 'origin/main' into pr-608
kixelated Nov 7, 2026
2c2f23c
fix(ts): lend rows of item-backed runs and join's array separator as …
kixelated Nov 7, 2026
c8d71b2
test(ts): a lent row's ptr::eq and join's separator Clone, against na…
kixelated Nov 7, 2026
b2a9375
refactor(ts)!: rename rs.Runs to rs.ArraySlice, with its helpers
kixelated Nov 7, 2026
06e0753
Merge origin/main into quest/m3/ts-slice-object
kixelated Nov 7, 2026
d028e1b
fix(ts): array slices from a deque, a heap, and raw pointers to slice…
kixelated Nov 7, 2026
57e8ff3
Merge origin/main into quest/m3/ts-slice-object
kixelated Nov 7, 2026
1a70032
fix(ts): array slices from a rest pattern, a MaybeUninit reborrow, an…
kixelated Nov 7, 2026
a4ec4fa
Merge origin/main into quest/m3/ts-slice-object
kixelated Nov 7, 2026
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
3 changes: 2 additions & 1 deletion quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ generic std gap is planned here even when moq never calls it.
- [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
- [rs.Slice](/quest/m3/ts-slice-object.md) - references to arrays of other values are a runtime slice too, and pointers compare by where they point
- [Parts of arrays without arrayView](/quest/m3/ts-slice-object.md) - a `&mut [T; N]` to part of an array is taken where it is, with no `Proxy`
- [Writes through iter_mut without a Place per item](/quest/m3/ts-iter-mut-places.md) - a loop over `iter_mut()` of copy values writes the slice's array by index
- [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
- [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
- [std's panic messages hold std's capacity](/quest/m3/ts-panic-payload-capacity.md) - a hook reads native's `String` capacity from std's own formatted panics
- [A slice of one byte's place](/quest/m3/ts-byte-slice-place.md) - `slice::from_mut` of a `&mut u8` writes the byte where the caller holds it
- [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
- [BufMut on a byte slice](/quest/m3/ts-bufmut-slices.md) - `bytes`' `BufMut` for `&mut [u8]` and `&mut [MaybeUninit<u8>]` translates
- [Fields typed by an associated type](/quest/m3/ts-assoc-type-fields.md) - a generic struct's field of `<T as Trait>::Item` translates at each instance
24 changes: 24 additions & 0 deletions quest/m3/ts-byte-slice-place.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [S] A slice of one byte's place

## Goal

[rust2-ts](/ts/README.md) translates `slice::from_mut` and `array::from_mut` of a
`&mut u8`, of a local, a field, or an element of an array of bytes alike, which it refuses
today: the slice writes the byte where the caller holds it. Proven by differential tests
against native Rust.

## Plan

✅ Decided with the maintainer on 2026-11-06 in
[moq-dev/rust2#608](https://github.com/moq-dev/rust2/pull/608): keep it, deferred to this
quest of its own, as rust2 aims to support all of std. The options were to drop it until
a consumer appears, since moq has none at its pin, or to keep it and measure the
`rs.Place`-backed `rs.ByteSlice` below (picked).

A slice of one over a `&mut u8` was found fragile when it meant a one-byte buffer for each
local; an `rs.ByteSlice` whose storage can be an `rs.Place` may avoid that. Measure what
each form costs a hot `&mut u8`, such as `read_exact(slice::from_mut(&mut byte))`, against
the `rs.ByteSlice` over a `Uint8Array` every other byte slice is.

Public API: the translator's output, and what a hand-written caller may receive as a
`rs.ByteSlice`.
2 changes: 1 addition & 1 deletion quest/m3/ts-iter-mut-places.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ iterator can yield the index instead.

## Related

- [rs.Slice](/quest/m3/ts-slice-object.md) - the slice these loops index
- [Parts of arrays without arrayView](/quest/m3/ts-slice-object.md) - the slices these loops index
20 changes: 20 additions & 0 deletions quest/m3/ts-runtime-throws.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ comparisons [#584](https://github.com/moq-dev/rust2/pull/584) refuses at transla
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](/quest/m3/ts-slice-object.md). 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.

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

## Related
Expand Down
71 changes: 42 additions & 29 deletions quest/m3/ts-slice-object.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,55 @@
# [M] rs.Slice
# [M] Parts of arrays without arrayView

## Goal

[rust2-ts](/ts/README.md) translates slices and references to arrays as `rs.Slice` and
`rs.ByteSlice`, their array, start, and length. What remains:

- A `&mut [[T; N]]` that `as_chunks_mut` and `as_rchunks_mut` lend holds its runs where
the slice holds their elements, rather than as the runtime's `Runs` and `ByteRuns`
`Proxy` views and `arrayView`, along with the detaching copies they need before a
whole write.
- `slice::from_mut` and `array::from_mut` of a `&mut u8` translate, writing the byte
through the slice where the caller holds it.
- Pointers to a `[T]` of values JS copies compare where they point, as pointers to a
`[T; N]` do.

Each is proven by differential tests against native Rust.
[rust2-ts](/ts/README.md) holds a slice of arrays as an `rs.ArraySlice` of the slice each array
is, with no `Proxy`. What remains is the runtime's `arrayView`, a `Proxy` JS array over
part of another, which `rs.sliceView` makes where a `&mut [T; N]` to part of an array is
taken whole as a JS array: by `mem::replace` and `mem::swap` of an element of a slice of
arrays, `Cell::from_mut` of one, std's mappings given one to write through, and a raw
pointer's `as_mut` or `Box::from_raw`. Each takes the slice where it is instead, or a
copy written back as `rs.arraySliceBy` writes arrays back, and `arrayView` and its `View` traps
go. Proven by differential tests against native Rust.

## Plan

✅ Decided with the maintainer on 2026-11-06 in
[moq-dev/rust2#608](https://github.com/moq-dev/rust2/pull/608), which made `rs.ArraySlice`:

1. How a `&mut [[T; N]]` holds its arrays:
- ✅ (a) A runtime type for every slice of arrays, nested or flat, whose element `i` is
the slice of array `i`, which replaces the `Runs` and `ByteRuns` `Proxy` views and
`arrayView`.
- (b) Bytes only: `as_chunks_mut` makes eager `subarray` views, keeping the `Proxy`
path for other elements.
- (c) Keep the `Proxy` views and drop the item.
2. `slice::from_mut` of a `&mut u8`: kept, deferred to
[a quest of its own](/quest/m3/ts-byte-slice-place.md).
3. Comparing `*const [T]` of values JS copies: moved to
[Throw only where Rust panics](/quest/m3/ts-runtime-throws.md).

✅ Decided with the maintainer on 2026-11-07 at the round-3 design checkpoint of
[moq-dev/rust2#608](https://github.com/moq-dev/rust2/pull/608):

4. Conversions that copied arrays where Rust borrows them: ✅ continue, with local fixes
(rejected: merge as is; `rs.ArraySlice` only for what `as_chunks` splits, keeping
`rs.Slice<T[]>` for other slices of arrays; drop). Where each array of an array slice is
a JS array of its own, `rs.arraySliceItems` slices the list of them rather than copying
each, so a std callback's `ptr::eq` sees the array it was lent, and `join` reads its
array separator without copying it, since it clones it anyway. The arrays `as_chunks`
splits from one flat slice have none of their own, so a callback still gets a copy:
that `ptr::eq` gap moved to [Throw only where Rust panics](/quest/m3/ts-runtime-throws.md).
5. The type's name: ✅ `rs.ArraySlice`, as `rs.ByteSlice` and `rs.RawSlice` are named,
with its helpers `rs.arraySlice*` (rejected: `rs.Slices`, `rs.Chunks`, keep `rs.Runs`).

Decided with the maintainer on 2026-11-03: Rust types the runtime can't hold as std's do
get a runtime wrapper, converted at API boundaries, rather than a JS native type
([#555](https://github.com/moq-dev/rust2/pull/555) made `Vec` one).

A slice of arrays holds each array as a JS array of its own, so a run of a flat slice has
no array to be. One option is a slice of runs over the flat array, whose element `i` is
the `rs.Slice` of run `i`, which a `&mut [[T; N]]` would be wherever its runs alias
another slice's elements.

A slice of one over a `&mut u8` was found fragile when it meant a one-byte buffer for each
local; an `rs.ByteSlice` whose storage can be an `rs.Place` may avoid that. Measure what
each form costs a hot `&mut u8`, such as `read_exact(slice::from_mut(&mut byte))`.

✅ Decided with the maintainer on 2026-11-04: generated code fails at run time only where
Rust panics, so a pointer comparison the translator can't answer exactly is refused at
translation with a span. Pointers to arrays compare as slices; comparing pointers to
elements and other places of values JS copies, which JS may make two `rs.Place`s for,
stays refused, as do pointers to an array of one, and their exact path is
[a quest of its own](/quest/m3/ts-runtime-throws.md).
`rs.replaceIn` and `rs.cellIn` of a JS array write a whole value into it, and copy its
elements to a depth where a write lands in them; a slice's form needs the same, as
`rs.arraySliceReplace` and `rs.cellOfArraySlice` do for arrays. A raw pointer to an array of arrays
that std's mapping gives back as a reference is refused today.

Public API: the translator's output and what a hand-written caller passes for a slice of
arrays.
28 changes: 17 additions & 11 deletions ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ crate translation reaches is refused, and so are two versions of one crate it re
| `Option<T>` | `T \| undefined`; `T \| rs.None` where `T` can already be `undefined`, as `()` and `Option` can, so `Option<()>` and `Option<Option<u64>>` keep `None` apart from each `Some`; a generic type's `Option<T>` is an `rs.OptionOf<T>`, either as each instance's `T` decides, as a generic `Poll<T>` is an `rs.PollOf<T>` |
| `Result<T, E>` | `rs.Result<T, E>`: `{ ok: true, value }` or `{ ok: false, error }`; `rs.OK` or `rs.ERR` where the payload is `undefined`, as in `Ok(())` |
| `ControlFlow<B, C>` | `rs.Result<C, B>`, `Ok` to continue; what `?` branches on is the value it was applied to, but for a `Poll`'s `Pending` and `Ready(None)`, which continue as `Ok` of themselves |
| tuple, `[T; N]`, `&[T; N]` | an array, and `rs.Slice<T>` of all of it for `&[T; N]`; for bytes, `u8` or `MaybeUninit<u8>`, `Uint8Array` for `[u8; N]`, and `rs.ByteSlice` for `&[u8; N]` |
| `&[T]`, `&mut [T]`, `Box<[T]>` | `rs.Slice<T>`, a range of an array; `rs.ByteSlice` for bytes |
| tuple, `[T; N]`, `&[T; N]` | an array, and `rs.Slice<T>` of all of it for `&[T; N]`, `rs.ArraySlice` for an array of arrays; for bytes, `u8` or `MaybeUninit<u8>`, `Uint8Array` for `[u8; N]`, and `rs.ByteSlice` for `&[u8; N]` |
| `&[T]`, `&mut [T]`, `Box<[T]>` | `rs.Slice<T>`, a range of an array; `rs.ByteSlice` for bytes; `rs.ArraySlice` of the slice each array is for a slice of arrays |
| `Vec<T>` | `rs.Vec<T>`: its items, the array `items`, and the capacity std's holds; `rs.vecFrom(items)` makes one of an array with room for exactly them. `rs.ByteVec` of bytes |
| `Cow<[T]>`, `Cow<str>` | `rs.Cow<T>`: the `Vec` it owns, or an `rs.Borrowed` holding the slice it borrows; `rs.ByteCow` for bytes, an `rs.ByteVec` or an `rs.ByteSlice`; `rs.StrCow` for text, the `rs.String` it owns or an `rs.Borrowed` holding the string it borrows, whose text `rs.strCowStr` reads |
| `Cow<[T]>`, `Cow<str>` | `rs.Cow<T>`: the `Vec` it owns, or an `rs.Borrowed` holding the slice it borrows, `rs.Cow<T, rs.ArraySlice<S>>` holding the arrays of a slice of arrays; `rs.ByteCow` for bytes, an `rs.ByteVec` or an `rs.ByteSlice`; `rs.StrCow` for text, the `rs.String` it owns or an `rs.Borrowed` holding the string it borrows, whose text `rs.strCowStr` reads |
| `&T`, `&mut T`, `Box<T>`, `Reverse<T>`, `NonZero<T>`, `AssertUnwindSafe<T>` | the value; a `&mut` to a value JS copies, such as a number, an `Option`, or a reference, or to an `Arc`, a `Waker`, or a `LocalKey`, whose copies share what it points at, is an `rs.Place<T>`, which for what a box holds is the place holding the box |
| `*const T`, `*mut T`, `NonNull<T>` | what a `&mut T` is: the object, an `rs.Slice` of a `[T; N]`'s elements or an `rs.ByteSlice` of a `[u8; N]`'s bytes, which compares by the array or buffer and offset they are at, refused for an array of one, which `array::from_ref`, `from_mut` of a variable, and `Option::as_slice` make of their own, or an `rs.Place<T>` to a value JS copies and to a library type such as a `Vec` or an `Arc`, which into an array, slice, or `Vec` is the `rs.AtIndex` of its element, which `add`, `sub`, `offset`, their wrapping forms, `offset_from`, `ptr::copy`, and `slice::from_raw_parts` move, measure, copy between, and view; one to a value in no array is element 0 of an array of one, as in Rust; JS keeps that array, and a shared subslice, which JS copies, apart from what holds it in Rust, so a pointer, slice, or copy the body shows reaching past either, a raw or mutable slice of a byte in no array, and `offset_from` between pointers the body shows JS keeps in different arrays are refused, and through a pointer from elsewhere, such as a field, any of these, or a read or write past the array JS keeps it in but for a `Vec`'s room up to its capacity, fails at run time; a pointer into an array of values JS keeps as objects is refused, as is comparing pointers to other values JS copies, each a place JS makes of its own; `rs.NULL` for null and `rs.DANGLING` for dangling. A pointer to a slice cast to one to an array of its elements is the `rs.Slice` or `rs.ByteSlice` of that many from its first, read through only where Rust reads. A `*const ()` is an identity, such as a waker's data, and `rs.DANGLING_UNIT` when dangling; one to a place of `()` or from a reference to one is refused. A pointer to a value JS keeps as an object cast to one, by `as` or `cast`, is the same object, which `rs.ptrErase` notes the type of, so a cast back to a pointer to another type fails at run time, and null and dangling stay so, dangling at the alignment of the type it was cast from; one to a value JS copies is the same place where an `Arc`'s or `Rc`'s `into_raw` or `as_ptr` gave it, its value's one place, which `rs.arcErase` checks where the body doesn't show it, and refused where the body shows a borrow's, a new place each borrow, as is casting one to a view of bytes, a new one each borrow, or one of no fixed size. A write of a whole object copies into it one object deep, as an assignment through a `&mut` does, the state an `rs` class such as `rs.LazyCell` keeps private included, so a pointer to a part kept as an object, such as a field of a tuple or struct type, is refused wherever the crate may put another object there while a pointer into that memory may still be read, which a write into a fresh allocation, or a variable before any pointer into it is taken, can't, whether by index or through std's iterators over its slots; a read copies what the crate writes into in place |
| `MaybeUninit<T>` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined`, as each of `Box::new_uninit_slice`'s is; a slice's `write_clone_of_slice` clones into its slots without dropping what they held |
Expand Down Expand Up @@ -173,14 +173,20 @@ same array. A caller passes one as `rs.sliceFrom(array)` and reads one it gets a
JS array, and a `&[T; N]` or `&mut [T; N]` is an `rs.Slice` of its elements where they
are, as a `&[u8; N]` is an `rs.ByteSlice`, so `first_chunk_mut`, `split_first_chunk_mut`,
a subslice pattern, and `array::from_mut` lend part of the same array, and a whole write
through one writes each element in place. A slice of arrays has each as a JS array of its
own, so `as_chunks_mut` and `as_rchunks_mut` lend their runs as a `Proxy` of an array
whose elements are views of each run, a `Uint8Array` one for bytes, and a whole write of
a run copies into it.
Before that write, a view of other values that a read took detaches to a copy of what it
held. A byte run's view can't, so it is copied wherever it is read by value: the
call counts as a write into the bytes, which has translated code copy each `[u8; N]` it
reads, and the runtime's swaps, rotations, and sorts move copies of the runs.
through one writes each element in place. A slice of arrays, a `&[[T; N]]`,
`&mut [[T; N]]`, `&[[T; N]; M]`, or `Box<[[T; N]]>`, is an `rs.ArraySlice` of the slice each
array is, `rs.ArraySlice<rs.Slice<T>>`, `rs.ArraySlice<rs.ByteSlice>` for bytes, and
`rs.ArraySlice` of `rs.ArraySlice` for arrays of arrays: its element `i` is the slice of array `i`, read and written
where the arrays are, whether each is a JS array of its own, as a `Vec<[T; N]>` or a
`[[T; N]; M]` holds them, or a part of one slice, as `as_chunks_mut` and `as_rchunks_mut`
split it. A caller passes one as `rs.arraySliceFrom(arrays, rs.sliceFrom)`, `rs.byteSliceFrom`
for bytes and `rs.arraySliceOf(rs.sliceFrom)` for arrays of arrays, and reads one as
`rs.arraySliceItems(slice)`: the arrays themselves, or copies of the ones a split of one
slice holds, which have no array of their own. std's methods that lend arrays or parts of
the slice, such as `iter_mut`, `split_at_mut`, `chunks_mut`, and `swap`, take them where
they are; any other reads them as `rs.arraySliceItems` does, or for a `&mut` one, such as
`sort` or `rotate_left`, reads copies of the arrays, which it writes back once it returns,
as Rust moves their elements.

`usize` is 32 bits, as on wasm32, so it is exact in a `number`. Arithmetic is checked as
a debug build checks it, so translated code panics where native code would: it never
Expand Down
4 changes: 2 additions & 2 deletions ts/fixtures/codec/expected/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ export function packets(selector: rs.u8, data: rs.ByteSlice, out: rs.ByteVec): v
let _32!: rs.u8;
let cc!: rs.u8;
const packets: rs.ByteVec = rs.byteVecOf(0, rs.usizeMul(rs.u64Low(rs.u64Min(takeU64(data)[0], 4096)), 188));
const iter: rs.Iter<[rs.usize, rs.ByteSlice]> = rs.iterEnumerate(rs.iterLend(rs.iterSlice(rs.sliceItems(rs.byteSliceAsChunksMut(rs.byteVecSlice(packets), 188)[0])), rs.byteSliceFrom));
const iter: rs.Iter<[rs.usize, rs.ByteSlice]> = rs.iterEnumerate(rs.arraySliceIter(rs.sliceAsChunks(rs.byteVecSlice(packets), 188)[0]));
while (true) {
const _21: [rs.usize, rs.ByteSlice] | rs.Done = iter.next();
if (_21 === rs.DONE) {
Expand All @@ -699,7 +699,7 @@ export function packets(selector: rs.u8, data: rs.ByteSlice, out: rs.ByteVec): v
}
const counters: Uint8Array = rs.bytes(3);
let sum: rs.u64 = 0;
const iter2: rs.Iter<rs.ByteSlice> = rs.iterLend(rs.iterSlice(rs.sliceItems(rs.byteSliceAsChunksMut(rs.byteVecSlice(packets), 188)[0])), rs.byteSliceFrom);
const iter2: rs.Iter<rs.ByteSlice> = rs.arraySliceIter(rs.sliceAsChunks(rs.byteVecSlice(packets), 188)[0]);
while (true) {
const _48: rs.ByteSlice | rs.Done = iter2.next();
if (_48 === rs.DONE) {
Expand Down
Loading