Skip to content

feat(ts): a byte local whose address is taken, in a buffer of its own - #640

Draft
kixelated wants to merge 2 commits into
mainfrom
quest/m3/ts-unsafe/lone-byte-buffers
Draft

kixelated wants to merge 2 commits into
mainfrom
quest/m3/ts-unsafe/lone-byte-buffers

Conversation

@kixelated

@kixelated kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Claims quest/m3/ts-unsafe/lone-byte-buffers.md, planned in moq-dev/rust2#624, and deletes it.

Problem

JS kept a u8 local in no array of bytes, so a raw or mutable slice of it could not be a view of where it is. moq-dev/rust2#624 refused such slices, and casts of them to *const [u8; N], only on the routes its raw_start tracer followed, and kept missing routes (&raw *s, NonNull::from, NonNull::new(p).unwrap()). The rest threw at run time.

Approach

A u8 or MaybeUninit<u8> local whose &raw the body takes, or whose reference made in the body is cast to a raw pointer (&mut b as *mut u8, let p: *const u8 = &b), is element 0 of a Uint8Array(1) of its own (Slot::Byte). Every read and write is b[0], and every pointer to it is rs.arrayAsPtr(b). Its raw, mutable, and shared slices, and casts of them to *const [u8; N], are views of that buffer on every route, and offset_from between its pointers measures them. A parameter keeps its name in the signature and is copied into the buffer on entry.

The lone-byte refusals are removed: Fx::lone_byte_array with its raw_start tracer, and stray's refusal of a raw or mutable slice of a lone byte. Array::Copy stays for bytes still in no array, where a shared slice is still an exact copy.

Proven by the differential case pointers::lone_bytes against native Rust. It covers each slice and cast route, a parameter, &mut b as *mut u8, a coercion from &c, and a MaybeUninit<u8>.

Decisions

  • Bytes in fields (2026-11-07, maintainer): ✅ leave the old lone-byte refusals removed. A raw or mutable slice of a byte stored in a field (a struct's, a tuple's, a closure's capture, or one an async body keeps across an .await) throws at run time. Throw only where Rust panics records the gap and owns replacing it with a translation or a refusal: a buffer for each address-taken u8 field. Rejected: keeping the refusals for fields only, which is partial route tracing that would be deleted later. Rejected: giving field bytes buffers in this PR.

Choices made without asking

  • The quest asks for locals whose &raw the body takes. A &raw of *r, where r is a reference to the local made once in the body, buffers the local too. That is what &mut b as *mut u8 and a coercion to a raw pointer compile to, and without it the most common spelling kept throwing. Calls such as ptr::from_mut(&mut b) are not traced and still throw, as they did before this PR (origin never traced them). ts-runtime-throws.md lists that as well.
  • MaybeUninit<u8> locals are buffered too, since crate::lower::byte counts them as bytes.

Overlaps

  • moq-dev/rust2#621 translates &raw const *r for any value JS copies. This PR handles it only for a buffered byte, in the same RawPtr arm of rvalue.rs. Whichever lands second resolves the conflict and keeps both: the buffered byte's arm first, then feat(ts): translate a raw pointer behind a reference #621's general one.
  • moq-dev/rust2#608 also edits quest/m3/ts-runtime-throws.md's gap list, and plans slice::from_mut(&mut b) as a quest of its own (that call is refused, not thrown).

Caused by

  • moq-dev/rust2#624 - refused casts of a lone byte's raw slice only on the routes raw_start traced
  • moq-dev/rust2#400 - kept a byte local in no array of bytes, so its raw and mutable slices could not be views

Impact

  • The translator's output: such a local is now a Uint8Array in generated code, and constructs the reject fixture refused (lone_byte_slices, lone_byte_arrays) now translate. No runtime or wire change.

Alternatives

  • Keep lone_byte_array for bytes in fields: rejected, see Decisions.
  • Buffer every byte local that any &mut borrows, which would also cover call routes like ptr::from_mut(&mut b): not taken. It changes the output of every &mut u8 local for routes no consumer uses.

Validation

  • quest gate just ci origin/main after merging origin/main at fd62edf2c: passed, including lang::pointers' new differential case pointers::lone_bytes against native Rust (89 fixture tests). With moq-dev/rust2#633 on main, UPDATE_EXPECT=1 just ts test left every snapshot, dyns and panics/hook included, unchanged.
  • quest check --fix: 50 documents ok.

Follow-ups

Models

Size: M
Implementer: Claude Code, claude-opus-5-5, high effort

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1b680-72a7-7f00-93a9-494d8a478eed, round 1
  • Head: 66a8b5be6482, named in full once a round reports no findings
  • Base: origin/main at 049c551
  • Score: 8/10, The buffer approach is sound in principle, but the reference-origin shortcut can silently redirect writes to the wrong byte.
  • Findings, open until the next round settles them:
    • Medium, ts/src/lower/mir/mod.rs:848: [P2] Exclude mutated references from byte-origin tracing

@pullfrog

pullfrog Bot commented Nov 7, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

A `u8` or `MaybeUninit<u8>` local whose `&raw` the body takes, or that of a
reference to it made there, is element 0 of a `Uint8Array` of its own, so
its raw, mutable, and shared slices, and arrays cast from them, view it.
The lone-byte refusals in origin.rs go; a byte in a field still throws at
run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title A byte local whose address is taken, in a buffer of its own feat(ts): a byte local whose address is taken, in a buffer of its own Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant