Repository navigation
feat(ts): translate str::as_ptr as a read-only pointer into its UTF-8 - #634
Conversation
Quest-Claim: bf851f467c4a2ef7
A `&str` stays a JS string, so `rs.strAsPtr` encodes its UTF-8 and points at the first byte. Reads, arithmetic, copies from it, and `from_raw_parts` read what Rust reads; a write, a copy into it, or a mutable slice of it, which Rust forbids, fails at run time. `offset_from` between two calls' pointers is refused where the body shows both. arrayvec's `ArrayString` now translates: the stock fixture runs it against native Rust. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me-throws gap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # ts/README.md
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit 7335b88 against base c0f5344.
No actionable findings identified in this static review. TextAt preserves the read-only pointer through normal and wrapping arithmetic, and the copy/mutable-slice paths reject writes into its encoding. I checked the call mapping, origin-based refusals, bounds and offset handling, the registered UTF-8 pointer cases and their generated output, and the ArrayString consumer fixture. Using the existing pointer machinery with a distinct text origin is a coherent direction for this scoped support.
The documented limits remain: separate as_ptr calls produce separate encodings, and substring reads beyond their own bytes can fail at runtime. This review does not treat those explicitly recorded design limits as resolved or settle the Rust memory-model question. The two documented UB-only write routes are also outside the added guards.
Verification limits: GitHub source/diff inspection only. I did not execute runtime tests, native differential fixtures, translator builds, the census, or just ci; the reported passing results and refusal reductions have not been independently reproduced. This is a COMMENT, not an approval.
(Written by review (OpenAI))
|
quest land watch: the merge queue removed this pull request at 7335b88 because it conflicts with The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs. (written by quest land watch) |
|
quest land watch: armed auto-merge on this pull request at 2b9b9d4, so it joins the back of the merge queue once its required checks pass, with the land fixer's fix ( (written by quest land watch) |
|
quest land watch: the merge queue removed this pull request at 2b9b9d4 because it conflicts with The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs. (written by quest land watch) |
|
quest land watch: armed auto-merge on this pull request at 17055d7, so it joins the back of the merge queue once its required checks pass, with the land fixer's fix ( (written by quest land watch) |

