Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6668a9b
chore(quest): claim quest/m3/ts-slice-object.md
kixelated Nov 4, 2026
6451033
wip(ts): a reference to an array of other values is an rs.Slice
kixelated Nov 4, 2026
fbfcbb0
wip(ts): compare element places, unsize an Arc's array, fix array len…
kixelated Nov 4, 2026
dbf48f4
Merge origin/main
kixelated Nov 4, 2026
518ab0b
style: format
kixelated Nov 4, 2026
1b50175
fix(ts): copy nested arrays a pointer reads, keep shared runs' place,…
kixelated Nov 4, 2026
d220d26
Merge origin/main
kixelated Nov 4, 2026
12b5e70
style: format
kixelated Nov 4, 2026
3509936
fix(ts): read format_args!'s array through its slice, folding a lone …
kixelated Nov 4, 2026
1e75a42
Merge origin/main
kixelated Nov 4, 2026
9d82149
fix(ts): refuse comparing element places at translation, as main does
kixelated Nov 4, 2026
fd87d43
Merge remote-tracking branch 'origin/main' into ts-slice-array
kixelated Nov 4, 2026
e9a5c4c
style: format
kixelated Nov 4, 2026
bed9f30
quest: leave element-place comparison to the runtime-throws plan
kixelated Nov 4, 2026
53f3a7d
Merge remote-tracking branch 'origin/main' into ts-slice-array
kixelated Nov 4, 2026
aba6ffd
test(ts): update the model's snapshots for format arguments read in p…
kixelated Nov 4, 2026
a395856
fix(ts): keep shared runs where they point, refuse comparing arrays o…
kixelated Nov 4, 2026
ad81b2b
fix(ts): quote array::from_ref verbatim in the census
kixelated Nov 4, 2026
7edc9c7
Merge remote-tracking branch 'origin/main' into ts-slice-array
kixelated Nov 4, 2026
9f488a4
quest: link the runtime-throws quest for the comparisons left refused
kixelated Nov 4, 2026
dd074b8
Merge origin/main
kixelated Nov 4, 2026
2cb31f0
test(ts): update snapshots after merging main
kixelated Nov 4, 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
49 changes: 25 additions & 24 deletions quest/m3/ts-slice-object.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@

## Goal

[rust2-ts](/ts/README.md) translates `&[T]`, `&mut [T]`, and `Box<[T]>` of values other
than bytes as `rs.Slice`, its array, start, and length, as `rs.ByteSlice` is for bytes.
What remains:

- A `&[T; N]` and `&mut [T; N]` of other values is an `rs.Slice` too, as `&[u8; N]` is an
`rs.ByteSlice`, so the run `first_chunk_mut`, `split_first_chunk_mut`, an array subslice
pattern, and `array::from_mut` lend is where the slice holds it, rather than the
runtime's `arrayView` `Proxy`.
- Pointers to a `[T; N]` and to elements JS copies compare where they point, as `ptr::eq`
does natively, lifting the refusal of
[moq-dev/rust2#510](https://github.com/moq-dev/rust2/pull/510) and of comparing two
`rs.Place`s of one element.
[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.

Expand All @@ -25,20 +22,24 @@ Decided with the maintainer on 2026-11-03: Rust types the runtime can't hold as
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 `[T; N]` value stays a JS array. A reference to one would be an `rs.Slice` of it, as
`Pointee::Bytes` makes a reference to a `[u8; N]` an `rs.ByteSlice`, generalized to any
element. Then `as_chunks_mut`'s `Runs` and `ByteRuns` views and `arrayView` go, along with
the detaching copies they need before a whole write.

Pointer identity then compares an `rs.AtIndex` by its array and index, and an `rs.Slice`
by its data, start, and length, as `rs.ByteSlice` already does.
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; a slice 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))`.

Public API: the translator's output and what a hand-written caller passes for a
reference to an array.
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).

Public API: the translator's output and what a hand-written caller passes for a slice of
arrays.

## Related

Expand Down
40 changes: 23 additions & 17 deletions ts/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions ts/fixtures/codec/expected/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,8 @@ export function format(data: rs.ByteSlice, out: rs.ByteVec): void {
const point: Point.Point = { x: i, y: rs.i64Neg(i) };
const _26: rs.f64 = rs.i64ToF64(i) / 3.0;
const _29: rs.i64 = rs.i64Neg(i);
const args2: rs.Fmt[] = [(f: rs.Formatter) => rs.i64Display(i, f), (f: rs.Formatter) => Point.fmt$fmt_Debug(point, f), (f: rs.Formatter) => rs.f64Display(_26, f), (f: rs.Formatter) => rs.i64Display(_29, f)];
s = rs.fmtWriteString(s, [rs.fmtSpec({ align: ">", width: 6 }, args2[0]), "|", args2[1], "|", rs.fmtSpec({ precision: 2 }, args2[2]), "|", args2[3], "\n"]).s;
const args3 = [(f: rs.Formatter) => rs.i64Display(i, f), (f: rs.Formatter) => Point.fmt$fmt_Debug(point, f), (f: rs.Formatter) => rs.f64Display(_26, f), (f: rs.Formatter) => rs.i64Display(_29, f)];
s = rs.fmtWriteString(s, [rs.fmtSpec({ align: ">", width: 6 }, args3[0]), "|", args3[1], "|", rs.fmtSpec({ precision: 2 }, args3[2]), "|", args3[3], "\n"]).s;
}
putUsize(out, rs.stringLen(s));
harness_putU64(out, rs.iterSum(rs.iterMap(rs.strBytes(s), (a0: rs.u8): rs.u64 => {
Expand Down
7 changes: 3 additions & 4 deletions ts/fixtures/codec/expected/coding/decode.DecodeError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export function fmt$fmt_Display(self: DecodeError, __formatter: rs.Formatter): r
}
if (self.tag === "InvalidMessage") {
const arg = self._0;
const args2: rs.Fmt[] = [(f: rs.Formatter) => rs.u64Debug(arg, f)];
return rs.fmtWriteFmt(__formatter, ["invalid message: ", args2[0]]);
return rs.fmtWriteFmt(__formatter, ["invalid message: ", (f: rs.Formatter) => rs.u64Debug(arg, f)]);
}
if (self.tag === "InvalidSubscribeLocation") {
return rs.fmtWriteStr(__formatter, "invalid subscribe location");
Expand All @@ -38,8 +37,8 @@ export function fmt$fmt_Display(self: DecodeError, __formatter: rs.Formatter): r
if (self.tag === "MessageTooLarge") {
const _26: rs.usize = asDisplay$thiserror___private21_AsDisplay_ref_usize(self.size);
const _28: rs.usize = asDisplay$thiserror___private21_AsDisplay_ref_usize(self.max);
const args4: rs.Fmt[] = [(f: rs.Formatter) => rs.usizeDisplay(_26, f), (f: rs.Formatter) => rs.usizeDisplay(_28, f)];
return rs.fmtWriteFmt(__formatter, ["message too large: ", args4[0], " bytes exceeds ", args4[1], " byte limit"]);
const args5 = [(f: rs.Formatter) => rs.usizeDisplay(_26, f), (f: rs.Formatter) => rs.usizeDisplay(_28, f)];
return rs.fmtWriteFmt(__formatter, ["message too large: ", args5[0], " bytes exceeds ", args5[1], " byte limit"]);
}
if (self.tag === "ExpectedData") {
return rs.fmtWriteStr(__formatter, "expected data");
Expand Down
18 changes: 6 additions & 12 deletions ts/fixtures/codec/expected/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,13 @@ export function takeU64(data: rs.ByteSlice): [rs.u64, rs.ByteSlice] {
/** A decode error's code, then its text. */
export function decodeFailed(out: rs.ByteVec, err: DecodeError.DecodeError): void {
decodeError(out, err);
const _5: string = rs.fmtToString((f: rs.Formatter) => DecodeError.fmt$fmt_Display(err, f));
const args2: rs.Fmt[] = [(f: rs.Formatter) => DecodeError.fmt$fmt_Debug(err, f)];
putText(out, _5, rs.fmtFormat([args2[0]]));
putText(out, rs.fmtToString((f: rs.Formatter) => DecodeError.fmt$fmt_Display(err, f)), rs.fmtFormat([(f: rs.Formatter) => DecodeError.fmt$fmt_Debug(err, f)]));
}

/** An encode error's code, then its text. */
export function encodeFailed(out: rs.ByteVec, err: EncodeError.EncodeError): void {
encodeError(out, err);
const _5: string = rs.fmtToString((f: rs.Formatter) => EncodeError.fmt$fmt_Display(err, f));
const args2: rs.Fmt[] = [(f: rs.Formatter) => EncodeError.fmt$fmt_Debug(err, f)];
putText(out, _5, rs.fmtFormat([args2[0]]));
putText(out, rs.fmtToString((f: rs.Formatter) => EncodeError.fmt$fmt_Display(err, f)), rs.fmtFormat([(f: rs.Formatter) => EncodeError.fmt$fmt_Debug(err, f)]));
}

/** An error's thiserror `Display` and derived `Debug`, which callers see. */
Expand Down Expand Up @@ -591,16 +587,14 @@ export function errors(dataData: Uint8Array, dataStart: number, dataLen: number,
const arg = {};
const _26: string = rs.fmtToString((f: rs.Formatter) => BoundsExceeded.fmt$fmt_Display(arg, f));
const arg2 = {};
const args2: rs.Fmt[] = [(f: rs.Formatter) => BoundsExceeded.fmt$fmt_Debug(arg2, f)];
putText(out, _26, rs.fmtFormat([args2[0], " ", rs.fmtSpec({ alternate: true }, args2[0])]));
const args7 = [(f: rs.Formatter) => BoundsExceeded.fmt$fmt_Debug(arg2, f)];
putText(out, _26, rs.fmtFormat([args7[0], " ", rs.fmtSpec({ alternate: true }, args7[0])]));
break b0;
}
err = { tag: "MalformedAuthToken" } as DecodeError.DecodeError;
}
const args4: rs.Fmt[] = [(f: rs.Formatter) => DecodeError.fmt$fmt_Display(err, f)];
const _38: string = rs.fmtFormat([args4[0], " ", rs.fmtSpec({ align: ">", width: 30 }, args4[0]), "|"]);
const args6: rs.Fmt[] = [(f: rs.Formatter) => DecodeError.fmt$fmt_Debug(err, f)];
putText(out, _38, rs.fmtFormat([rs.fmtSpec({ alternate: true }, args6[0])]));
const args8 = [(f: rs.Formatter) => DecodeError.fmt$fmt_Display(err, f)];
putText(out, rs.fmtFormat([args8[0], " ", rs.fmtSpec({ align: ">", width: 30 }, args8[0]), "|"]), rs.fmtFormat([rs.fmtSpec({ alternate: true }, (f: rs.Formatter) => DecodeError.fmt$fmt_Debug(err, f))]));
decodeFailed(out, err);
}
}
Expand Down
3 changes: 1 addition & 2 deletions ts/fixtures/codec/expected/lib.SessionError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export function fmt$fmt_Display(self: SessionError, __formatter: rs.Formatter):
return rs.fmtWriteStr(__formatter, "protocol violation");
}
const arg = asDisplay$thiserror___private21_AsDisplay_ref_u16(self._0);
const args2: rs.Fmt[] = [(f: rs.Formatter) => rs.u16Display(arg, f)];
return rs.fmtWriteFmt(__formatter, ["app code=", args2[0]]);
return rs.fmtWriteFmt(__formatter, ["app code=", (f: rs.Formatter) => rs.u16Display(arg, f)]);
}

export function fmt$fmt_Debug(self: SessionError, f: rs.Formatter): rs.Result<undefined, rs.FmtError> {
Expand Down
6 changes: 3 additions & 3 deletions ts/fixtures/crates/bind/slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function withBase(base: rs.i16): Slots {
return { base };
}

export function fill(self: Slots, out: rs.i16[]): undefined {
out[0] = self.base;
out[1] = rs.i16Sub(self.base, 1 as rs.i16);
export function fill(self: Slots, out: rs.Slice<rs.i16>): undefined {
out.data[out.start] = self.base;
out.data[out.start + 1] = rs.i16Sub(self.base, 1 as rs.i16);
}
6 changes: 3 additions & 3 deletions ts/fixtures/crates/expected/bind/slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function withBase(base: rs.i16): Slots {
return { base };
}

export function fill(self: Slots, out: rs.i16[]): undefined {
out[0] = self.base;
out[1] = rs.i16Sub(self.base, 1 as rs.i16);
export function fill(self: Slots, out: rs.Slice<rs.i16>): undefined {
out.data[out.start] = self.base;
out.data[out.start + 1] = rs.i16Sub(self.base, 1 as rs.i16);
}
2 changes: 1 addition & 1 deletion ts/fixtures/crates/expected/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function run(input: rs.ByteSlice): rs.ByteVec {
}
const kept: rs.i16[] = [(input.length << 16) >> 16, 7];
const filled: rs.i16[] = copy$array_i16(kept);
Slots.fill(Slots.withBase(kept[0]), filled);
Slots.fill(Slots.withBase(kept[0]), rs.sliceFrom(filled));
rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(kept[0] & 0xff, kept[1] & 0xff, filled[0] & 0xff, filled[1] & 0xff)), undefined, true);
const _202: rs.u8 = first(input.data, input.start, input.length);
rs.byteVecPush(out, _202);
Expand Down
6 changes: 2 additions & 4 deletions ts/fixtures/lang/args/expected/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@ export function op(name: string): rs.u16 {
return _20.value;
}
}
const args4: rs.Fmt[] = [(f: rs.Formatter) => rs.fmtPad(f, name)];
rs.panicFmt(["case ", args4[0], " is past what two bytes pick"]);
rs.panicFmt(["case ", (f: rs.Formatter) => rs.fmtPad(f, name), " is past what two bytes pick"]);
} else {
const args2: rs.Fmt[] = [(f: rs.Formatter) => rs.fmtPad(f, name)];
rs.panicFmt(["no case ", args2[0]]);
rs.panicFmt(["no case ", (f: rs.Formatter) => rs.fmtPad(f, name)]);
}
}

Expand Down
Loading