Repository navigation
feat(ts): model typed allocation as a fresh JS array - #418
Merged
Merged
Conversation
Quest-Claim: c56ea58c47a4ebd5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…older replaces Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, views, and empty slices Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue's pointer in an empty slice Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # ts/fixtures/lang/expected/harness.ts # ts/fixtures/lang/src/harness.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per the maintainer's decision on #400: offset_from between pointers into a value in no array or a copied shared subslice and another array, a raw or mutable slice of a lone byte, and constant offsets, slices, and copies reaching past either are refused with a span. Pointers only run time shows still fail loudly; ptrSlice, ptrByteSlice, and ptrCopy now check their whole extent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared slice of a byte in no array is a copy, so its pointer measured against the byte's is refused; a slice from raw parts otherwise starts where its pointer points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per the maintainer's decision on #400: memory from alloc, cast to *mut T where the body shows the pointer came from alloc or realloc, is a JS array of T; realloc copies it and dealloc frees it, through a cast back to *mut u8 only as their argument. Layout, LayoutError, and align_of map to the runtime. Byte use of the memory, and every other cast, stay refused. 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>
…nd wrap null The provenance trace matched the crate's own functions named as std's; a copy ran a value's copy over memory nothing wrote; and wrapping arithmetic on a null pointer threw from the lone-value map instead of moving it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dangling pointer is at its pointee's alignment, so wrapping arithmetic can take it to null, as it can take null away and back. 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>
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>
# Conflicts: # ts/fixtures/lang/expected/harness.ts # ts/fixtures/lang/src/harness.rs # ts/fixtures/reject/census.md # ts/fixtures/reject/expected.txt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # ts/fixtures/lang/expected/harness.ts # ts/fixtures/reject/census.md # ts/fixtures/reject/expected.txt # ts/src/lower/mir/origin.rs
…alues that fit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were the harness case and import lists, the reject roots and cases, and the reject snapshots, which were regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…keep empty parts' pointers 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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # ts/fixtures/reject/census.md # ts/fixtures/reject/expected.txt
# Conflicts: # ts/fixtures/lang/expected/harness.ts # ts/fixtures/lang/src/harness.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # quest/m3/ts-unsafe.md # ts/README.md # ts/fixtures/lang/expected/harness.ts # ts/fixtures/lang/src/harness.rs # ts/fixtures/reject/census.md # ts/fixtures/reject/expected.txt # ts/fixtures/reject/src/lib.rs # ts/fixtures/reject/ts.toml # ts/src/lower/mir/origin.rs # ts/tests/pointers.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # quest/m3/ts-unsafe.md # ts/README.md # ts/fixtures/lang/expected/core/bool.ts # ts/fixtures/lang/expected/harness.ts # ts/fixtures/lang/src/harness.rs # ts/fixtures/lang/src/lib.rs # ts/fixtures/reject/census.md # ts/fixtures/reject/expected.txt # ts/src/lower/mir/call.rs # ts/src/lower/mir/origin.rs # ts/src/lower/mir/rvalue.rs # ts/tests/pointers.ts
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>
# Conflicts: # ts/fixtures/lang/expected/harness.ts # ts/fixtures/lang/src/harness.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
enabled auto-merge (squash)
November 2, 2026 20:38
kixelated
added a commit
that referenced
this pull request
Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq-bot Bot
pushed a commit
that referenced
this pull request
Nov 2, 2026
…n asked (#425) * chore(quest): claim quest/m3/ts-unsafe.md Quest-Claim: c56ea58c47a4ebd5 * feat(ts): translate pointers into arrays as rt.AtIndex Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): refuse a pointer to an element of an array its variable or holder replaces Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the lang harness snapshot after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): keep element pointers exact at wrapped addresses, lone values, views, and empty slices Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): copy only a shared slice of a lone byte, and keep a lone value's pointer in an empty slice Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the lang harness snapshot after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ts): refuse pointers a body shows crossing what JS keeps apart Per the maintainer's decision on #400: offset_from between pointers into a value in no array or a copied shared subslice and another array, a raw or mutable slice of a lone byte, and constant offsets, slices, and copies reaching past either are refused with a span. Pointers only run time shows still fail loudly; ptrSlice, ptrByteSlice, and ptrCopy now check their whole extent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): trace slices from raw parts, and type arguments past lifetimes A shared slice of a byte in no array is a copy, so its pointer measured against the byte's is refused; a slice from raw parts otherwise starts where its pointer points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the lang snapshot for a shared part's pointers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ts): model typed allocation as a fresh JS array Per the maintainer's decision on #400: memory from alloc, cast to *mut T where the body shows the pointer came from alloc or realloc, is a JS array of T; realloc copies it and dealloc frees it, through a cast back to *mut u8 only as their argument. Layout, LayoutError, and align_of map to the runtime. Byte use of the memory, and every other cast, stay refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): key memory from alloc by its type, and snapshot the heap cases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(ts): format the alloc refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): trace only std's pointer functions, copy unwritten memory, and wrap null The provenance trace matched the crate's own functions named as std's; a copy ran a value's copy over memory nothing wrote; and wrapping arithmetic on a null pointer threw from the lone-value map instead of moving it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the lang snapshot for the pointer fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): move null and dangling pointers by address A dangling pointer is at its pointee's alignment, so wrapping arithmetic can take it to null, as it can take null away and back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(ts): format the dangling cases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the lang snapshot for dangling pointers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): measure equal addresses as none apart Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(ts): format the equal addresses case Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the lang snapshot for equal addresses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the snapshots after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): key alloc memory by what a MaybeUninit holds, and keep the values that fit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): free alloc memory through cast and as_ptr Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the reject snapshot after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ts): hold a value by ManuallyDrop as the value, dropped only when asked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): refresh the snapshots for ManuallyDrop's copies and the renamed struct Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): keep a ManuallyDrop's payload from dropping through its holder, and copy it as its payload Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ts): accept wasm32's fma a last place off where the translation is exact Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): refuse offset_from across array fields, fail on dangling elements, keep null array casts null Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): regenerate the reject census after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): normalize pointers through mutable parts, keep null and dangling in empty raw slices Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): keep the empty byte slices' sentinels tree-shakeable, regenerate elements Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): keep the pointer an empty slice from no array was made from Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): find an empty slice's null pointer, which ?? passes over Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): unsize a raw pointer to an array as the place of its slice, keep empty parts' pointers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ts): read an unsized raw slice pointer through a cast Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): regenerate the elements snapshot Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ts): keep a raw slice pointer as its start and length Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(ts): keep the slice a raw slice pointer's first read makes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): fail a raw slice read past an array that shrank Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): read through NonNull::as_mut of a slice Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): keep the pointer of an empty slice of a lone byte Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): regenerate snapshots after merging #400's raw slices Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(quest): record offset_from across views of one byte buffer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): refuse memory from alloc cast to two types in one body Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): trace an allocation's type through realloc in one body Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ts): trace an allocation through pointer cast calls Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ts): regenerate snapshots after merging #418 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Luke Curley <432854+kixelated@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
smallvec spills to memory from
alloc::alloc(Layout::from_size_align(size_of::<T>() * n, align_of::<T>())), cast to*mut T, and frees and grows it withdeallocandreallocthrough*mut u8. rust2-ts refused all of it (Layout,alloc,realloc,dealloc,align_of, and the casts), so smallvec, which moq-net's model and kio reach, could not translate.Approach
The maintainer's decision 2 on #400 (2026-11-01): model a typed allocation as a fresh JS array; untyped and byte allocation stay refused. Builds on #400's
rt.AtIndexandrt.RawSlice; #400 has landed.rt.Layoutis{ size, align }, never changed once made:Layout::new,array,from_size_align(checked as std checks, against wasm32'sisize::MAX),from_size_align_unchecked,size,align, constants, and std'sDebug;LayoutErrorwith itsDebugandDisplay;mem::align_of.allocgives anrt.Blockof the layout's bytes. A cast of a*mut u8to*mut T(asorNonNull::cast) thatmir/alloc.rstraces back through the body toallocorrealloc, throughNonNull::new,Option::ok_or,unwrap,expect, and?, makes it a JS array of as many holes as wholeTs fit in its bytes, asrt.AtIndexof its first.Tmust be a value JS copies, as for any pointer into an array.*mut Tto*mut u8(as, orcastandNonNull::as_ptr) is allowed only as the pointerdeallocorrealloctakes, and gives the block, made on demand for aVec's array (smallvec'sfrom_vec).realloccopies the values that fit into a new block;dealloclets the array go.MaybeUninit<T>asT), not only its size, so a cast to another type of the same size (i32afteru32) fails rather than rereading the values. Where one body casts memory from onealloc(followed throughrealloc,as, andcast) to two types, translation refuses each cast with a span.alloc(*p,ptr::write, and the other pointer functions) where the body shows it came fromalloc, any other cast between bytes and values, a cast to a type JS keeps as an object, andalloc_zeroed. Through a pointer the body can't trace, each fails at run time with arust2-ts:error, per decision 1.Impact
Layout,alloc,realloc,dealloc,handle_alloc_error, andalign_oftranslate instead of failing.rt/alloc.ts, re-exported byrt.ts:Layout,LayoutError,LAYOUT_ERROR,Block,layoutFromSizeAlign,layoutArray,fmtLayoutDebug,fmtLayoutErrorDebug,fmtLayoutErrorDisplay,alloc,allocAs,allocBytes,dealloc,realloc,handleAllocError, all tree-shaken when unused. No hand-written binding names them, so noMIGRATE.mdentry.Census at main
b1656c97's moq pin, against #400's head21e40904merged with that main:In moq-net, smallvec goes from 54 refusals to 26, none of them on its allocation. What remains is
ManuallyDrop, its union,&rawthrough a reference, its iterators' trait impls, andVec::from_raw_parts. crossbeam-epoch, zlib-rs, moq, and kio lose theirLayout,alloc, andalign_ofrefusals. No refusal is added.Tests:
fixtures/lang/src/heap.rs, differential against native wasm32: smallvec's spill shape overu32s,Arcs (strong counts after each drop), andOptionpairs, pushed, grown and shrunk byrealloc(dropping what no longer fits), and freed; storage ofMaybeUninit<u32>read and freed asu32s; aMaybeUninit<u8>grown and freed throughcast; aLayoutof 9 bytes holding twou32s, grown to 11;Layouts across sizes and alignments up toisize::MAX, arrays ofu64and of(), constants, and theirDebugandDisplay.fixtures/reject:allocated(bytes read and written),bytes_as_values(an array's bytes asu32s, memory fromallocas the crate's struct, a value's bytes read, a byte written byptr::write),zeroed,alloc_retyped(one allocation cast tou32andi32, also afterreallocand acastback to bytes).tests/pointers.ts: a block's bytes, a cast to another type of the same size, a pointer past the start cast back,deallocandreallocof a pointer that is no block, andreallockeeping the values that fit.Alternatives
allocfrom itsLayout. Rejected at the design checkpoint: a layout is a size and an alignment at run time, and smallvec builds it in another function fromsize_ofandalign_of, so this would refuse smallvec unless the trace crossed functions.SmallVecto a runtime type. Rejected by the maintainer in favor of this, which serves any crate's typed allocation.Follow-ups
ManuallyDrop(next in the quest, in its own PR), a union over its type parameter,&rawthrough a reference, its own iterators' trait impls, andVec::from_raw_partsof its block; the quest keeps each.Review
Codex
gpt-6.1-solat medium effort (quest review --base origin/main), round 4 onc88a91f9: no findings.Design checkpoint at round 4 (2026-11-02): kept as is. It follows the maintainer's settled typed-allocation decision, and review converged (5, 2, 1, 0 findings). Rejected:
Typing the block at
allocfrom aLayout::array::<T>the body can see. smallvec builds its layout in another function, so this would refuse it.Dropping the PR and keeping allocation refused.
Round 3 on
39a0a393: the lang harness's one-byte selector couldn't reach cases past 254, so this branch'sheapcases and feat(ts): translate pointers into arrays as rt.AtIndex #400'selementscases never ran. Fixed on main by fix(ts): hold a byte vector's capacity as a heap or deque takes it #417, which carried test(ts): pick lang cases by two bytes, so none past 255 is silently skipped #414's two-byte selector. With it merged here, all 263 cases run, andjust cipasses with them agreeing with native wasm32.Round 2 on
cd6c8e3d:MaybeUninit<u8>block freed throughcast::<u8>()was not recognized as freeing. Fixed:freedtakesas,cast, andNonNull::as_ptr, andrt.dealloc/rt.reallocfail with arust2-ts:error for a pointer that is no block.offset_fromthrough a helper's parameters: refuted by the maintainer's decision 1 on feat(ts): translate pointers into arrays as rt.AtIndex #400. A case only visible at run time keeps its loud runtime error.Round 1 on
9fe6969b: five findings, all fixed.is_stdchecks, the uninit copy, and null wrapping were fixed in feat(ts): translate pointers into arrays as rt.AtIndex #400 and merged here. AMaybeUninittype key and trailing bytes were fixed here, each with aheap::remnantsdifferential case.Merged feat(ts): translate pointers into arrays as rt.AtIndex #400's final head and main (unions of both sides' case lists, reject roots,
ts/tests/pointers.ts, the quest Plan, and README's Not-yet sentence; snapshots regenerated;heap.Spill's drops now read throughrt.RawSlice). New session on this checkout's branch, Codexgpt-6.1-solat medium effort:Round 1 on
1d82ffa2: 1 finding, declined for this PR:offset_frombetween byte pointers into twoUint8Arrayviews of one buffer throws, in feat(ts): translate pointers into arrays as rt.AtIndex #400'sptrOffsetFrom(now on main). Loud, recorded inquest/m3/ts-unsafe.md.Round 2 on
3c4b7c8d, round 3 onaa24f669: 1 finding, fixed after round 3: memory from onealloccast to two types in one body is refused at translation, as decision 1 does for array pointers.Round 4 on
43d032cf, round 5 ona816bfb6: the same finding throughrealloc, then throughcastcalls, each fixed. Design checkpoint at round 4: kept; the runtime type check catches every conflicting cast loudly, and translation refuses what one body proves.Round 6 on
72367205: clean.just cipasses locally on7236720555ef532fda79cd188a66d3965c86f5d7.(written by Claude Opus 5.5)
🤖 Generated with Claude Code