Closes the quest quest/m2/ts-model/str-as-ptr.md.
Decisions
s[..1].as_ptr().add(1)read forward ands[1..].as_ptr().wrapping_sub(1)read back, keep failing loudly at run time with arust2-ts:error. ✅ Merge as is (rejected: trackingstrlengths and wrapping offsets through to reads at translation; a substring sharing its text's encoding, which reverses moq-dev/rust2#574; refusingstr::as_ptrunless the body proves every read in bounds). Decided by the maintainer on 2026-11-07 at the round-3 design checkpoint. These reads are UB under Stacked Borrows and allowed under Tree Borrows, and Rust's memory model is unsettled, so a loud failure never silently differs from Rust. The constantsubrefusal from round 1 is kept. The future fix is recorded in quest/m3/ts-runtime-throws.md's gap list: if Rust rules these reads defined, a substring's pointer carries its text's encoding and offset.Problem
str::as_ptrwas an unsupported call, so arrayvec'sArrayString::try_push_str, which copies a&strinto its buffer through the pointer, refused translation. At the moq pin that is 1 refusal in the model's census and 3 in moq-net whole (arrayvec's, plus two in zlib-rs). It was the last child of quest/m2/ts-model/README.md that this repo owns. moq-dev/rust2#574 settled that a&strstays a JS string, so the bytes behind the pointer are an encoding of the text.Approach
rs.strAsPtr(s)encodes the text's UTF-8 and returns a pointer to its first byte. The pointer is aTextAt, a privateAtIndexsubclass. Reads work as they do for any byte pointer:*p,read, arithmetic,offset_fromwithin one encoding,ptr::copy/copy_nonoverlappingfrom it, andslice::from_raw_parts.&str's pointer. Here such a write would land in a copy the text never reads, so it fails at run time instead. That covers*p = x, a copy into it, and a mutable slice (from_raw_parts_mut, orderefBytes(false)of a raw slice). Arithmetic keeps the pointer read-only:AtIndexgains anat(index)method thatptrAddandptrWrappingAddcall, andTextAtoverrides it.str::as_ptrcall is its own origin (Array::Text) inmir/origin.rs. Sooffset_frombetween two calls' pointers is refused with a span wherever the body shows both. A typical case is a substring's pointer against its text's, which Rust allows but JS can't match. A pointer starts at index 0 of its own encoding, so a constantadd/sub/offsetthe body shows moving it back before its first byte is refused with a span, as one off anArray::Copyis. That covers a substring's pointer moved back into its text. Anything the body doesn't show is left to the runtime, which fails on a read outside the encoding.Validation
lang/strs' newstrs::pointers: text of every UTF-8 width and a substring from each char boundary, against native Rust. It copies into aVec's room and between bytes, reads every tail throughfrom_raw_parts, walks forward and back by*,NonNull, andoffset_from/offset_from_unsigned, and pushes into a[MaybeUninit<u8>; 12]asArrayStringdoes.stock's newfixed::strings: the realarrayvec::ArrayString<12>(0.7.8, the version moq's lock has). It coverstry_push,try_push_strwith its errors,push_str,pop,truncate,remove,from, andclearover chars of each width.tests/pointers.ts: reads, arithmetic, and views pass. Writes, copies in, and mutable slices fail with arust2-ts:error, and so doesoffset_fromacross two encodings.text_apart's twooffset_froms andtext_before'ssub(1)are refused with spans.str::as_ptrrefusals, and no new refusal appears.just cipasses at8fdffa85b, after merging origin/main at049c5516dwith moq-dev/rust2#633's TypeId-hash snapshot fix.Impact
Public API (translator output):
str::as_ptrtranslates instead of refusing.rs.strAsPtr.AtIndex.at(index)method.strandstr::as_ptrverbatim in its constructs.ArrayString::try_push_str, and zlib-rs' twostr::as_ptrs.Quests:
quest/m2/ts-model/str-as-ptr.mdand its link.[L]for its remaining end-to-end work, and its census paragraph is refreshed to 51.Choices made without asking
cast_mutoras *mutof the pointer, and the result can flow anywhere, so only the runtime sees every write. That is the same split the README documents for pointers from elsewhere. The checks cost oneinstanceofper copy or mutable slice and nothing on other pointers' writes, becauseTextAtis its own class.*mut [u8; N]cast from a raw slice of the text;as_ptrof a shared slice thatfrom_raw_partsmade of the text.*const u8s was already refused.str::as_mut_ptrstays unsupported. Writes through it are defined in Rust and must reach theString, so it needs the bytes to be the string's own storage. Nothing in moq calls it, and it is outside this quest's Goal.Alternatives
ptr::eqandoffset_fromagree across calls. A strongMapof strings leaks, and a substring is a new JS string anyway, so its offset into its text still could not match.ArrayBuffer(transferToImmutable). Node, Bun, Chrome, and Firefox don't all ship it, and writes to it fail silently outside strict mode.Follow-ups
None.
Models
Size: S
Implementer: Claude Code, claude-opus-5-5, high effort
Review
mechanical fix by codex, gpt-6.1-sol, medium effort:
Merged base and re·solved ts/fixtures/reject/census.txt by preserving both sides’ expected diagnostics; ran git diff --check --cached, just check base, and just test base successfully.(head 17055d7)mechanical fix by codex, gpt-6.1-sol, medium effort:
Merged base, preserved both documentation and fixture additions, and regenerated harness.ts; passed UPDATE_EXPECT=1 just ts test --test fixtures stock, just check base, and just test base.(head 2b9b9d4)Rewritten by
quest reviewafter each round.01a1b52a-0b86-7ac1-8df0-8726554271f2, round 5The branch’s added and deleted lines match the previously approved diff, the README merge preserves both changes, and pointer runtime tests pass; no new defects were found.7335b88daadds only a conflict-free merge of origin/main (moq-dev/rust2#646, bench and quest files) over the reviewed head;just checkpasses on it.