From 77de6f0a27f24234224b90b4cabb9adad95cabde Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:41:31 -0800 Subject: [PATCH 01/45] chore(quest): claim quest/m3/ts-unsafe/raw-behind-ref.md Quest-Claim: 4a3efcd012476046 From 3478933bbe33d5efc029a6585612a612fad31710 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:22:39 -0800 Subject: [PATCH 02/45] feat(ts): translate a raw pointer behind a reference A pointer made from a reference is where the body shows the reference borrowed, and else a place of its own: of a shared reference's value, or writing a whole value into the object a &mut is in place. Co-Authored-By: Claude Opus 5.5 --- quest/m3/ts-unsafe/README.md | 1 - quest/m3/ts-unsafe/raw-behind-ref.md | 23 -- ts/README.md | 2 +- ts/fixtures/census/census.md | 17 +- .../lang/conversions/expected/conversions.ts | 14 +- .../lang/pointers/expected/core/bool.ts | 7 + .../lang/pointers/expected/core/option.ts | 35 ++ ts/fixtures/lang/pointers/expected/lib.ts | 4 +- .../pointers/expected/pointers.Drained.ts | 62 ++++ .../lang/pointers/expected/pointers.ts | 289 ++++++++++++++- ts/fixtures/lang/pointers/src/lib.rs | 1 + ts/fixtures/lang/pointers/src/pointers.rs | 153 ++++++++ ts/fixtures/reject/census.txt | 2 +- ts/fixtures/reject/expected.txt | 2 +- ts/fixtures/reject/src/alloc.rs | 8 +- ts/fixtures/reject/ts.toml | 2 +- ts/src/lower/mir/call.rs | 11 + ts/src/lower/mir/origin.rs | 110 ++++-- ts/src/lower/mir/place.rs | 24 +- ts/src/lower/mir/rvalue.rs | 332 ++++++++++++------ ts/src/lower/pointer.rs | 13 +- 21 files changed, 914 insertions(+), 198 deletions(-) delete mode 100644 quest/m3/ts-unsafe/raw-behind-ref.md create mode 100644 ts/fixtures/lang/pointers/expected/pointers.Drained.ts diff --git a/quest/m3/ts-unsafe/README.md b/quest/m3/ts-unsafe/README.md index fbe362cfd..28a7da1ba 100644 --- a/quest/m3/ts-unsafe/README.md +++ b/quest/m3/ts-unsafe/README.md @@ -70,7 +70,6 @@ Public API: the translator's output. ## Required - [A Vec's spare capacity](/quest/m3/ts-unsafe/vec-spare-capacity.md) - writes through `as_mut_ptr` past the length, committed by `set_len`, translate for any `Vec` -- [A raw pointer behind a reference](/quest/m3/ts-unsafe/raw-behind-ref.md) - `&raw const *r` of a value JS copies points where the reference came from - [Vec::from_raw_parts of a typed allocation](/quest/m3/ts-unsafe/vec-from-raw-parts.md) - a `Vec` adopts an `alloc`ed array of `T`s, with its length, capacity, and drops - [Unions over a type parameter](/quest/m3/ts-unsafe/generic-unions.md) - each instance of a generic union is laid out with its own offsets - [Vec::drain from std's MIR](/quest/m3/ts-unsafe/vec-drain-mir.md) - std's `Drain` translates, replacing the runtime's port diff --git a/quest/m3/ts-unsafe/raw-behind-ref.md b/quest/m3/ts-unsafe/raw-behind-ref.md deleted file mode 100644 index 9fecd0d2e..000000000 --- a/quest/m3/ts-unsafe/raw-behind-ref.md +++ /dev/null @@ -1,23 +0,0 @@ -# [M] A raw pointer behind a reference - -## Goal - -[rust2-ts](/ts/README.md) translates `&raw const *r` and `ptr::read(r)` where `r` is a `&T` -to a value JS copies or a library object, so the pointer is where the reference came -from, proven by differential tests against native Rust. Today it is refused: ``a raw -pointer to `T` behind a reference, which is the object and not where it is kept`` -(`ts/src/lower/mir/rvalue.rs`). - -## Plan - -The reference is the value, not where it is kept, so the lowering needs the place the -reference was made from. Since [#576](https://github.com/moq-dev/rust2/pull/576) and -[#584](https://github.com/moq-dev/rust2/pull/584), a slice's reference knows its -`{data, start}`, which is that place for elements. - -At the moq pin it refuses 6 times in the model and 8 in moq-net, all smallvec's `Drain` -(`lib.rs:383`, `397`) and `retain` (`1733`, `1740`), plus moq-net's own -`ietf/parameters.rs:14,40` num_enum tag reads, which are also byte reinterpretation and -stay refused. - -Public API: the translator's output. diff --git a/ts/README.md b/ts/README.md index d7717fd16..189d795e6 100644 --- a/ts/README.md +++ b/ts/README.md @@ -109,7 +109,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: 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 string it owns or an `rs.Borrowed` holding the one it borrows, which `rs.strCowStr` reads | | `&T`, `&mut T`, `Box`, `Reverse`, `NonZero`, `AssertUnwindSafe` | the value; a `&mut` to a value JS copies, such as a number, an `Option`, a `String`, or a reference, or to an `Arc`, a `Waker`, or a `LocalKey`, whose copies share what it points at, is an `rs.Place`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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, 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 `*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 | +| `*const T`, `*mut T`, `NonNull` | 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` 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, 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or writing a whole value into the object a `&mut` is in place. 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`: `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 | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/census/census.md b/ts/fixtures/census/census.md index e7587fd63..633cc4d92 100644 --- a/ts/fixtures/census/census.md +++ b/ts/fixtures/census/census.md @@ -1,6 +1,6 @@ # Census of `census-fixture` -- Refusals: 12 +- Refusals: 9 - Function instances: 6 - Generic functions no instance reached, lowered over their type parameters: 1 @@ -8,7 +8,6 @@ | Count | Construct | First | | ---: | --- | --- | -| 3 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | fixtures/census/src/lib.rs:9:2 | | 2 | a cast from `_` to `_`, between a pointer and an integer | fixtures/census/src/lib.rs:9:2 | | 2 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | fixtures/census/src/lib.rs:19:12 | | 2 | a mutable static, which any code may write | fixtures/census/src/lib.rs:38:3 | @@ -22,38 +21,28 @@ The type or module each refused type or path belongs to. | Count | API | First | | ---: | --- | --- | -| 4 | `u32` | fixtures/census/src/lib.rs:9:2 | -| 2 | `u64` | fixtures/census/src/lib.rs:19:47 | +| 2 | `u32` | fixtures/census/src/lib.rs:19:12 | | 2 | `u8` | fixtures/census/src/lib.rs:19:12 | | 2 | `usize` | fixtures/census/src/lib.rs:9:2 | | 1 | `*const T` | fixtures/census/src/lib.rs:14:2 | | 1 | `*const u32` | fixtures/census/src/lib.rs:9:2 | | 1 | `Bits` | fixtures/census/src/lib.rs:28:1 | | 1 | `std::sync::Arc` | fixtures/census/src/lib.rs:45:2 | +| 1 | `u64` | fixtures/census/src/lib.rs:19:47 | ## By type or path | Count | Construct | Type or path | First | | ---: | --- | --- | --- | -| 2 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | `u32` | fixtures/census/src/lib.rs:9:2 | | 1 | `Bytes::from_owner` of `_`, which would drop with the last clone of what it makes, which is not supported | `std::sync::Arc` | fixtures/census/src/lib.rs:45:2 | | 1 | a cast from `_` to `_`, between a pointer and an integer | `*const T`, `usize` | fixtures/census/src/lib.rs:14:2 | | 1 | a cast from `_` to `_`, between a pointer and an integer | `*const u32`, `usize` | fixtures/census/src/lib.rs:9:2 | | 1 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | `u32`, `u8` | fixtures/census/src/lib.rs:19:12 | | 1 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | `u64`, `u8` | fixtures/census/src/lib.rs:19:47 | -| 1 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | `u64` | fixtures/census/src/lib.rs:19:48 | | 1 | union `_`, whose fields overlap in `_` and `f64`, which JS keeps apart | `Bits`, `u32` | fixtures/census/src/lib.rs:28:1 | ## Spans -
a raw pointer to `_` behind a reference, which is the object and not where it is kept (3) - -- fixtures/census/src/lib.rs:9:2: a raw pointer to `u32` behind a reference, which is the object and not where it is kept -- fixtures/census/src/lib.rs:19:13: a raw pointer to `u32` behind a reference, which is the object and not where it is kept -- fixtures/census/src/lib.rs:19:48: a raw pointer to `u64` behind a reference, which is the object and not where it is kept - -
-
a cast from `_` to `_`, between a pointer and an integer (2) - fixtures/census/src/lib.rs:9:2: a cast from `*const u32` to `usize`, between a pointer and an integer diff --git a/ts/fixtures/lang/conversions/expected/conversions.ts b/ts/fixtures/lang/conversions/expected/conversions.ts index 6059901f0..d6504be04 100644 --- a/ts/fixtures/lang/conversions/expected/conversions.ts +++ b/ts/fixtures/lang/conversions/expected/conversions.ts @@ -2100,7 +2100,7 @@ export function converted$Option_u16(data: rs.ByteSlice, out: rs.ByteVec): void put$conversions_Value_Option_u16(_199, out); } boxed$Option_u16(data, out); - put$conversions_Value_Option_u16({ v: b }.v, out); + put$conversions_Value_Option_u16(b, out); c = make$conversions_Value_Option_u16(data, 16); put$conversions_Value_Option_u16(c, out); } @@ -2187,7 +2187,7 @@ export function converted$String(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_String(_199, out); } boxed$String(data, out); - put$conversions_Value_String({ v: b }.v, out); + put$conversions_Value_String(b, out); c = make$conversions_Value_String(data, 16); put$conversions_Value_String(c, out); } @@ -2275,7 +2275,7 @@ export function converted$Vec_u32(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_Vec_u32(_199, out); } boxed$Vec_u32(data, out); - put$conversions_Value_Vec_u32({ v: b }.v, out); + put$conversions_Value_Vec_u32(b, out); c = make$conversions_Value_Vec_u32(data, 16); put$conversions_Value_Vec_u32(c, out); } @@ -2363,7 +2363,7 @@ export function converted$Vec_u8(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_Vec_u8(_199, out); } boxed$Vec_u8(data, out); - put$conversions_Value_Vec_u8({ v: b }.v, out); + put$conversions_Value_Vec_u8(b, out); c = make$conversions_Value_Vec_u8(data, 16); put$conversions_Value_Vec_u8(c, out); } @@ -3198,7 +3198,7 @@ export function converted$u32(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u32(_199, out); } boxed$u32(data, out); - put$conversions_Value_u32({ v: b }.v, out); + put$conversions_Value_u32(b, out); c = make$conversions_Value_u32(data, 16); put$conversions_Value_u32(c, out); } @@ -3285,7 +3285,7 @@ export function converted$u64(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u64(_199, out); } boxed$u64(data, out); - put$conversions_Value_u64({ v: b }.v, out); + put$conversions_Value_u64(b, out); c = make$conversions_Value_u64(data, 16); put$conversions_Value_u64(c, out); } @@ -3370,7 +3370,7 @@ export function converted$u8(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u8(_199, out); } boxed$u8(data, out); - put$conversions_Value_u8({ v: b }.v, out); + put$conversions_Value_u8(b, out); c = make$conversions_Value_u8(data, 16); put$conversions_Value_u8(c, out); } diff --git a/ts/fixtures/lang/pointers/expected/core/bool.ts b/ts/fixtures/lang/pointers/expected/core/bool.ts index e694d25bc..df98e8987 100644 --- a/ts/fixtures/lang/pointers/expected/core/bool.ts +++ b/ts/fixtures/lang/pointers/expected/core/bool.ts @@ -3,6 +3,13 @@ import * as rs from "../rs.ts"; import type * as Spot from "../pointers.Spot.ts"; +export function thenSome$1gvhopg(self: boolean, t: rs.u16): rs.u16 | undefined { + if (!self) { + return undefined; + } + return t; +} + export function then$0bgt67i(self: boolean, f: () => rs.Weak): rs.Weak | undefined { if (!self) { return undefined; diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 8f6edbacb..9d27f5111 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -48,6 +48,27 @@ export function unwrapOr$1fr498u(self: rs.u8 | undefined, default_: rs.u8): rs.u return self; } +export function map$00buaky(self: rs.u16 | undefined | rs.None, f: (a0: rs.u16 | undefined) => rs.u16 | undefined): rs.u16 | undefined | rs.None { + if (self === rs.None) { + return rs.None; + } + return f(self); +} + +export function map$18tou5q(self: string | undefined, f: (a0: string) => string): string | undefined { + if (self === undefined) { + return undefined; + } + return f(self); +} + +export function map$1h9tk16(self: rs.ByteVec | undefined, f: (a0: rs.ByteVec) => rs.ByteVec): rs.ByteVec | undefined { + if (self === undefined) { + return undefined; + } + return f(self); +} + export function map$1cfrse5(self: Loaded.Loaded | undefined, f: (a0: Loaded.Loaded) => Loaded.Loaded): Loaded.Loaded | undefined { if (self === undefined) { return undefined; @@ -55,6 +76,13 @@ export function map$1cfrse5(self: Loaded.Loaded | undefined, f: (a0: Loaded.Load return f(self); } +export function map$070fw7m(self: rs.u32 | undefined, f: (a0: rs.u32) => rs.u32): rs.u32 | undefined { + if (self === undefined) { + return undefined; + } + return f(self); +} + export function mapOr$078vnqn(self: Pair.Pair | undefined, default_: rs.u8, f: (a0: Pair.Pair) => rs.u8): rs.u8 { if (self === undefined) { return default_; @@ -76,6 +104,13 @@ export function mapOr$173j2gr(self: [rs.u8, rs.Arc] | undefined, default_ return f(self); } +export function mapOr$0smr0aj(self: rs.u16 | undefined, default_: Uint8Array, _: (a0: rs.u16) => Uint8Array): Uint8Array { + if (self === undefined) { + return default_; + } + return rs.u16ToBeBytes(self); +} + export function filter$0wvwd70(self: rs.u32 | undefined, predicate: (a0: rs.u32) => boolean): rs.u32 | undefined { if (self !== undefined) { const x: rs.u32 = self; diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index e38684560..f90105b1a 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays } from "./pointers.ts"; +import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, behindRefs } from "./pointers.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -289,6 +289,8 @@ export function cases(): rs.Vec<[string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.By rawPlaces(data26, out26); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::arrays", (_236: rs.u8, data27: rs.ByteSlice, out27: rs.ByteVec): undefined => { arrays(data27, out27); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::behind_refs", (_237: rs.u8, data28: rs.ByteSlice, out28: rs.ByteVec): undefined => { + behindRefs(data28, out28); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts new file mode 100644 index 000000000..b49b63d8b --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts @@ -0,0 +1,62 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; +import { map$00buaky as option_map$00buaky, map$18tou5q as option_map$18tou5q, map$1h9tk16 as option_map$1h9tk16, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; + +/** + * What smallvec's `Drain` is: the elements of a slice nothing will drop again, each read + * out through the reference its iterator lends. + */ +export interface Drained { + iter: rs.Iter>; +} + +export function next$iter_Iterator_Option_u16(self: Drained): rs.u16 | undefined | rs.Done { + return rs.iterFromOption(option_map$00buaky(rs.iterToOption((self.iter as rs.Iter).next(), rs.None), (r: rs.u16 | undefined): rs.u16 | undefined => { + return { v: r }.v; + }), rs.None); +} + +export function next$iter_Iterator_String(self: Drained): string | rs.Done { + return rs.iterFromOption(option_map$18tou5q(rs.iterToOption((self.iter as rs.Iter).next()), (r: string): string => { + return { v: r }.v; + })); +} + +export function next$iter_Iterator_Vec_u8(self: Drained): rs.ByteVec | rs.Done { + return rs.iterFromOption(option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.ByteVec): rs.ByteVec => { + return { v: r }.v; + })); +} + +export function next$iter_Iterator_u32(self: Drained): rs.u32 | rs.Done { + return rs.iterFromOption(option_map$070fw7m(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.u32): rs.u32 => { + return { v: r }.v; + })); +} + +export function nextBack$iter_DoubleEndedIterator_Option_u16(self: Drained): rs.u16 | undefined | rs.None { + return option_map$00buaky(rs.iterToOption((self.iter as rs.Iter).nextBack(), rs.None), (r: rs.u16 | undefined): rs.u16 | undefined => { + return { v: r }.v; + }); +} + +export function nextBack$iter_DoubleEndedIterator_String(self: Drained): string | undefined { + return option_map$18tou5q(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: string): string => { + return { v: r }.v; + }); +} + +export function nextBack$iter_DoubleEndedIterator_Vec_u8(self: Drained): rs.ByteVec | undefined { + return option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.ByteVec): rs.ByteVec => { + return { v: r }.v; + }); +} + +export function nextBack$iter_DoubleEndedIterator_u32(self: Drained): rs.u32 | undefined { + return option_map$070fw7m(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.u32): rs.u32 => { + return { v: r }.v; + }); +} + +//# sourceMappingURL=pointers.Drained.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 27c5d87df..990b05a91 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -2,14 +2,15 @@ import * as rs from "./rs.ts"; import { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_Twin, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; -import { then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; -import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$173j2gr as option_mapOr$173j2gr, filter$0wvwd70 } from "./core/option.ts"; +import { thenSome$1gvhopg, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; +import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; import * as ByClosure from "./pointers.ByClosure.ts"; import type * as ByObject from "./pointers.ByObject.ts"; import * as Counted from "./pointers.Counted.ts"; +import * as Drained from "./pointers.Drained.ts"; import type * as Duo from "./pointers.Duo.ts"; import type * as Enclosed from "./pointers.Enclosed.ts"; import type * as Gauge from "./pointers.Gauge.ts"; @@ -2980,4 +2981,288 @@ export function lent(n: rs.u8): rs.ByteVec { return o; } +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$Option_u16(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.u16 | undefined | rs.None; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_Option_u16(iter), rs.None); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_Option_u16(iter); + } + if (next !== rs.None) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: string | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_String(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_String(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$Vec_u8(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.ByteVec | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_Vec_u8(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_Vec_u8(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$u32(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.u32 | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_u32(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_u32(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$String_pointers_behind_refs_closure6(v: rs.Vec, keep: (a0: rs.Place) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place = rs.arrayAsPtr(v.items); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15)) { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSplitOff(v, write); +} + +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$Vec_u8_pointers_behind_refs_closure8(v: rs.Vec, keep: (a0: rs.ByteVec) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place = rs.arrayAsPtr(v.items); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15.v)) { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSplitOff(v, write); +} + +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$u32_pointers_behind_refs_closure4(v: rs.Vec, keep: (a0: rs.Place) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place = rs.arrayAsPtr(v.items); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15)) { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSplitOff(v, write); +} + +/** The length of the `Vec` a pointer from a reference the body is given points at. */ +export function vecLen(v: rs.ByteVec): rs.usize { + return { v }.v.length; +} + +/** + * `with` written whole through a pointer from a `&mut` the body is given, made each way + * std makes one, which its caller's `Vec` holds after. + */ +export function refilled(v: rs.ByteVec, with_: rs.ByteVec, how: rs.u8): void { + let p!: rs.Place; + const _5: rs.u8 = rs.u8Rem(how, 4); + if (_5 === 0) { + const target4 = v; + p = rs.place(() => target4, (value4) => Object.assign(target4, value4)); + } else if (_5 === 1) { + const target3 = v; + p = rs.place(() => target3, (value3) => Object.assign(target3, value3)); + } else if (_5 === 2) { + const target2 = v; + p = rs.place(() => target2, (value2) => Object.assign(target2, value2)); + } else { + const target = v; + p = rs.place(() => target, (value) => Object.assign(target, value)); + } + p.v = with_; + rs.byteVecPush(p.v, how); +} + +/** + * The value a pointer from a shared reference the body is given points at, made each way + * std makes one. + */ +export function behind(r: rs.u32, how: rs.u8): rs.u32 { + let p!: rs.Place; + const _4: rs.u8 = rs.u8Rem(how, 3); + if (_4 === 0) { + p = { v: r }; + } else if (_4 === 1) { + p = { v: r }; + } else { + p = { v: r }; + } + return p.v; +} + +/** + * Raw pointers from references: smallvec's `Drain` and `retain`, over values JS copies, a + * library type, and an `Option`; pointers from a reference the body borrowed, which move + * within the array it borrowed from; and from references the body is given, to a number + * and to a `Vec`, read and written whole. + */ +export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { + let _112!: rs.usize; + const bits: rs.u8 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0); + const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): rs.u32 => { + return rs.u32Mul(_2 as rs.u32, 3); + }), undefined, 4, true); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(drained$u32(rs.vecFrom(nums.items.slice()), bits).items), (_23: rs.u32): rs.u8 => { + return _23 & 0xff; + }), undefined, true); + const words: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b3: rs.u8): string => { + return rs.fmtToString((f: rs.Formatter) => rs.u8Display(b3, f)); + }), undefined, 12, true); + for (const _25 of drained$String(rs.vecFrom(words.items.slice()), bits).items) { + rs.iterExtendBytes(out, rs.strBytes(_25), undefined, true); + } + const runs: rs.Vec = rs.iterToVec(rs.iterMap(rs.byteSliceChunks(data, 3), (a0: rs.ByteSlice): rs.ByteVec => { + return rs.byteSliceToByteVec(a0); + }), undefined, 12, true); + for (const r of drained$Vec_u8(rs.vecFrom(runs.items.map((a02: rs.ByteVec): rs.ByteVec => { + return rs.byteVecFrom(rs.byteVecView(a02)); + })), bits).items) { + rs.byteVecPush(out, r.length & 0xff); + rs.iterExtendBytes(out, rs.byteVecIntoIter(r), undefined, true); + } + for (const _56 of drained$Option_u16(rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_24: rs.u8): rs.u16 | undefined => { + const b4: rs.u8 = _24; + return thenSome$1gvhopg(rs.u8Rem(b4, 3) !== 0, b4 as rs.u16); + }), undefined, 4, true), bits).items) { + rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0smr0aj(_56, Uint8Array.of(255, 255), (a03: rs.u16): Uint8Array => { + return rs.u16ToBeBytes(a03); + })), undefined, true); + } + const nums2: rs.Vec = nums; + retained$u32_pointers_behind_refs_closure4(nums2, (n2: rs.Place): boolean => { + n2.v = rs.u32Add(n2.v, 1); + return rs.u32Rem(n2.v, 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(nums2.items), (_26: rs.u32): rs.u8 => { + return _26 & 0xff; + }), undefined, true); + const words2: rs.Vec = words; + retained$String_pointers_behind_refs_closure6(words2, (w2: rs.Place): boolean => { + w2.v = w2.v + "!"; + return rs.usizeRem(rs.stringLen(w2.v), 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w3: string): rs.Iter => { + return rs.strBytes(w3); + }, undefined, true), "next"); + const runs2: rs.Vec = runs; + retained$Vec_u8_pointers_behind_refs_closure8(runs2, (r2: rs.ByteVec): boolean => { + rs.byteVecPush(r2, 7); + return rs.u8Rem(r2.data[rs.idx(r2, 0)], 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterFlatten(rs.iterSlice(runs2.items), (value: rs.ByteVec) => rs.iterSlice(rs.byteVecView(value)), true), "next"); + const nums3: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_27: rs.u8): rs.u32 => { + return _27 as rs.u32; + }), undefined, 4, true); + if (rs.sliceFrom(nums3.items).length >= 2) { + const first: rs.Place = rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 0)); + rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, rs.usizeSub(nums3.items.length, 1))), first) & 0xff); + const _109: rs.usize = nums3.items.length; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = _109; + while ((_112 = iterStart++) < iterEnd) { + rs.byteVecPush(out, rs.ptrAdd(first, _112).v & 0xff); + } + rs.byteVecPush(out, rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v & 0xff); + rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v = 9; + rs.byteVecPush(out, nums3.items[rs.idx(nums3.items, 0)] & 0xff); + } + const v: rs.ByteVec = rs.byteSliceToByteVec(data); + rs.byteVecPush(out, vecLen(v) & 0xff); + const _143: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); + refilled(v, _143, bits); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); + const iter5: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); + while (true) { + const _153: [rs.usize, rs.u8] | rs.Done = iter5.next(); + if (_153 === rs.DONE) { + break; + } + rs.byteVecPush(out, behind(_153[1] as rs.u32, _153[0] & 0xff) & 0xff); + } +} + //# sourceMappingURL=pointers.ts.map diff --git a/ts/fixtures/lang/pointers/src/lib.rs b/ts/fixtures/lang/pointers/src/lib.rs index 28baff2b1..ef925469f 100644 --- a/ts/fixtures/lang/pointers/src/lib.rs +++ b/ts/fixtures/lang/pointers/src/lib.rs @@ -49,5 +49,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("pointers::raw_arcs", |_, data, out| pointers::raw_arcs(data, out)), ("pointers::raw_places", |_, data, out| pointers::raw_places(data, out)), ("pointers::arrays", |_, data, out| pointers::arrays(data, out)), + ("pointers::behind_refs", |_, data, out| pointers::behind_refs(data, out)), ] } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 10bfc8ffd..c4428c980 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2409,3 +2409,156 @@ fn lent(n: u8) -> Vec { struct Holder { arr: [u32; 2], } + +/// What smallvec's `Drain` is: the elements of a slice nothing will drop again, each read +/// out through the reference its iterator lends. +struct Drained<'a, T> { + iter: std::slice::Iter<'a, T>, +} + +impl Iterator for Drained<'_, T> { + type Item = T; + + fn next(&mut self) -> Option { + self.iter.next().map(|r| unsafe { ptr::read(r) }) + } +} + +impl DoubleEndedIterator for Drained<'_, T> { + fn next_back(&mut self) -> Option { + self.iter.next_back().map(|r| unsafe { ptr::read(r) }) + } +} + +/// `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. +fn drained(v: Vec, bits: u8) -> Vec { + let v = mem::ManuallyDrop::new(v); + let mut iter = Drained { iter: v.iter() }; + let mut out = Vec::new(); + let mut i = 0; + loop { + let next = match bits >> (i % 8) & 1 { + 0 => iter.next(), + _ => iter.next_back(), + }; + let Some(next) = next else { break }; + out.push(next); + i += 1; + } + out +} + +/// What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then +/// drops or moves to the front. +fn retained(v: &mut Vec, mut keep: impl FnMut(&mut T) -> bool) { + let len = v.len(); + let p = v.as_mut_ptr(); + let mut write = 0; + for read in 0..len { + let cur = unsafe { &mut *p.add(read) }; + if !keep(cur) { + unsafe { ptr::drop_in_place(cur) }; + } else { + unsafe { ptr::copy(cur, p.add(write), 1) }; + write += 1; + } + } + // What `set_len` leaves past the length, which nothing drops again. + mem::forget(v.split_off(write)); +} + +/// The length of the `Vec` a pointer from a reference the body is given points at. +fn vec_len(v: &Vec) -> usize { + let p = &raw const *v; + unsafe { (*p).len() } +} + +/// `with` written whole through a pointer from a `&mut` the body is given, made each way +/// std makes one, which its caller's `Vec` holds after. +fn refilled(v: &mut Vec, with: Vec, how: u8) { + let p = match how % 4 { + 0 => v as *mut Vec, + 1 => ptr::from_mut(v), + 2 => NonNull::from(v).as_ptr(), + _ => Into::>>::into(v).as_ptr(), + }; + unsafe { *p = with }; + unsafe { (*p).push(how) }; +} + +/// The value a pointer from a shared reference the body is given points at, made each way +/// std makes one. +fn behind(r: &u32, how: u8) -> u32 { + let p = match how % 3 { + 0 => r as *const u32, + 1 => ptr::from_ref(r), + _ => NonNull::from(r).as_ptr(), + }; + unsafe { ptr::read(p) } +} + +/// Raw pointers from references: smallvec's `Drain` and `retain`, over values JS copies, a +/// library type, and an `Option`; pointers from a reference the body borrowed, which move +/// within the array it borrowed from; and from references the body is given, to a number +/// and to a `Vec`, read and written whole. +pub fn behind_refs(data: &[u8], out: &mut Vec) { + let bits = data.first().copied().unwrap_or(0); + let nums: Vec = data.iter().map(|&b| u32::from(b) * 3).collect(); + out.extend(drained(nums.clone(), bits).iter().map(|&n| n as u8)); + let words: Vec = data.iter().map(|b| b.to_string()).collect(); + for w in drained(words.clone(), bits) { + out.extend(w.bytes()); + } + let runs: Vec> = data.chunks(3).map(<[u8]>::to_vec).collect(); + for r in drained(runs.clone(), bits) { + out.push(r.len() as u8); + out.extend(r); + } + let opts: Vec> = data.iter().map(|&b| (b % 3 != 0).then_some(u16::from(b))).collect(); + for o in drained(opts, bits) { + out.extend(o.map_or([0xff, 0xff], u16::to_be_bytes)); + } + + let mut nums = nums; + retained(&mut nums, |n| { + *n += 1; + *n % 2 == 0 + }); + out.extend(nums.iter().map(|&n| n as u8)); + let mut words = words; + retained(&mut words, |w| { + w.push('!'); + w.len() % 2 == 0 + }); + out.extend(words.iter().flat_map(|w| w.bytes())); + let mut runs = runs; + retained(&mut runs, |r| { + r.push(7); + r[0] % 2 == 0 + }); + out.extend(runs.iter().flatten()); + + // From a reference the body borrowed, where the element is. + let mut nums: Vec = data.iter().map(|&b| u32::from(b)).collect(); + if let [_, _, ..] = nums[..] { + let first = &nums[0] as *const u32; + let last = &nums[nums.len() - 1] as *const u32; + out.push(unsafe { last.offset_from(first) } as u8); + for i in 0..nums.len() { + out.push(unsafe { *first.add(i) } as u8); + } + let second = ptr::from_ref(&nums[1]); + out.push(unsafe { *second.sub(1) } as u8); + let m = &mut nums[1] as *mut u32; + unsafe { *m.sub(1) = 9 }; + out.push(nums[0] as u8); + } + + let mut v = data.to_vec(); + out.push(vec_len(&v) as u8); + refilled(&mut v, data.iter().rev().copied().collect(), bits); + out.extend(&v); + for (i, &b) in data.iter().enumerate() { + out.push(behind(&u32::from(b), i as u8) as u8); + } +} diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index d7acc7b88..3d5a1563e 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -10,8 +10,8 @@ fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another -fixtures/reject/src/alloc.rs:vec_pointer+1:10: a raw pointer to `std::vec::Vec` behind a reference, which is the object and not where it is kept fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:13: unsupported call to `std::alloc::alloc_zeroed` fixtures/reject/src/buffers.rs:blank_bytes+1:2: `Buf::copy_to_bytes` of `buffers::Blank`, a value JS copies, which is not supported diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index fb9f70ed7..4e629dde7 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -14,8 +14,8 @@ fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another -fixtures/reject/src/alloc.rs:vec_pointer+1:10: a raw pointer to `std::vec::Vec` behind a reference, which is the object and not where it is kept fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:13: unsupported call to `std::alloc::alloc_zeroed` fixtures/reject/src/buffers.rs:blank_bytes+1:2: `Buf::copy_to_bytes` of `buffers::Blank`, a value JS copies, which is not supported diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index dd4e03d8b..d09a28cc9 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -24,10 +24,10 @@ pub fn allocated() -> u8 { } } -/// A pointer to a `Vec`, whose JS array is its heap, not where Rust keeps the `Vec`. -pub fn vec_pointer(v: &mut Vec) -> usize { - let p = &raw const *v; - unsafe { (*p).len() } +/// A pointer from a reference the body is given, which JS keeps in no array, moved past +/// it. +pub fn past_reference(r: &u32) -> u32 { + unsafe { *(r as *const u32).add(2) } } /// Casts between pointers to bytes and to values outside memory from `alloc`, which read diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index b5754cba1..5116be58e 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -4,8 +4,8 @@ roots = [ "alloc::alloc_retyped", "alloc::allocated", "alloc::bytes_as_values", + "alloc::past_reference", "alloc::reinterpreted", - "alloc::vec_pointer", "alloc::zeroed", "bound_ranges", "buffers::blank_bytes", diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index b32ff4e28..b52faf9df 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -533,6 +533,17 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { { done(p, false) } + // A pointer from a reference to a value JS keeps in a place, as `&raw const *r` makes. + ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") + if let Some(r) = args[0].node.place() + && crate::lower::pointer::raw_pointee(tcx, dest.ty(&self.mir.local_decls, tcx).ty) + .is_some_and(|pointee| self.behind_place(r, pointee)) => + { + if let Some(why) = self.made_out_of_step(def_id, gargs, &args[0].node) { + return done(self.err(span, why), false); + } + done(self.behind_reference(r, span, out), false) + } // A place of no bytes has nothing to read or write, and every value of its type // is the one it holds. What made the `&mut` still runs, as its unread temporary. ("mem", "take" | "replace" | "swap") if self.b.t.zero_sized(targ(0)?) => match name.as_str() { diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 33fd92e18..3a91b89df 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -194,36 +194,27 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { op, _, ) if self.same_pointee(op, local) => self.origin(op), - Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => { - match place.projection.as_slice() { - // `&raw mut *p` is `p`. - [ProjectionElem::Deref] => self.origin(&Operand::Copy(mir::Place::from(place.local))), - [.., ProjectionElem::Deref] => None, - [ - array @ .., - ProjectionElem::Index(_) | ProjectionElem::ConstantIndex { .. }, - ] => (!place.projection.contains(&ProjectionElem::Deref)).then(|| Origin { - array: Array::Owned(mir::Place { - local: place.local, - projection: self.tcx.mk_place_elems(array), - }), - index: None, - }), - _ => Some(Origin { - array: Array::Lone(Location { - block: bb, - statement_index: i, - }), - index: Some(0), - }), - } - } + Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_origin( + *place, + Location { + block: bb, + statement_index: i, + }, + ), _ => None, }; } let (def_id, _, args) = self.def_call(local)?; let (owner, name) = library::key(self.tcx, def_id)?; match (owner.as_str(), name.as_str()) { + ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") => { + let block = self.def_block(local)?; + let at = Location { + block, + statement_index: self.mir.basic_blocks[block].statements.len(), + }; + self.reference_origin(args[0].node.place()?, at) + } ("ptr" | "NonNull", "add" | "sub" | "offset") => { Some(self.moved(self.origin(&args[0].node)?, name.as_str(), &args[1].node)) } @@ -233,6 +224,77 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { } } + /// Where a raw pointer to `place`, made at `at`, points, if the body shows it. + fn raw_origin(&self, place: mir::Place<'tcx>, at: Location) -> Option> { + match place.projection.as_slice() { + // `&raw mut *p` is `p`. + [ProjectionElem::Deref] if self.mir.local_decls[place.local].ty.is_raw_ptr() => { + self.origin(&Operand::Copy(mir::Place::from(place.local))) + } + [rest @ .., ProjectionElem::Deref] => self.reference_origin( + mir::Place { + local: place.local, + projection: self.tcx.mk_place_elems(rest), + }, + at, + ), + [ + array @ .., + ProjectionElem::Index(_) | ProjectionElem::ConstantIndex { .. }, + ] => (!place.projection.contains(&ProjectionElem::Deref)).then(|| Origin { + array: Array::Owned(mir::Place { + local: place.local, + projection: self.tcx.mk_place_elems(array), + }), + index: None, + }), + _ => Some(Origin { + array: Array::Lone(at), + index: Some(0), + }), + } + } + + /// Where a raw pointer to what the reference `r` points at, made at `at`, points, as + /// [`Self::behind_reference`] makes it: where the body shows `r` borrowed, and else at + /// a place of its own, or, where `r` is an `rs.Place`, where that points. + fn reference_origin(&self, r: mir::Place<'tcx>, at: Location) -> Option> { + let ty = r.ty(&self.mir.local_decls, self.tcx).ty; + if !ty.is_ref() { + return None; + } + let pointee = self.tcx.mk_place_deref(r); + let borrowed = self.through_aliases(pointee); + if borrowed != pointee { + return self.raw_origin(borrowed, at); + } + if self.indexed(r).is_some() { + let (def_id, _, args) = self.def_call(r.as_local()?)?; + let (owner, name) = library::key(self.tcx, def_id)?; + if !matches!( + (owner.as_str(), name.as_str()), + ("Index" | "IndexMut", "index" | "index_mut") + ) { + return None; + } + let o = self.start(&args[0].node)?; + return Some(Origin { + index: o.index.zip(self.const_int(&args[1].node)).map(|(i, n)| i + n), + ..o + }); + } + if let Some(p) = self.reborrowed_pointer(r) { + return self.origin(&Operand::Copy(p)); + } + if self.b.t.place_pointee(ty).is_some() { + return None; + } + Some(Origin { + array: Array::Lone(at), + index: Some(0), + }) + } + /// `o` moved by the offset `op` as `add`, `sub`, or `offset` moves it. fn moved(&self, o: Origin<'tcx>, name: &str, op: &Operand<'tcx>) -> Origin<'tcx> { let n = self.const_int(op).map(|n| match name { diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index f93619b65..1f3e882b7 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -819,15 +819,8 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { matches!(self.mir.local_decls[place.local].ty.kind(), ty::RawPtr(..)) && !self.box_pointer(place.local); let e = match raw { true => Expr::rs("ptrWrite", vec![target, value]), - false => match self.b.t.write_in(pointee) { - Some(ObjectKind::Bytes) => target.member("set").call(vec![value]), - // A boxed slice is a JS array, whose length a write changes, unlike a - // tuple's or an array's. - Some(ObjectKind::Array) if self.b.t.erased(pointee).is_slice() => { - Expr::rs("arrayAssign", vec![target, value]) - } - Some(_) if self.b.t.moves_private(pointee) => Expr::rs("ptrWrite", vec![target, value]), - Some(_) => Expr::ident("Object").member("assign").call(vec![target, value]), + false => match self.write_into(target, value, pointee) { + Some(e) => e, None => self.err(span, format!("an assignment through a reference to `{pointee}`")), }, }; @@ -840,6 +833,19 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { out } + /// The write of `value`, a whole `pointee`, into `target`, the object a reference to it + /// is, in place, if JS can write one so. + pub(super) fn write_into(&self, target: Expr, value: Expr, pointee: Ty<'tcx>) -> Option { + Some(match self.b.t.write_in(pointee)? { + ObjectKind::Bytes => target.member("set").call(vec![value]), + // A boxed slice is a JS array, whose length a write changes, unlike a tuple's or + // an array's. + ObjectKind::Array if self.b.t.erased(pointee).is_slice() => Expr::rs("arrayAssign", vec![target, value]), + _ if self.b.t.moves_private(pointee) => Expr::rs("ptrWrite", vec![target, value]), + _ => Expr::ident("Object").member("assign").call(vec![target, value]), + }) + } + /// `target = value`, where `target` may be a struct's byte slice field held as its /// parts: each of its fields gets its part, in order, read before any is written where /// a part reads a field written before it. diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index 6d127f34b..593621f28 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -70,114 +70,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { // A bounds check of a `&mut` slice reads its length through a raw pointer that // only `PtrMetadata` reads, which reads the slice's. Rvalue::RawPtr(mir::RawPtrKind::FakeForPtrMetadata, place) => self.place_expr(*place, span, out), - Rvalue::RawPtr(_, place) => { - if self.union_whole(*place, span).is_some() { - return Some(self.err(span, "a pointer to a union's field kept in more than one slot")); - } - if let Some(why) = self.out_of_step(*place, false) { - return Some(self.err(span, why)); - } - let pointee = place.ty(&self.mir.local_decls, tcx).ty; - match self.b.t.pointee(pointee) { - Pointee::Object => self.place_expr(*place, span, out), - // `&raw mut *p` of a raw pointer is `p`; of a reference, the slice's start and - // length. - Pointee::Slice => match place.projection.as_slice() { - [rest @ .., mir::ProjectionElem::Deref] - if Place::ty_from(place.local, rest, &self.mir.local_decls, tcx) - .ty - .is_raw_ptr() => - { - let base = Place { - local: place.local, - projection: tcx.mk_place_elems(rest), - }; - self.place_expr(base, span, out) - } - _ => { - let value = self.place_expr(*place, span, out); - match crate::lower::pointer::slice_of_bytes(pointee) { - true => Expr::rs("rawByteSliceOf", vec![value]), - false => Expr::rs("rawSliceOf", vec![value]), - } - } - }, - // A slice of all of an array, as a borrow of it is. - Pointee::Array => { - if let Some(e) = self.array_subslice(mir::BorrowKind::Shared, *place, span, out) { - return Some(e); - } - let value = self.place_expr(*place, span, out); - match self.in_array(*place) { - true => value, - false => self.b.t.array_rs(pointee, "From").call(vec![value]), - } - } - // `&raw mut *p` is `p`. A pointer to `()` is an identity, which only another - // pointer is: not the `rs.Place` of a place or of a `&mut`, each borrow's own. - Pointee::Place => match place.projection.as_slice() { - [rest @ .., mir::ProjectionElem::Deref] - if !self.box_pointer(place.local) && { - let base = Place::ty_from(place.local, rest, &self.mir.local_decls, tcx).ty; - match self.b.t.identity_pointee(pointee) { - true => base.is_raw_ptr(), - false => self.b.t.place_pointee(base).is_some(), - } - } => - { - let base = Place { - local: place.local, - projection: tcx.mk_place_elems(rest), - }; - self.place_expr(base, span, out) - } - // A reference to an `Arc`'s value, which is that value's one place. - [mir::ProjectionElem::Deref] - if let Some(p) = self.arc_raw(&Operand::Copy(Place::from(place.local)), span, out) => - { - p - } - _ if self.b.t.identity_pointee(pointee) => self.err( - span, - "a raw pointer to a place of `()`, which has no address of its own", - ), - // A reference to an object is the object, not the place holding it. - [.., mir::ProjectionElem::Deref] if !self.box_pointer(place.local) => self.err( - span, - format!( - "a raw pointer to `{pointee}` behind a reference, which is the object and not where it is kept" - ), - ), - // A pointer to an element is where it is in its array, which pointer - // arithmetic moves within. - [ - array @ .., - mir::ProjectionElem::Index(_) | mir::ProjectionElem::ConstantIndex { .. }, - ] => { - // It points into the array, which must stay where it is. - let array = Place { - local: place.local, - projection: tcx.mk_place_elems(array), - }; - if let Some(why) = self.out_of_step(array, false) { - return Some(self.err(span, why)); - } - let target = self.place_expr(*place, span, out); - let target = self.stable_target(*place, target, span, out); - match target.unlocated() { - Expr::Index(array, index) => { - Expr::rs("arrayAsPtr", vec![*array.clone(), *index.clone()]) - } - _ => self.place_of(target, None), - } - } - _ => { - let target = self.place_expr(*place, span, out); - self.make_place(*place, target, span, out) - } - }, - } - } + Rvalue::RawPtr(_, place) => self.raw_pointer(*place, span, out), Rvalue::Reborrow(..) => self.err(span, "reborrowing a type by its `Reborrow` impl is not supported"), Rvalue::ThreadLocalRef(_) => self.err(span, "thread locals are not supported"), Rvalue::WrapUnsafeBinder(..) => self.err(span, "unsafe binders are not supported"), @@ -262,6 +155,229 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { }) } + /// A raw pointer to `place`. + pub(super) fn raw_pointer(&mut self, place: Place<'tcx>, span: Span, out: &mut Vec) -> Expr { + let tcx = self.tcx; + if self.union_whole(place, span).is_some() { + return self.err(span, "a pointer to a union's field kept in more than one slot"); + } + if let Some(why) = self.out_of_step(place, false) { + return self.err(span, why); + } + let pointee = place.ty(&self.mir.local_decls, tcx).ty; + match self.b.t.pointee(pointee) { + Pointee::Object => self.place_expr(place, span, out), + // `&raw mut *p` of a raw pointer is `p`; of a reference, the slice's start and + // length. + Pointee::Slice => match place.projection.as_slice() { + [rest @ .., mir::ProjectionElem::Deref] + if Place::ty_from(place.local, rest, &self.mir.local_decls, tcx) + .ty + .is_raw_ptr() => + { + let base = Place { + local: place.local, + projection: tcx.mk_place_elems(rest), + }; + self.place_expr(base, span, out) + } + _ => { + let value = self.place_expr(place, span, out); + match crate::lower::pointer::slice_of_bytes(pointee) { + true => Expr::rs("rawByteSliceOf", vec![value]), + false => Expr::rs("rawSliceOf", vec![value]), + } + } + }, + // A slice of all of an array, as a borrow of it is. + Pointee::Array => { + if let Some(e) = self.array_subslice(mir::BorrowKind::Shared, place, span, out) { + return e; + } + let value = self.place_expr(place, span, out); + match self.in_array(place) { + true => value, + false => self.b.t.array_rs(pointee, "From").call(vec![value]), + } + } + // `&raw mut *p` is `p`. A pointer to `()` is an identity, which only another + // pointer is: not the `rs.Place` of a place or of a `&mut`, each borrow's own. + Pointee::Place => match place.projection.as_slice() { + [rest @ .., mir::ProjectionElem::Deref] + if !self.box_pointer(place.local) && { + let base = Place::ty_from(place.local, rest, &self.mir.local_decls, tcx).ty; + match self.b.t.identity_pointee(pointee) { + true => base.is_raw_ptr(), + false => self.b.t.place_pointee(base).is_some(), + } + } => + { + let base = Place { + local: place.local, + projection: tcx.mk_place_elems(rest), + }; + let borrowed = match Place::ty_from(place.local, rest, &self.mir.local_decls, tcx).ty.is_ref() { + true => self.borrowed_pointer(base, span, out), + false => None, + }; + match borrowed { + Some(p) => p, + None => self.place_expr(base, span, out), + } + } + // A reference to an `Arc`'s value, which is that value's one place. + [mir::ProjectionElem::Deref] + if let Some(p) = self.arc_raw(&Operand::Copy(Place::from(place.local)), span, out) => + { + p + } + _ if self.b.t.identity_pointee(pointee) => self.err( + span, + "a raw pointer to a place of `()`, which has no address of its own", + ), + // A reference to an object is the object, not the place holding it. + // A reference to an object is the object, not the place holding it. + [rest @ .., mir::ProjectionElem::Deref] if !self.box_pointer(place.local) => { + let reference = Place { + local: place.local, + projection: tcx.mk_place_elems(rest), + }; + self.behind_reference(reference, span, out) + } + // A pointer to an element is where it is in its array, which pointer + // arithmetic moves within. + [ + array @ .., + mir::ProjectionElem::Index(_) | mir::ProjectionElem::ConstantIndex { .. }, + ] => { + // It points into the array, which must stay where it is. + let array = Place { + local: place.local, + projection: tcx.mk_place_elems(array), + }; + if let Some(why) = self.out_of_step(array, false) { + return self.err(span, why); + } + let target = self.place_expr(place, span, out); + let target = self.stable_target(place, target, span, out); + match target.unlocated() { + Expr::Index(array, index) => { + Expr::rs("arrayAsPtr", vec![*array.clone(), *index.clone()]) + } + _ => self.place_of(target, None), + } + } + _ => { + let target = self.place_expr(place, span, out); + self.make_place(place, target, span, out) + } + }, + } + } + + /// A raw pointer to what the reference `r` points at, a value whose pointer is an + /// `rs.Place`, where `r` is no `rs.Place` itself but the value: the place the body shows + /// `r` borrowed, as [`Self::borrowed_pointer`] finds it. Otherwise it is a place of its + /// own, as `ptr::from_ref` makes: of the value, where `r` is shared and nothing may write + /// the value while the pointer lives, and else writing a whole value into the object `r` + /// is in place, as `*r = v` does. + pub(super) fn behind_reference(&mut self, r: Place<'tcx>, span: Span, out: &mut Vec) -> Expr { + let tcx = self.tcx; + if let Some(p) = self.borrowed_pointer(r, span, out) { + return p; + } + let pointee = tcx.mk_place_deref(r); + let ty = pointee.ty(&self.mir.local_decls, tcx).ty; + let shared = matches!( + r.ty(&self.mir.local_decls, tcx).ty.kind(), + ty::Ref(_, _, rustc_ast::Mutability::Not) + ); + let value = self.place_expr(pointee, span, out); + if shared && ty.is_freeze(tcx, self.b.t.typing_env_pub()) { + return Expr::Object(vec![(Key::Name("v".into()), value)]); + } + let target = self.b.spill(value, "target", out); + let written = self.b.fresh("value"); + let Some(write) = self.write_into(target.clone(), Expr::ident(&written), ty) else { + return self.err( + span, + format!("a raw pointer to `{ty}` behind a reference, which JS can't write a whole value into in place"), + ); + }; + let get = Expr::Lambda(Vec::new(), Box::new(target)); + let set = Expr::Lambda( + vec![Param { + name: written, + ty: None, + }], + Box::new(write), + ); + Expr::rs("place", vec![get, set]) + } + + /// The raw pointer to the place the body shows the reference `r` borrowed, if it does: + /// the place an alias of a borrow stands for, the element an index lends, or where the + /// raw pointer `r` reborrows what it points at from points. + fn borrowed_pointer(&mut self, r: Place<'tcx>, span: Span, out: &mut Vec) -> Option { + let pointee = self.tcx.mk_place_deref(r); + let borrowed = self.through_aliases(pointee); + if borrowed != pointee { + return Some(self.raw_pointer(borrowed, span, out)); + } + if let Some((array, index)) = self.indexed(r) { + return Some(Expr::rs("arrayAsPtr", vec![array, index])); + } + let p = self.reborrowed_pointer(r)?; + Some(self.place_expr(p, span, out)) + } + + /// Whether a raw pointer to `pointee`, made from the reference `r`, is what + /// [`Self::behind_reference`] makes: one to a value whose pointer is an `rs.Place`, which + /// `r` is not. + pub(super) fn behind_place(&self, r: Place<'tcx>, pointee: Ty<'tcx>) -> bool { + let ty = r.ty(&self.mir.local_decls, self.tcx).ty; + let t = &self.b.t; + ty.is_ref() + && t.place_pointee(ty).is_none() + && t.pointee(pointee) == Pointee::Place + && !t.identity_pointee(pointee) + && !t.zero_sized(pointee) + } + + /// The array and index of the element the reference `r` is an alias of, as indexing a + /// slice, an array, or a `Vec` by a number makes one. + pub(super) fn indexed(&self, r: Place<'tcx>) -> Option<(Expr, Expr)> { + let local = r.as_local()?; + if self.def_rvalue(local).is_some() { + return None; + } + let Slot::Alias(Some(target)) = &self.slots[local] else { + return None; + }; + let Expr::Index(array, index) = target.unlocated() else { + return None; + }; + Some((*array.clone(), *index.clone())) + } + + /// The raw pointer whose pointee the reference `r` borrows, if the body shows it: `r` is + /// `&*p` or `&mut *p`, each of which it assigns once, so `p` still points there. + pub(super) fn reborrowed_pointer(&self, r: Place<'tcx>) -> Option> { + let Rvalue::Ref(_, _, p) = self.def_rvalue(self.traced(&Operand::Copy(r))?)? else { + return None; + }; + let [mir::ProjectionElem::Deref] = p.projection.as_slice() else { + return None; + }; + let pointer = Place::from(p.local); + let fixed = match self.is_arg(p.local) { + true => self.defs[p.local] == 0, + false => self.traced(&Operand::Copy(pointer)).is_some(), + }; + let raw = self.mir.local_decls[p.local].ty.is_raw_ptr() && !self.box_pointer(p.local); + (raw && fixed && !self.borrowed[p.local]).then_some(pointer) + } + /// An unsigned integer of at most 32 bits, as a `u32`. fn half(&mut self, op: &Operand<'tcx>, span: Span, out: &mut Vec) -> Expr { let from = op.ty(&self.mir.local_decls, self.tcx); diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index 087e48216..9e16fe4e0 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -12,6 +12,11 @@ //! pointer arithmetic moves within the array. A pointer to an element JS keeps as an //! object is that object, which knows no array it is in, so arithmetic on one is refused. //! +//! A reference to a value JS copies is the value, not where it is kept, so a pointer made +//! from one is the place the body shows the reference borrowed, and else a place of its +//! own: of a shared reference's value, which nothing may write while the pointer lives, +//! or writing a whole value into the object a `&mut` is in place. +//! //! A pointer to `()`, as std erases one, is an identity that compares as itself: null, //! `rs.DANGLING_UNIT`, or a waker's data. One to a place of `()` or from a reference to //! one is refused, as an `rs.Place` of it would be an identity of its own each borrow. @@ -1105,7 +1110,13 @@ pub fn raw_pointee<'tcx>(tcx: ty::TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option /// The value `p`, a pointer to `ty`, points at, as a value of its own. fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> Expr { let value = match b.t.pointee(ty) { - Pointee::Place => p.member("v"), + // The value a pointer made here of a shared reference holds. + Pointee::Place => match p { + Expr::Object(mut members) if matches!(members.as_slice(), [(Key::Name(v), _)] if v == "v") => { + members.pop().expect("one member").1 + } + p => p.member("v"), + }, Pointee::Object => p, Pointee::Array => { let ty = b.t.uninit_inner(ty); From 19345e17d81b94f3601421be1dbbe0799a2ffc78 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:54:06 -0800 Subject: [PATCH 03/45] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/mir/rvalue.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index 593621f28..7884af69a 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -216,7 +216,10 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { local: place.local, projection: tcx.mk_place_elems(rest), }; - let borrowed = match Place::ty_from(place.local, rest, &self.mir.local_decls, tcx).ty.is_ref() { + let borrowed = match Place::ty_from(place.local, rest, &self.mir.local_decls, tcx) + .ty + .is_ref() + { true => self.borrowed_pointer(base, span, out), false => None, }; @@ -261,9 +264,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let target = self.place_expr(place, span, out); let target = self.stable_target(place, target, span, out); match target.unlocated() { - Expr::Index(array, index) => { - Expr::rs("arrayAsPtr", vec![*array.clone(), *index.clone()]) - } + Expr::Index(array, index) => Expr::rs("arrayAsPtr", vec![*array.clone(), *index.clone()]), _ => self.place_of(target, None), } } From 71cd73303af521df4f6f548d0969176fa916b58c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:11:19 -0800 Subject: [PATCH 04/45] test(ts): regenerate the census fixture after merging main Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/census/census.md | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/ts/fixtures/census/census.md b/ts/fixtures/census/census.md index 3f0f658c1..aecd16873 100644 --- a/ts/fixtures/census/census.md +++ b/ts/fixtures/census/census.md @@ -1,6 +1,6 @@ # Census of `census-fixture` -- Refusals: 13 +- Refusals: 9 - Function instances: 6 - Generic functions no instance reached, lowered over their type parameters: 1 @@ -8,7 +8,6 @@ | Count | Construct | First | | ---: | --- | --- | -| 4 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | fixtures/census/src/lib.rs:9:2 | | 3 | a cast from `_` to `_`, between a pointer and an integer | fixtures/census/src/lib.rs:9:2 | | 2 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | fixtures/census/src/lib.rs:19:12 | | 2 | a mutable static, which any code may write | fixtures/census/src/lib.rs:38:3 | @@ -21,41 +20,28 @@ The type or module each refused type or path belongs to. | Count | API | First | | ---: | --- | --- | -| 4 | `u32` | fixtures/census/src/lib.rs:9:2 | | 3 | `usize` | fixtures/census/src/lib.rs:9:2 | -| 2 | `u64` | fixtures/census/src/lib.rs:19:47 | +| 2 | `u32` | fixtures/census/src/lib.rs:19:12 | | 2 | `u8` | fixtures/census/src/lib.rs:19:12 | | 1 | `*const T` | fixtures/census/src/lib.rs:14:2 | | 1 | `*const bytes::Bytes` | fixtures/census/src/lib.rs:45:2 | | 1 | `*const u32` | fixtures/census/src/lib.rs:9:2 | | 1 | `Bits` | fixtures/census/src/lib.rs:28:1 | -| 1 | `bytes::Bytes` | fixtures/census/src/lib.rs:45:2 | +| 1 | `u64` | fixtures/census/src/lib.rs:19:47 | ## By type or path | Count | Construct | Type or path | First | | ---: | --- | --- | --- | -| 2 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | `u32` | fixtures/census/src/lib.rs:9:2 | | 1 | a cast from `_` to `_`, between a pointer and an integer | `*const T`, `usize` | fixtures/census/src/lib.rs:14:2 | | 1 | a cast from `_` to `_`, between a pointer and an integer | `*const bytes::Bytes`, `usize` | fixtures/census/src/lib.rs:45:2 | | 1 | a cast from `_` to `_`, between a pointer and an integer | `*const u32`, `usize` | fixtures/census/src/lib.rs:9:2 | | 1 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | `u32`, `u8` | fixtures/census/src/lib.rs:19:12 | | 1 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | `u64`, `u8` | fixtures/census/src/lib.rs:19:47 | -| 1 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | `bytes::Bytes` | fixtures/census/src/lib.rs:45:2 | -| 1 | a raw pointer to `_` behind a reference, which is the object and not where it is kept | `u64` | fixtures/census/src/lib.rs:19:48 | | 1 | union `_`, whose fields overlap in `_` and `f64`, which JS keeps apart | `Bits`, `u32` | fixtures/census/src/lib.rs:28:1 | ## Spans -
a raw pointer to `_` behind a reference, which is the object and not where it is kept (4) - -- fixtures/census/src/lib.rs:9:2: a raw pointer to `u32` behind a reference, which is the object and not where it is kept -- fixtures/census/src/lib.rs:19:13: a raw pointer to `u32` behind a reference, which is the object and not where it is kept -- fixtures/census/src/lib.rs:19:48: a raw pointer to `u64` behind a reference, which is the object and not where it is kept -- fixtures/census/src/lib.rs:45:2: a raw pointer to `bytes::Bytes` behind a reference, which is the object and not where it is kept - -
-
a cast from `_` to `_`, between a pointer and an integer (3) - fixtures/census/src/lib.rs:9:2: a cast from `*const u32` to `usize`, between a pointer and an integer From 5a40085951572073e19950592505fa04d70dbece Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:18:31 -0800 Subject: [PATCH 05/45] docs(quest): the census at the pin after a raw pointer behind a reference Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 4 ++-- quest/m3/ts-unsafe/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index c4458a33f..c84b89c75 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -45,13 +45,13 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi - The end-to-end work stays here: the model's roots, its differential harness, and the `frames.ts` workload. moq writes the binding files `model.toml` names, in its own tree. -The census at the pin on 2026-11-06: 71 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-07: 45 refusals. `Timestamp` alone refuses nothing, and the [model fixture](/ts/README.md#tests)'s harness runs it against native moq-net at the pin; each case it adds as a type translates. Every other root reaches the rest of the model, so each child below unblocks all of them together. The rest is moq's, for its research quest: kio's `log.rs` reads bytes as its segments and retires them through crossbeam-epoch, whose tagged pointers and inline closures (36 refusals) translation -refuses for good, and smallvec's union over a type parameter and raw parts (10) wait on +refuses for good, and smallvec's union over a type parameter and raw parts (4) wait on [unsafe](/quest/m3/ts-unsafe/README.md), so both need safe `cfg(rust2_ts)` twins or bindings there, as does the `unsafe` slice of `model/frame.rs`'s `Filled`. diff --git a/quest/m3/ts-unsafe/README.md b/quest/m3/ts-unsafe/README.md index f61d838b8..43c532c5f 100644 --- a/quest/m3/ts-unsafe/README.md +++ b/quest/m3/ts-unsafe/README.md @@ -63,7 +63,7 @@ construct still open at `b3df54b6c`, all in m3: [#568](https://github.com/moq-dev/rust2/pull/568). `bytes`' `Buf::get_u16` and wider map to the runtime since [#447](https://github.com/moq-dev/rust2/pull/447). -Census at the moq pin on 2026-11-06: model 82, kio 76, moq-net 409 refusals. +Census at the moq pin on 2026-11-07: model 45, kio 72, moq-net 347 refusals. Public API: the translator's output. From 30e8b1225333034edc725e04cdb09d577cb65ce1 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:38:52 -0800 Subject: [PATCH 06/45] fix(ts): keep an Arc's value's pointer its one place, and read an enum of cells through a shared reference's pointer Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/core/bool.ts | 7 ++++ .../lang/pointers/expected/pointers.ts | 42 ++++++++++++++++--- ts/fixtures/lang/pointers/src/pointers.rs | 29 +++++++++++-- ts/src/lower/mir/origin.rs | 3 +- ts/src/lower/mir/rvalue.rs | 7 +++- 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/core/bool.ts b/ts/fixtures/lang/pointers/expected/core/bool.ts index df98e8987..ba69bc95a 100644 --- a/ts/fixtures/lang/pointers/expected/core/bool.ts +++ b/ts/fixtures/lang/pointers/expected/core/bool.ts @@ -10,6 +10,13 @@ export function thenSome$1gvhopg(self: boolean, t: rs.u16): rs.u16 | undefined { return t; } +export function then$01qpu7s(self: boolean, f: () => rs.Cell): rs.Cell | undefined { + if (!self) { + return undefined; + } + return f(); +} + export function then$0bgt67i(self: boolean, f: () => rs.Weak): rs.Weak | undefined { if (!self) { return undefined; diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 990b05a91..8469d0833 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -2,7 +2,7 @@ import * as rs from "./rs.ts"; import { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_Twin, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; -import { thenSome$1gvhopg, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; +import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; @@ -3174,11 +3174,31 @@ export function behind(r: rs.u32, how: rs.u8): rs.u32 { return p.v; } +/** + * Whether the `Option` a pointer from a shared reference the body is given points at + * holds a cell, and the number in it, read through the pointer made each way std makes + * one. + */ +export function cellBehind(r: rs.Cell | undefined, how: rs.u8): rs.u32 { + let p!: rs.Place | undefined>; + if (rs.u8Rem(how, 2) === 0) { + p = { v: r }; + } else { + p = { v: r }; + } + const _6: rs.Cell | undefined = p.v; + if (_6 === undefined) { + return 0; + } + return rs.u32Add(_6.v, 1); +} + /** * Raw pointers from references: smallvec's `Drain` and `retain`, over values JS copies, a * library type, and an `Option`; pointers from a reference the body borrowed, which move - * within the array it borrowed from; and from references the body is given, to a number - * and to a `Vec`, read and written whole. + * within the array it borrowed from; from references the body is given, to a number, to an + * `Option` of a cell, and to a `Vec`, read and written whole; and from references to an + * `Arc`'s value, measured against each other. */ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { let _112!: rs.usize; @@ -3261,8 +3281,20 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { if (_153 === rs.DONE) { break; } - rs.byteVecPush(out, behind(_153[1] as rs.u32, _153[0] & 0xff) & 0xff); - } + const i2: rs.usize = _153[0]; + const b: rs.u8 = _153[1]; + rs.byteVecPush(out, behind(b as rs.u32, i2 & 0xff) & 0xff); + rs.byteVecPush(out, cellBehind(then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { + return new rs.Cell(b as rs.u32); + }), b) & 0xff); + } + const arc: rs.Arc = new rs.Arc(bits); + const _178: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); + const _181: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); + const p: rs.Place = _178; + rs.byteVecPush(out, rs.ptrOffsetFrom(p, _181) & 0xff); + rs.byteVecPush(out, p.v); + arc[Symbol.dispose](); } //# sourceMappingURL=pointers.ts.map diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index c4428c980..5cd12523e 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2497,10 +2497,25 @@ fn behind(r: &u32, how: u8) -> u32 { unsafe { ptr::read(p) } } +/// Whether the `Option` a pointer from a shared reference the body is given points at +/// holds a cell, and the number in it, read through the pointer made each way std makes +/// one. +fn cell_behind(r: &Option>, how: u8) -> u32 { + let p = match how % 2 { + 0 => &raw const *r, + _ => ptr::from_ref(r), + }; + match unsafe { &*p } { + Some(cell) => cell.get() + 1, + None => 0, + } +} + /// Raw pointers from references: smallvec's `Drain` and `retain`, over values JS copies, a /// library type, and an `Option`; pointers from a reference the body borrowed, which move -/// within the array it borrowed from; and from references the body is given, to a number -/// and to a `Vec`, read and written whole. +/// within the array it borrowed from; from references the body is given, to a number, to an +/// `Option` of a cell, and to a `Vec`, read and written whole; and from references to an +/// `Arc`'s value, measured against each other. pub fn behind_refs(data: &[u8], out: &mut Vec) { let bits = data.first().copied().unwrap_or(0); let nums: Vec = data.iter().map(|&b| u32::from(b) * 3).collect(); @@ -2560,5 +2575,13 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { out.extend(&v); for (i, &b) in data.iter().enumerate() { out.push(behind(&u32::from(b), i as u8) as u8); - } + let cell = (b % 3 != 0).then(|| Cell::new(u32::from(b))); + out.push(cell_behind(&cell, b) as u8); + } + // Pointers from references to an `Arc`'s value, which is its one place. + let arc = Arc::new(u32::from(bits)); + let p = ptr::from_ref(&*arc); + let q = ptr::from_ref(&*arc); + out.push(unsafe { p.offset_from(q) } as u8); + out.push(unsafe { *q } as u8); } diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 3a91b89df..648b3f732 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -260,7 +260,8 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { /// a place of its own, or, where `r` is an `rs.Place`, where that points. fn reference_origin(&self, r: mir::Place<'tcx>, at: Location) -> Option> { let ty = r.ty(&self.mir.local_decls, self.tcx).ty; - if !ty.is_ref() { + // A reference to an `Arc`'s value points at that value's one place. + if !ty.is_ref() || self.arc_of(&Operand::Copy(r)).is_some() { return None; } let pointee = self.tcx.mk_place_deref(r); diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index 7884af69a..1eec69c0c 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -280,7 +280,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// `rs.Place`, where `r` is no `rs.Place` itself but the value: the place the body shows /// `r` borrowed, as [`Self::borrowed_pointer`] finds it. Otherwise it is a place of its /// own, as `ptr::from_ref` makes: of the value, where `r` is shared and nothing may write - /// the value while the pointer lives, and else writing a whole value into the object `r` + /// a whole value through the pointer, and else writing a whole value into the object `r` /// is in place, as `*r = v` does. pub(super) fn behind_reference(&mut self, r: Place<'tcx>, span: Span, out: &mut Vec) -> Expr { let tcx = self.tcx; @@ -293,8 +293,11 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { r.ty(&self.mir.local_decls, tcx).ty.kind(), ty::Ref(_, _, rustc_ast::Mutability::Not) ); + // Through a shared reference, a whole write is undefined unless every byte of the + // value is in an `UnsafeCell`, as no enum's tag is. + let tagged = matches!(ty.kind(), ty::Adt(adt, _) if adt.is_enum() && adt.variants().len() > 1); let value = self.place_expr(pointee, span, out); - if shared && ty.is_freeze(tcx, self.b.t.typing_env_pub()) { + if shared && (tagged || ty.is_freeze(tcx, self.b.t.typing_env_pub())) { return Expr::Object(vec![(Key::Name("v".into()), value)]); } let target = self.b.spill(value, "target", out); From 437a61a706ed24a50473b40fe6a5ebfc1a416643 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:44:49 -0800 Subject: [PATCH 07/45] test(ts): snapshot the Arc pointers' case Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/pointers/expected/pointers.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 8469d0833..bf22d7baf 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -3288,12 +3288,10 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { return new rs.Cell(b as rs.u32); }), b) & 0xff); } - const arc: rs.Arc = new rs.Arc(bits); - const _178: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); - const _181: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); - const p: rs.Place = _178; - rs.byteVecPush(out, rs.ptrOffsetFrom(p, _181) & 0xff); - rs.byteVecPush(out, p.v); + const arc: rs.Arc = new rs.Arc(bits as rs.u32); + const q: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); + rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc, rs.arcPlace(arc), "Arc"), q) & 0xff); + rs.byteVecPush(out, q.v & 0xff); arc[Symbol.dispose](); } From bce76b5c134fca6d0c5c06caf90135a11df316e9 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:49:33 -0800 Subject: [PATCH 08/45] fix(ts): tell a tag outside every UnsafeCell by the value's layout Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 1 + ts/fixtures/reject/expected.txt | 1 + ts/fixtures/reject/src/alloc.rs | 7 +++++++ ts/fixtures/reject/ts.toml | 1 + ts/src/lower/mir/rvalue.rs | 9 ++++++--- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index a8a994c16..7fe94f034 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -10,6 +10,7 @@ fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index b73a09827..b82c6fd82 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -14,6 +14,7 @@ fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index d09a28cc9..127ba0ea8 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -24,6 +24,13 @@ pub fn allocated() -> u8 { } } +/// A whole value written through a pointer from a shared reference to a `Result` of a cell +/// whose only inhabited variant leaves it no tag, all of it in an `UnsafeCell`, which JS +/// can't write into in place. +pub fn infallible_cell(r: &Result, std::convert::Infallible>) { + unsafe { std::ptr::from_ref(r).cast_mut().write(Ok(std::cell::Cell::new(19))) } +} + /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 769b86785..2a268d10a 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -4,6 +4,7 @@ roots = [ "alloc::alloc_retyped", "alloc::allocated", "alloc::bytes_as_values", + "alloc::infallible_cell", "alloc::past_reference", "alloc::reinterpreted", "alloc::zeroed", diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index 1eec69c0c..1f4b30505 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -294,10 +294,13 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { ty::Ref(_, _, rustc_ast::Mutability::Not) ); // Through a shared reference, a whole write is undefined unless every byte of the - // value is in an `UnsafeCell`, as no enum's tag is. - let tagged = matches!(ty.kind(), ty::Adt(adt, _) if adt.is_enum() && adt.variants().len() > 1); + // value is in an `UnsafeCell`, as no tag or niche of a layout's variants is. + let env = self.b.t.typing_env_pub(); + let tagged = tcx + .layout_of(env.as_query_input(ty)) + .is_ok_and(|layout| matches!(layout.variants, rustc_abi::Variants::Multiple { .. })); let value = self.place_expr(pointee, span, out); - if shared && (tagged || ty.is_freeze(tcx, self.b.t.typing_env_pub())) { + if shared && (tagged || ty.is_freeze(tcx, env)) { return Expr::Object(vec![(Key::Name("v".into()), value)]); } let target = self.b.spill(value, "target", out); From 0766fe431d54c98999daf123bb527b5305d8999c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:39:43 -0800 Subject: [PATCH 09/45] chore(ts): regenerate the pointer snapshots for rs.String Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/core/option.ts | 2 +- .../pointers/expected/pointers.Drained.ts | 10 ++-- .../lang/pointers/expected/pointers.ts | 50 ++++++++++--------- ts/fixtures/lang/pointers/src/pointers.rs | 4 +- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 9d27f5111..9244a4c57 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -55,7 +55,7 @@ export function map$00buaky(self: rs.u16 | undefined | rs.None, f: (a0: rs.u16 | return f(self); } -export function map$18tou5q(self: string | undefined, f: (a0: string) => string): string | undefined { +export function map$1lzqy5c(self: rs.String | undefined, f: (a0: rs.String) => rs.String): rs.String | undefined { if (self === undefined) { return undefined; } diff --git a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts index b49b63d8b..da114c70c 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { map$00buaky as option_map$00buaky, map$18tou5q as option_map$18tou5q, map$1h9tk16 as option_map$1h9tk16, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; +import { map$00buaky as option_map$00buaky, map$1lzqy5c as option_map$1lzqy5c, map$1h9tk16 as option_map$1h9tk16, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; /** * What smallvec's `Drain` is: the elements of a slice nothing will drop again, each read @@ -17,8 +17,8 @@ export function next$iter_Iterator_Option_u16(self: Drained) }), rs.None); } -export function next$iter_Iterator_String(self: Drained): string | rs.Done { - return rs.iterFromOption(option_map$18tou5q(rs.iterToOption((self.iter as rs.Iter).next()), (r: string): string => { +export function next$iter_Iterator_String(self: Drained): rs.String | rs.Done { + return rs.iterFromOption(option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.String): rs.String => { return { v: r }.v; })); } @@ -41,8 +41,8 @@ export function nextBack$iter_DoubleEndedIterator_Option_u16(self: Drained): string | undefined { - return option_map$18tou5q(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: string): string => { +export function nextBack$iter_DoubleEndedIterator_String(self: Drained): rs.String | undefined { + return option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.String): rs.String => { return { v: r }.v; }); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 7f2d45902..421328885 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -3002,10 +3002,10 @@ export function drained$Option_u16(v: rs.Vec, bits: rs.u8): } /** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ -export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { - let next!: string | undefined; - const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; - const out: rs.Vec = new rs.Vec(); +export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.String | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); let i: rs.i32 = 0; while (true) { if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { @@ -3068,16 +3068,16 @@ export function drained$u32(v: rs.Vec, bits: rs.u8): rs.Vec { * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then * drops or moves to the front. */ -export function retained$String_pointers_behind_refs_closure6(v: rs.Vec, keep: (a0: rs.Place) => boolean): void { +export function retained$String_pointers_behind_refs_closure6(v: rs.Vec, keep: (a0: rs.String) => boolean): void { let _10!: rs.usize; const len: rs.usize = v.items.length; - const p: rs.Place = rs.arrayAsPtr(v.items); + const p: rs.Place = rs.arrayAsPtr(v.items); let write: rs.usize = 0; let iterStart: rs.usize = 0; const iterEnd: rs.usize = len; while ((_10 = iterStart++) < iterEnd) { - const _15: rs.Place = rs.ptrAdd(p, _10); - if (keep(_15)) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15.v)) { rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); write = rs.usizeAdd(write, 1); } @@ -3208,17 +3208,19 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(drained$u32(rs.vecFrom(nums.items.slice()), bits).items), (_23: rs.u32): rs.u8 => { return _23 & 0xff; }), undefined, true); - const words: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b3: rs.u8): string => { - return rs.fmtToString((f: rs.Formatter) => rs.u8Display(b3, f)); + const words: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b3: rs.u8): rs.String => { + return rs.fmtToString((f: rs.Formatter) => rs.u8Display(b3, f), 0); }), undefined, 12, true); - for (const _25 of drained$String(rs.vecFrom(words.items.slice()), bits).items) { - rs.iterExtendBytes(out, rs.strBytes(_25), undefined, true); + for (const _25 of drained$String(rs.vecFrom(words.items.map((a0: rs.String): rs.String => { + return rs.stringClone(a0); + })), bits).items) { + rs.iterExtendBytes(out, rs.strBytes(_25.v), undefined, true); } - const runs: rs.Vec = rs.iterToVec(rs.iterMap(rs.byteSliceChunks(data, 3), (a0: rs.ByteSlice): rs.ByteVec => { - return rs.byteSliceToByteVec(a0); + const runs: rs.Vec = rs.iterToVec(rs.iterMap(rs.byteSliceChunks(data, 3), (a02: rs.ByteSlice): rs.ByteVec => { + return rs.byteSliceToByteVec(a02); }), undefined, 12, true); - for (const r of drained$Vec_u8(rs.vecFrom(runs.items.map((a02: rs.ByteVec): rs.ByteVec => { - return rs.byteVecFrom(rs.byteVecView(a02)); + for (const r of drained$Vec_u8(rs.vecFrom(runs.items.map((a03: rs.ByteVec): rs.ByteVec => { + return rs.byteVecFrom(rs.byteVecView(a03)); })), bits).items) { rs.byteVecPush(out, r.length & 0xff); rs.iterExtendBytes(out, rs.byteVecIntoIter(r), undefined, true); @@ -3227,8 +3229,8 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { const b4: rs.u8 = _24; return thenSome$1gvhopg(rs.u8Rem(b4, 3) !== 0, b4 as rs.u16); }), undefined, 4, true), bits).items) { - rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0smr0aj(_56, Uint8Array.of(255, 255), (a03: rs.u16): Uint8Array => { - return rs.u16ToBeBytes(a03); + rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0smr0aj(_56, Uint8Array.of(255, 255), (a04: rs.u16): Uint8Array => { + return rs.u16ToBeBytes(a04); })), undefined, true); } const nums2: rs.Vec = nums; @@ -3239,13 +3241,13 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(nums2.items), (_26: rs.u32): rs.u8 => { return _26 & 0xff; }), undefined, true); - const words2: rs.Vec = words; - retained$String_pointers_behind_refs_closure6(words2, (w2: rs.Place): boolean => { - w2.v = w2.v + "!"; - return rs.usizeRem(rs.stringLen(w2.v), 2) === 0; + const words2: rs.Vec = words; + retained$String_pointers_behind_refs_closure6(words2, (w2: rs.String): boolean => { + rs.stringPush(w2, 33); + return rs.usizeRem(w2.len, 2) === 0; }); - rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w3: string): rs.Iter => { - return rs.strBytes(w3); + rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w3: rs.String): rs.Iter => { + return rs.strBytes(w3.v); }, undefined, true), "next"); const runs2: rs.Vec = runs; retained$Vec_u8_pointers_behind_refs_closure8(runs2, (r2: rs.ByteVec): boolean => { diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 5cd12523e..eda948ec7 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2511,8 +2511,8 @@ fn cell_behind(r: &Option>, how: u8) -> u32 { } } -/// Raw pointers from references: smallvec's `Drain` and `retain`, over values JS copies, a -/// library type, and an `Option`; pointers from a reference the body borrowed, which move +/// Raw pointers from references: smallvec's `Drain` and `retain`, over a number, library +/// types, and an `Option`; pointers from a reference the body borrowed, which move /// within the array it borrowed from; from references the body is given, to a number, to an /// `Option` of a cell, and to a `Vec`, read and written whole; and from references to an /// `Arc`'s value, measured against each other. From 694c6e18b980ea7afebf05f99ae0106fb51e3eb9 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:04:30 -0800 Subject: [PATCH 10/45] chore(ts): regenerate the pointer snapshots after merging main Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/pointers/expected/lib.ts | 8 +- .../lang/pointers/expected/pointers.ts | 321 +++++++++++++++++- 2 files changed, 324 insertions(+), 5 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 0f3ef1956..ae9d5ec7d 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, genericUnions } from "./pointers.ts"; +import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, behindRefs, genericUnions } from "./pointers.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -302,8 +302,10 @@ export function cases(): rs.Vec<[string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.By rawPlaces(data26, out26); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::arrays", (_236: rs.u8, data27: rs.ByteSlice, out27: rs.ByteVec): undefined => { arrays(data27, out27); - }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::generic_unions", (_237: rs.u8, data28: rs.ByteSlice, out28: rs.ByteVec): undefined => { - genericUnions(data28, out28); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::behind_refs", (_237: rs.u8, data28: rs.ByteSlice, out28: rs.ByteVec): undefined => { + behindRefs(data28, out28); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::generic_unions", (_238: rs.u8, data29: rs.ByteSlice, out29: rs.ByteVec): undefined => { + genericUnions(data29, out29); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index a059e4922..f6918860e 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -2,14 +2,15 @@ import * as rs from "./rs.ts"; import { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_Twin, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; -import { then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; -import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$173j2gr as option_mapOr$173j2gr, filter$0wvwd70 } from "./core/option.ts"; +import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; +import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; import * as ByClosure from "./pointers.ByClosure.ts"; import type * as ByObject from "./pointers.ByObject.ts"; import * as Counted from "./pointers.Counted.ts"; +import * as Drained from "./pointers.Drained.ts"; import type * as Duo from "./pointers.Duo.ts"; import type * as Either from "./pointers.Either.ts"; import type * as Enclosed from "./pointers.Enclosed.ts"; @@ -2980,6 +2981,322 @@ export function lent(n: rs.u8): rs.ByteVec { return o; } +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$Option_u16(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.u16 | undefined | rs.None; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_Option_u16(iter), rs.None); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_Option_u16(iter); + } + if (next !== rs.None) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.String | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_String(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_String(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$Vec_u8(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.ByteVec | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_Vec_u8(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_Vec_u8(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ +export function drained$u32(v: rs.Vec, bits: rs.u8): rs.Vec { + let next!: rs.u32 | undefined; + const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const out: rs.Vec = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_u32(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_u32(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + return out; + } + } +} + +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$String_pointers_behind_refs_closure6(v: rs.Vec, keep: (a0: rs.String) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place = rs.vecAsPtr(v, 12); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15.v)) { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSplitOff(v, write); +} + +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$Vec_u8_pointers_behind_refs_closure8(v: rs.Vec, keep: (a0: rs.ByteVec) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place = rs.vecAsPtr(v, 12); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15.v)) { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSplitOff(v, write); +} + +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$u32_pointers_behind_refs_closure4(v: rs.Vec, keep: (a0: rs.Place) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place = rs.vecAsPtr(v, 4); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place = rs.ptrAdd(p, _10); + if (keep(_15)) { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSplitOff(v, write); +} + +/** The length of the `Vec` a pointer from a reference the body is given points at. */ +export function vecLen(v: rs.ByteVec): rs.usize { + return { v }.v.length; +} + +/** + * `with` written whole through a pointer from a `&mut` the body is given, made each way + * std makes one, which its caller's `Vec` holds after. + */ +export function refilled(v: rs.ByteVec, with_: rs.ByteVec, how: rs.u8): void { + let p!: rs.Place; + const _5: rs.u8 = rs.u8Rem(how, 4); + if (_5 === 0) { + const target4 = v; + p = rs.place(() => target4, (value4) => Object.assign(target4, value4)); + } else if (_5 === 1) { + const target3 = v; + p = rs.place(() => target3, (value3) => Object.assign(target3, value3)); + } else if (_5 === 2) { + const target2 = v; + p = rs.place(() => target2, (value2) => Object.assign(target2, value2)); + } else { + const target = v; + p = rs.place(() => target, (value) => Object.assign(target, value)); + } + p.v = with_; + rs.byteVecPush(p.v, how); +} + +/** + * The value a pointer from a shared reference the body is given points at, made each way + * std makes one. + */ +export function behind(r: rs.u32, how: rs.u8): rs.u32 { + let p!: rs.Place; + const _4: rs.u8 = rs.u8Rem(how, 3); + if (_4 === 0) { + p = { v: r }; + } else if (_4 === 1) { + p = { v: r }; + } else { + p = { v: r }; + } + return p.v; +} + +/** + * Whether the `Option` a pointer from a shared reference the body is given points at + * holds a cell, and the number in it, read through the pointer made each way std makes + * one. + */ +export function cellBehind(r: rs.Cell | undefined, how: rs.u8): rs.u32 { + let p!: rs.Place | undefined>; + if (rs.u8Rem(how, 2) === 0) { + p = { v: r }; + } else { + p = { v: r }; + } + const _6: rs.Cell | undefined = p.v; + if (_6 === undefined) { + return 0; + } + return rs.u32Add(_6.v, 1); +} + +/** + * Raw pointers from references: smallvec's `Drain` and `retain`, over a number, library + * types, and an `Option`; pointers from a reference the body borrowed, which move + * within the array it borrowed from; from references the body is given, to a number, to an + * `Option` of a cell, and to a `Vec`, read and written whole; and from references to an + * `Arc`'s value, measured against each other. + */ +export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { + let _112!: rs.usize; + const bits: rs.u8 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0); + const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): rs.u32 => { + return rs.u32Mul(_2 as rs.u32, 3); + }), undefined, 4, true); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(drained$u32(rs.vecFrom(nums.items.slice()), bits).items), (_23: rs.u32): rs.u8 => { + return _23 & 0xff; + }), undefined, true); + const words: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b3: rs.u8): rs.String => { + return rs.fmtToString((f: rs.Formatter) => rs.u8Display(b3, f), 0); + }), undefined, 12, true); + for (const _25 of drained$String(rs.vecFrom(words.items.map((a0: rs.String): rs.String => { + return rs.stringClone(a0); + })), bits).items) { + rs.iterExtendBytes(out, rs.strBytes(_25.v), undefined, true); + } + const runs: rs.Vec = rs.iterToVec(rs.iterMap(rs.byteSliceChunks(data, 3), (a02: rs.ByteSlice): rs.ByteVec => { + return rs.byteSliceToByteVec(a02); + }), undefined, 12, true); + for (const r of drained$Vec_u8(rs.vecFrom(runs.items.map((a03: rs.ByteVec): rs.ByteVec => { + return rs.byteVecFrom(rs.byteVecView(a03)); + })), bits).items) { + rs.byteVecPush(out, r.length & 0xff); + rs.iterExtendBytes(out, rs.byteVecIntoIter(r), undefined, true); + } + for (const _56 of drained$Option_u16(rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_24: rs.u8): rs.u16 | undefined => { + const b4: rs.u8 = _24; + return thenSome$1gvhopg(rs.u8Rem(b4, 3) !== 0, b4 as rs.u16); + }), undefined, 4, true), bits).items) { + rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0smr0aj(_56, Uint8Array.of(255, 255), (a04: rs.u16): Uint8Array => { + return rs.u16ToBeBytes(a04); + })), undefined, true); + } + const nums2: rs.Vec = nums; + retained$u32_pointers_behind_refs_closure4(nums2, (n2: rs.Place): boolean => { + n2.v = rs.u32Add(n2.v, 1); + return rs.u32Rem(n2.v, 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(nums2.items), (_26: rs.u32): rs.u8 => { + return _26 & 0xff; + }), undefined, true); + const words2: rs.Vec = words; + retained$String_pointers_behind_refs_closure6(words2, (w2: rs.String): boolean => { + rs.stringPush(w2, 33); + return rs.usizeRem(w2.len, 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w3: rs.String): rs.Iter => { + return rs.strBytes(w3.v); + }, undefined, true), "next"); + const runs2: rs.Vec = runs; + retained$Vec_u8_pointers_behind_refs_closure8(runs2, (r2: rs.ByteVec): boolean => { + rs.byteVecPush(r2, 7); + return rs.u8Rem(r2.data[rs.idx(r2, 0)], 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterFlatten(rs.iterSlice(runs2.items), (value: rs.ByteVec) => rs.iterSlice(rs.byteVecView(value)), true), "next"); + const nums3: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_27: rs.u8): rs.u32 => { + return _27 as rs.u32; + }), undefined, 4, true); + if (rs.sliceFrom(nums3.items).length >= 2) { + const first: rs.Place = rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 0)); + rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, rs.usizeSub(nums3.items.length, 1))), first) & 0xff); + const _109: rs.usize = nums3.items.length; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = _109; + while ((_112 = iterStart++) < iterEnd) { + rs.byteVecPush(out, rs.ptrAdd(first, _112).v & 0xff); + } + rs.byteVecPush(out, rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v & 0xff); + rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v = 9; + rs.byteVecPush(out, nums3.items[rs.idx(nums3.items, 0)] & 0xff); + } + const v: rs.ByteVec = rs.byteSliceToByteVec(data); + rs.byteVecPush(out, vecLen(v) & 0xff); + const _143: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); + refilled(v, _143, bits); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); + const iter5: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); + while (true) { + const _153: [rs.usize, rs.u8] | rs.Done = iter5.next(); + if (_153 === rs.DONE) { + break; + } + const i2: rs.usize = _153[0]; + const b: rs.u8 = _153[1]; + rs.byteVecPush(out, behind(b as rs.u32, i2 & 0xff) & 0xff); + rs.byteVecPush(out, cellBehind(then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { + return new rs.Cell(b as rs.u32); + }), b) & 0xff); + } + const arc: rs.Arc = new rs.Arc(bits as rs.u32); + const q: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); + rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc, rs.arcPlace(arc), "Arc"), q) & 0xff); + rs.byteVecPush(out, q.v & 0xff); + arc[Symbol.dispose](); +} + /** * Each instance of a union over a type parameter: written as one field, in part through * another and through a reference into it, and whole as a third, read as each, and copied From f45cfbcada9a13ab5afad4cd41dafa7c3fa42678 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:10:54 -0800 Subject: [PATCH 11/45] quest: recount the census after merging main Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 10 +++++----- quest/m3/ts-unsafe/README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index f54823ec8..2e6f162e2 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -42,15 +42,15 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi - The end-to-end work stays here: the model's roots, its differential harness, and the `frames.ts` workload. moq writes the binding files `model.toml` names, in its own tree. -The census at the pin on 2026-11-07: 45 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-07: 46 refusals. `Timestamp` alone refuses nothing, and the [model fixture](/ts/README.md#tests)'s harness runs it against native moq-net at the pin; each case it adds as a type translates. Every other root reaches the rest of the model, so each child below unblocks all of them together. The rest is moq's, for its research quest: kio's `log.rs` reads bytes as its segments and retires them through -crossbeam-epoch, whose tagged pointers and inline closures (36 refusals) translation -refuses for good, and smallvec's union over a type parameter and raw parts (4) wait on -[unsafe](/quest/m3/ts-unsafe/README.md), so moq rewrites or drops them, as it does the -`unsafe` slice of `model/frame.rs`'s `Filled`. +crossbeam-epoch, whose tagged pointers and inline closures (33 refusals) translation +refuses for good, and smallvec's union over a type parameter, its `Array::Item`, and raw +parts (5) wait on [unsafe](/quest/m3/ts-unsafe/README.md), so moq rewrites or drops them, +as it does the `unsafe` slice of `model/frame.rs`'s `Filled`. Decided with the maintainer on 2026-11-04, from a census at the bumped pin (moq-dev/rust2#390): the refusals no quest owned became the children below. ✅ arrayvec's diff --git a/quest/m3/ts-unsafe/README.md b/quest/m3/ts-unsafe/README.md index 33d06aef6..b2ec2e79c 100644 --- a/quest/m3/ts-unsafe/README.md +++ b/quest/m3/ts-unsafe/README.md @@ -66,7 +66,7 @@ construct still open at `b3df54b6c`, all in m3: [#568](https://github.com/moq-dev/rust2/pull/568). `bytes`' `Buf::get_u16` and wider map to the runtime since [#447](https://github.com/moq-dev/rust2/pull/447). -Census at the moq pin on 2026-11-07: model 45, kio 72, moq-net 347 refusals. +Census at the moq pin on 2026-11-07: model 46, kio 72, moq-net 348 refusals. Public API: the translator's output. From e58dcda302c732fc2d0ef7cbec2fe41edb2dbed4 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:35:36 -0800 Subject: [PATCH 12/45] fix(ts): move a copy out of a pointer from a &mut to an object A raw pointer from a `&mut` to a library object writes a whole value into that object in place, so `ptr::replace`, `ptr::read` before a write, `ptr::copy`, and `ptr::swap` returned the object the write then changed. Such a pointer is now an `rs.placeIn`, whose reads that move the value out copy it, as `replaceIn` does for `mem::replace`. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- .../pointers/expected/pointers.Drained.ts | 8 +- .../lang/pointers/expected/pointers.ts | 86 ++++++++++++++----- ts/fixtures/lang/pointers/src/pointers.rs | 39 ++++++++- ts/runtime/rs.ts | 47 +++++++++- ts/src/lower/mir/call.rs | 2 +- ts/src/lower/mir/mod.rs | 10 +++ ts/src/lower/mir/place.rs | 24 ++---- ts/src/lower/mir/rvalue.rs | 22 ++--- ts/src/lower/pointer.rs | 10 +-- 10 files changed, 182 insertions(+), 68 deletions(-) diff --git a/ts/README.md b/ts/README.md index 2bd2d5f19..941ef2681 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: 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 | | `&T`, `&mut T`, `Box`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or writing a whole value into the object a `&mut` is in place. 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 | +| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place, which `ptr::read`, `replace`, `swap`, and `copy` move out a copy of. 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`: `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 | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts index da114c70c..66be08b9e 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts @@ -19,13 +19,13 @@ export function next$iter_Iterator_Option_u16(self: Drained) export function next$iter_Iterator_String(self: Drained): rs.String | rs.Done { return rs.iterFromOption(option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.String): rs.String => { - return { v: r }.v; + return r; })); } export function next$iter_Iterator_Vec_u8(self: Drained): rs.ByteVec | rs.Done { return rs.iterFromOption(option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.ByteVec): rs.ByteVec => { - return { v: r }.v; + return r; })); } @@ -43,13 +43,13 @@ export function nextBack$iter_DoubleEndedIterator_Option_u16(self: Drained): rs.String | undefined { return option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.String): rs.String => { - return { v: r }.v; + return r; }); } export function nextBack$iter_DoubleEndedIterator_Vec_u8(self: Drained): rs.ByteVec | undefined { return option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.ByteVec): rs.ByteVec => { - return { v: r }.v; + return r; }); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index f6918860e..a6cce8b8f 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -3141,22 +3141,50 @@ export function refilled(v: rs.ByteVec, with_: rs.ByteVec, how: rs.u8): void { let p!: rs.Place; const _5: rs.u8 = rs.u8Rem(how, 4); if (_5 === 0) { - const target4 = v; - p = rs.place(() => target4, (value4) => Object.assign(target4, value4)); + p = rs.placeIn(v); } else if (_5 === 1) { - const target3 = v; - p = rs.place(() => target3, (value3) => Object.assign(target3, value3)); + p = rs.placeIn(v); } else if (_5 === 2) { - const target2 = v; - p = rs.place(() => target2, (value2) => Object.assign(target2, value2)); + p = rs.placeIn(v); } else { - const target = v; - p = rs.place(() => target, (value) => Object.assign(target, value)); + p = rs.placeIn(v); } p.v = with_; rs.byteVecPush(p.v, how); } +/** + * What the `Vec` a pointer from a `&mut` the body is given points at held, moved out + * each way std moves one out as `with` is written whole in its place, and then swapped + * with `w` through pointers from `&mut`s to each. + */ +export function replaced(v: rs.ByteVec, w: rs.ByteVec, with_: rs.ByteVec, how: rs.u8): rs.ByteVec { + let old!: rs.ByteVec; + const p: rs.Place = rs.placeIn(v); + const _7: rs.u8 = rs.u8Rem(how, 3); + if (_7 === 0) { + const pointer = p; + const old3 = rs.ptrRead(pointer); + pointer.v = with_; + old = old3; + } else if (_7 === 1) { + const old2: rs.ByteVec = rs.ptrRead(p); + p.v = with_; + old = old2; + } else { + let slot: rs.ByteVec = (undefined as unknown) as rs.ByteVec; + rs.ptrCopy(p, rs.place(() => slot, (value) => slot = value), 1); + p.v = with_; + old = slot; + } + const x = rs.placeIn(v); + const y = rs.placeIn(w); + const old4 = rs.ptrRead(x); + x.v = rs.ptrRead(y); + y.v = old4; + return old; +} + /** * The value a pointer from a shared reference the body is given points at, made each way * std makes one. @@ -3195,13 +3223,14 @@ export function cellBehind(r: rs.Cell | undefined, how: rs.u8): rs.u32 { /** * Raw pointers from references: smallvec's `Drain` and `retain`, over a number, library - * types, and an `Option`; pointers from a reference the body borrowed, which move - * within the array it borrowed from; from references the body is given, to a number, to an - * `Option` of a cell, and to a `Vec`, read and written whole; and from references to an - * `Arc`'s value, measured against each other. + * types, and an `Option`; pointers from a reference the body borrowed, which move within + * the array it borrowed from; from references the body is given, to a number, to an + * `Option` of a cell, and to a `Vec`, read, written, moved out of, and swapped whole; and + * from references to an `Arc`'s value, measured against each other. */ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { let _112!: rs.usize; + let _152!: rs.u8; const bits: rs.u8 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0); const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): rs.u32 => { return rs.u32Mul(_2 as rs.u32, 3); @@ -3243,12 +3272,12 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { return _26 & 0xff; }), undefined, true); const words2: rs.Vec = words; - retained$String_pointers_behind_refs_closure6(words2, (w2: rs.String): boolean => { - rs.stringPush(w2, 33); - return rs.usizeRem(w2.len, 2) === 0; + retained$String_pointers_behind_refs_closure6(words2, (w3: rs.String): boolean => { + rs.stringPush(w3, 33); + return rs.usizeRem(w3.len, 2) === 0; }); - rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w3: rs.String): rs.Iter => { - return rs.strBytes(w3.v); + rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w4: rs.String): rs.Iter => { + return rs.strBytes(w4.v); }, undefined, true), "next"); const runs2: rs.Vec = runs; retained$Vec_u8_pointers_behind_refs_closure8(runs2, (r2: rs.ByteVec): boolean => { @@ -3277,14 +3306,27 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { const _143: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); refilled(v, _143, bits); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); - const iter5: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); + let iterStart2: rs.u8 = 0; + const iterEnd2: rs.u8 = 3; + while ((_152 = iterStart2++) < iterEnd2) { + const how: rs.u8 = _152; + const w2: rs.ByteVec = rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b6: rs.u8): rs.u8 => { + return b6 ^ how; + }), undefined, true); + const _164: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w2, _164, how))), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(w2)), undefined, true); + rs.byteVecPush(out, 255); + } + const iter6: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); while (true) { - const _153: [rs.usize, rs.u8] | rs.Done = iter5.next(); - if (_153 === rs.DONE) { + const _179: [rs.usize, rs.u8] | rs.Done = iter6.next(); + if (_179 === rs.DONE) { break; } - const i2: rs.usize = _153[0]; - const b: rs.u8 = _153[1]; + const i2: rs.usize = _179[0]; + const b: rs.u8 = _179[1]; rs.byteVecPush(out, behind(b as rs.u32, i2 & 0xff) & 0xff); rs.byteVecPush(out, cellBehind(then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { return new rs.Cell(b as rs.u32); diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 2983b01de..640767f26 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2487,6 +2487,29 @@ fn refilled(v: &mut Vec, with: Vec, how: u8) { unsafe { (*p).push(how) }; } +/// What the `Vec` a pointer from a `&mut` the body is given points at held, moved out +/// each way std moves one out as `with` is written whole in its place, and then swapped +/// with `w` through pointers from `&mut`s to each. +fn replaced(v: &mut Vec, w: &mut Vec, with: Vec, how: u8) -> Vec { + let p = ptr::from_mut(v); + let old = match how % 3 { + 0 => unsafe { ptr::replace(p, with) }, + 1 => { + let old = unsafe { ptr::read(p) }; + unsafe { ptr::write(p, with) }; + old + } + _ => { + let mut slot = MaybeUninit::>::uninit(); + unsafe { ptr::copy_nonoverlapping(p, slot.as_mut_ptr(), 1) }; + unsafe { ptr::write(p, with) }; + unsafe { slot.assume_init() } + } + }; + unsafe { ptr::swap(ptr::from_mut(v), ptr::from_mut(w)) }; + old +} + /// The value a pointer from a shared reference the body is given points at, made each way /// std makes one. fn behind(r: &u32, how: u8) -> u32 { @@ -2513,10 +2536,10 @@ fn cell_behind(r: &Option>, how: u8) -> u32 { } /// Raw pointers from references: smallvec's `Drain` and `retain`, over a number, library -/// types, and an `Option`; pointers from a reference the body borrowed, which move -/// within the array it borrowed from; from references the body is given, to a number, to an -/// `Option` of a cell, and to a `Vec`, read and written whole; and from references to an -/// `Arc`'s value, measured against each other. +/// types, and an `Option`; pointers from a reference the body borrowed, which move within +/// the array it borrowed from; from references the body is given, to a number, to an +/// `Option` of a cell, and to a `Vec`, read, written, moved out of, and swapped whole; and +/// from references to an `Arc`'s value, measured against each other. pub fn behind_refs(data: &[u8], out: &mut Vec) { let bits = data.first().copied().unwrap_or(0); let nums: Vec = data.iter().map(|&b| u32::from(b) * 3).collect(); @@ -2574,6 +2597,14 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { out.push(vec_len(&v) as u8); refilled(&mut v, data.iter().rev().copied().collect(), bits); out.extend(&v); + for how in 0..3 { + let mut w: Vec = data.iter().map(|b| b ^ how).collect(); + let old = replaced(&mut v, &mut w, vec![bits; usize::from(how) + 1], how); + out.extend(&old); + out.extend(&v); + out.extend(&w); + out.push(0xff); + } for (i, &b) in data.iter().enumerate() { out.push(behind(&u32::from(b), i as u8) as u8); let cell = (b % 3 != 0).then(|| Cell::new(u32::from(b))); diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index f43eeecb1..70a949afd 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -141,6 +141,43 @@ export function place(get: () => T, set: (value: T) => void): Place { return new Accessed(get, set); } +/** + * A place that is the object `target`, which a write of a whole value lands in, as + * `replaceIn` writes: a raw pointer from a `&mut` to an object, which others may hold. + */ +class WrittenIn implements Place { + readonly target: T; + /** The levels of arrays whose items a copy of `target` copies too, as `objectCopy`'s. */ + readonly depth: number; + + constructor(target: T, depth: number) { + this.target = target; + this.depth = depth; + } + + get v(): T { + return this.target; + } + + set v(value: T) { + writeIn(this.target, value); + } +} + +/** The raw pointer from a `&mut` to `target`, an object, as `WrittenIn` is. */ +export function placeIn(target: T, depth = 0): Place { + return new WrittenIn(target, depth); +} + +/** + * What a read through `p` moves out, as `ptr::read` does: what it holds, or a copy of the + * object a `placeIn` is, which a write through it would change. + */ +export function ptrRead(p: Place): T { + const home = ptrHome(p); + return home instanceof WrittenIn ? (objectCopy(home.target, home.depth) as T) : p.v; +} + // Raw pointers /** A null raw pointer, of any type: nothing reads through it. */ @@ -445,6 +482,9 @@ export function ptrCopy(src: Place, dst: Place, n: number, copy?: (valu return; } const values = Array.from({ length: n }, (_, i) => elemGet(from.array, from.index + i)); + // What a `placeIn` holds changes with the next write through it. + const home = ptrHome(from); + if (home instanceof WrittenIn) values[0] = objectCopy(home.target, home.depth) as T; // Memory nothing wrote yet is copied as it is, which no value's copy can read. for (let i = 0; i < n; i++) elemSet(to.array, to.index + i, copy && values[i] !== undefined ? copy(values[i]) : values[i]); } @@ -565,9 +605,14 @@ export function ptrWrite(p: T, value: T): void { */ export function replaceIn(p: T, value: T, depth = 0): T { const old = objectCopy(p, depth); + writeIn(p, value); + return old; +} + +/** `value`'s fields, or its items for a JS array, moved into `p`. */ +function writeIn(p: T, value: T): void { if (Array.isArray(p)) arrayAssign(p, value as unknown[]); else ptrWrite(p, value); - return old; } /** diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index 8d0610232..d749adccc 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -1834,7 +1834,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// How many levels of arrays of objects a write may land in `ty` nests, whose items a /// copy of a view of one copies too, unlike references, which move as they are: the /// runtime's `depth`, if any. - fn copy_depth(&self, ty: Ty<'tcx>) -> Option { + pub(super) fn copy_depth(&self, ty: Ty<'tcx>) -> Option { let mut depth = 0; let mut at = ty; while let ty::Array(elem, _) = at.kind() diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index 45dc195cb..32a536015 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -3685,6 +3685,16 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { { *e = value.clone(); } + // A read through a pointer made here of a shared reference is the value it holds. + if let Expr::Call(f, args) = &*e + && let (Expr::Rs(read), [p]) = (&**f, args.as_slice()) + && read == "ptrRead" + && let Expr::Object(members) = step::peel(p) + && let [(ts::Key::Name(v), value)] = members.as_slice() + && v == "v" + { + *e = value.clone(); + } // The elements of a slice of all of an array are the array, and a copy of them its copy. if let Expr::Call(f, args) = &*e && let (Expr::Rs(read), [slice]) = (&**f, args.as_slice()) diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index 252fa63d9..f0b7e266b 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -823,8 +823,15 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { matches!(self.mir.local_decls[place.local].ty.kind(), ty::RawPtr(..)) && !self.box_pointer(place.local); let e = match raw { true => Expr::rs("ptrWrite", vec![target, value]), - false => match self.write_into(target, value, pointee) { - Some(e) => e, + false => match self.b.t.write_in(pointee) { + Some(ObjectKind::Bytes) => target.member("set").call(vec![value]), + // A boxed slice is a JS array, whose length a write changes, unlike a + // tuple's or an array's. + Some(ObjectKind::Array) if self.b.t.erased(pointee).is_slice() => { + Expr::rs("arrayAssign", vec![target, value]) + } + Some(_) if self.b.t.moves_private(pointee) => Expr::rs("ptrWrite", vec![target, value]), + Some(_) => Expr::ident("Object").member("assign").call(vec![target, value]), None => self.err(span, format!("an assignment through a reference to `{pointee}`")), }, }; @@ -837,19 +844,6 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { out } - /// The write of `value`, a whole `pointee`, into `target`, the object a reference to it - /// is, in place, if JS can write one so. - pub(super) fn write_into(&self, target: Expr, value: Expr, pointee: Ty<'tcx>) -> Option { - Some(match self.b.t.write_in(pointee)? { - ObjectKind::Bytes => target.member("set").call(vec![value]), - // A boxed slice is a JS array, whose length a write changes, unlike a tuple's or - // an array's. - ObjectKind::Array if self.b.t.erased(pointee).is_slice() => Expr::rs("arrayAssign", vec![target, value]), - _ if self.b.t.moves_private(pointee) => Expr::rs("ptrWrite", vec![target, value]), - _ => Expr::ident("Object").member("assign").call(vec![target, value]), - }) - } - /// `target = value`, where `target` may be a struct's byte slice field held as its /// parts: each of its fields gets its part, in order, read before any is written where /// a part reads a field written before it. diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index 2918fd1b3..3513f9686 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -280,8 +280,8 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// `rs.Place`, where `r` is no `rs.Place` itself but the value: the place the body shows /// `r` borrowed, as [`Self::borrowed_pointer`] finds it. Otherwise it is a place of its /// own, as `ptr::from_ref` makes: of the value, where `r` is shared and nothing may write - /// a whole value through the pointer, and else writing a whole value into the object `r` - /// is in place, as `*r = v` does. + /// a whole value through the pointer, and else an `rs.placeIn` writing a whole value into + /// the object `r` is in place, as `*r = v` does, which a read moving out copies. pub(super) fn behind_reference(&mut self, r: Place<'tcx>, span: Span, out: &mut Vec) -> Expr { let tcx = self.tcx; if let Some(p) = self.borrowed_pointer(r, span, out) { @@ -303,23 +303,15 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { if shared && (tagged || ty.is_freeze(tcx, env)) { return Expr::Object(vec![(Key::Name("v".into()), value)]); } - let target = self.b.spill(value, "target", out); - let written = self.b.fresh("value"); - let Some(write) = self.write_into(target.clone(), Expr::ident(&written), ty) else { + if self.b.t.write_in(ty).is_none() { return self.err( span, format!("a raw pointer to `{ty}` behind a reference, which JS can't write a whole value into in place"), ); - }; - let get = Expr::Lambda(Vec::new(), Box::new(target)); - let set = Expr::Lambda( - vec![Param { - name: written, - ty: None, - }], - Box::new(write), - ); - Expr::rs("place", vec![get, set]) + } + let mut made = vec![value]; + made.extend(self.copy_depth(ty)); + Expr::rs("placeIn", made) } /// The raw pointer to the place the body shows the reference `r` borrowed, if it does: diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index 340d6e2e9..e046c1e9b 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -1112,11 +1112,11 @@ pub fn raw_pointee<'tcx>(tcx: ty::TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> Expr { let value = match b.t.pointee(ty) { // The value a pointer made here of a shared reference holds. - Pointee::Place => match p { - Expr::Object(mut members) if matches!(members.as_slice(), [(Key::Name(v), _)] if v == "v") => { - members.pop().expect("one member").1 - } - p => p.member("v"), + Pointee::Place => match p.unlocated() { + Expr::Object(members) if let [(Key::Name(v), value)] = members.as_slice() && v == "v" => value.clone(), + // It may be an `rs.placeIn`, whose object the next write through it changes. + _ if b.t.write_in(ty).is_some() => Expr::rs("ptrRead", vec![p]), + _ => p.member("v"), }, Pointee::Object => p, Pointee::Array => { From 0916afe59c7ca6716fe25cf6e3fe69758ec5ab34 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:54:51 -0800 Subject: [PATCH 13/45] chore(ts): regenerate the snapshots after merging main Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/elements/expected/spare.ts | 4 ++-- ts/fixtures/lang/pointers/expected/lib.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ts/fixtures/lang/elements/expected/spare.ts b/ts/fixtures/lang/elements/expected/spare.ts index 33f182bbe..d92a34680 100644 --- a/ts/fixtures/lang/elements/expected/spare.ts +++ b/ts/fixtures/lang/elements/expected/spare.ts @@ -397,7 +397,7 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { let iterStart2: rs.usize = at2; const iterEnd2: rs.usize = end2; while ((_155 = iterStart2++) < iterEnd2) { - put$spare_Item_String(rs.ptrAdd(p2, _155).v, out); + put$spare_Item_String(rs.ptrRead(rs.ptrAdd(p2, _155)), out); } rs.ptrCopy(rs.ptrAdd(p2, end2), rs.ptrAdd(p2, at2), rs.usizeSub(len, end2)); rs.vecSetLen(v, rs.usizeSub(len, rs.usizeSub(end2, at2)), 12, (): rs.String => { @@ -416,7 +416,7 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { const iterEnd3: rs.usize = len; while ((_184 = iterStart3++) < iterEnd3) { const i4: rs.usize = _184; - const x: rs.String = rs.ptrAdd(p3, i4).v; + const x: rs.String = rs.ptrRead(rs.ptrAdd(p3, i4)); if (rs.usizeRem(rs.usizeAdd(i4, n as rs.usize), 3) !== 0) { rs.ptrAdd(p3, kept).v = x; kept = rs.usizeAdd(kept, 1); diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index e0bf9590c..261f55ad2 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, arrayCasts, genericUnions } from "./pointers.ts"; +import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, behindRefs, arrayCasts, genericUnions } from "./pointers.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -302,10 +302,12 @@ export function cases(): rs.Vec<[string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.By rawPlaces(data26, out26); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::arrays", (_236: rs.u8, data27: rs.ByteSlice, out27: rs.ByteVec): undefined => { arrays(data27, out27); - }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::array_casts", (_237: rs.u8, data28: rs.ByteSlice, out28: rs.ByteVec): undefined => { - arrayCasts(data28, out28); - }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::generic_unions", (_238: rs.u8, data29: rs.ByteSlice, out29: rs.ByteVec): undefined => { - genericUnions(data29, out29); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::behind_refs", (_237: rs.u8, data28: rs.ByteSlice, out28: rs.ByteVec): undefined => { + behindRefs(data28, out28); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::array_casts", (_238: rs.u8, data29: rs.ByteSlice, out29: rs.ByteVec): undefined => { + arrayCasts(data29, out29); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::generic_unions", (_239: rs.u8, data30: rs.ByteSlice, out30: rs.ByteVec): undefined => { + genericUnions(data30, out30); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); } From 9bfcf2785ac3c06785330d5c3cd8726c264e1128 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:58:10 -0800 Subject: [PATCH 14/45] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/pointer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index 8615499e0..63ae9914c 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -1132,7 +1132,12 @@ fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> let value = match b.t.pointee(ty) { // The value a pointer made here of a shared reference holds. Pointee::Place => match p.unlocated() { - Expr::Object(members) if let [(Key::Name(v), value)] = members.as_slice() && v == "v" => value.clone(), + Expr::Object(members) + if let [(Key::Name(v), value)] = members.as_slice() + && v == "v" => + { + value.clone() + } // It may be an `rs.placeIn`, whose object the next write through it changes. _ if b.t.write_in(ty).is_some() => Expr::rs("ptrRead", vec![p]), _ => p.member("v"), From 28c5450337e9bc35902ad39f5a9d2617e7cccfa0 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 06:46:19 -0800 Subject: [PATCH 15/45] fix(ts): copy every value a pointer read moves out, library objects included ptr::read, replace, swap, and copy now copy each object they move out that the crate writes into in place, at any depth, a library object such as a Vec one object deep and one with interior mutability always, by extending the struct copies in copy.rs. That replaces the runtime ptrRead and WrittenIn check, which missed pointers from shared references, casts to MaybeUninit and ManuallyDrop, and library objects inside an Option, tuple, or struct. From and Into take the pointer shortcuts only for std's reference-to-NonNull impl, and Into of a reference to an Arc's value is its one place. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- ts/fixtures/lang/conversions/expected/lib.ts | 14 +- ts/fixtures/lang/elements/expected/lib.ts | 4 + ts/fixtures/lang/elements/expected/spare.ts | 12 +- ts/fixtures/lang/locals/expected/locals.ts | 2 +- .../lang/pointers/expected/core/option.ts | 14 + ts/fixtures/lang/pointers/expected/lib.ts | 42 ++- .../pointers/expected/pointers.Drained.ts | 17 +- .../pointers/expected/pointers.Halves32.ts | 11 + .../pointers/expected/pointers.TaggedVec.ts | 11 + .../lang/pointers/expected/pointers.ts | 302 +++++++++++++++--- ts/fixtures/lang/pointers/src/lib.rs | 1 + ts/fixtures/lang/pointers/src/pointers.rs | 155 ++++++++- ts/runtime/rs.ts | 40 +-- ts/src/lower/copy.rs | 21 +- ts/src/lower/mir/call.rs | 10 +- ts/src/lower/mir/mod.rs | 18 +- ts/src/lower/mir/origin.rs | 6 +- ts/src/lower/mir/rvalue.rs | 6 +- ts/src/lower/pointer.rs | 8 +- 20 files changed, 569 insertions(+), 127 deletions(-) create mode 100644 ts/fixtures/lang/pointers/expected/pointers.Halves32.ts create mode 100644 ts/fixtures/lang/pointers/expected/pointers.TaggedVec.ts diff --git a/ts/README.md b/ts/README.md index 181115509..b5b1c65b4 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: 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 | | `&T`, `&mut T`, `Box`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place, which `ptr::read`, `replace`, `swap`, and `copy` move out a copy of. 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 | +| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, and one with interior mutability, such as a `Cell`, always | | `MaybeUninit` | `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 | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/lang/conversions/expected/lib.ts b/ts/fixtures/lang/conversions/expected/lib.ts index 0a7051530..a448b3248 100644 --- a/ts/fixtures/lang/conversions/expected/lib.ts +++ b/ts/fixtures/lang/conversions/expected/lib.ts @@ -5,6 +5,18 @@ import { conversions } from "./conversions.ts"; import * as Logged from "./conversions.Logged.ts"; import type * as Named from "./conversions.Named.ts"; +export function copy$String(v: rs.String): rs.String { + return rs.objectCopy(v); +} + +export function copy$Vec_u32(v: rs.Vec): rs.Vec { + return rs.objectCopy(v); +} + +export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { + return rs.objectCopy(v); +} + export function copy$array_Option_u16(v: Array): Array { return v.slice(); } @@ -46,7 +58,7 @@ export function copy$conversions_Logged(v: Logged.Logged): Logged.Logged { } export function copy$conversions_Named(v: Named.Named): Named.Named { - return { id: v.id, name: v.name }; + return { id: v.id, name: copy$String(v.name) }; } export function drop$tuple2_conversions_Logged_u8(_1: [Logged.Logged, rs.u8]): void { diff --git a/ts/fixtures/lang/elements/expected/lib.ts b/ts/fixtures/lang/elements/expected/lib.ts index 63ac32a2b..408c432b1 100644 --- a/ts/fixtures/lang/elements/expected/lib.ts +++ b/ts/fixtures/lang/elements/expected/lib.ts @@ -9,6 +9,10 @@ export function copy$Option_tuple2_u8_u8(v: [rs.u8, rs.u8] | undefined): [rs.u8, return v === undefined ? v : copy$tuple2_u8_u8(v); } +export function copy$String(v: rs.String): rs.String { + return rs.objectCopy(v); +} + export function copy$array_Option_tuple2_u8_u8(v: Array<[rs.u8, rs.u8] | undefined>): Array<[rs.u8, rs.u8] | undefined> { return v.map(copy$Option_tuple2_u8_u8); } diff --git a/ts/fixtures/lang/elements/expected/spare.ts b/ts/fixtures/lang/elements/expected/spare.ts index 58d456119..103efd939 100644 --- a/ts/fixtures/lang/elements/expected/spare.ts +++ b/ts/fixtures/lang/elements/expected/spare.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$mem_MaybeUninit_tuple2_u8_u32, copy$tuple2_u8_u32 } from "./lib.ts"; +import { copy$String, copy$mem_MaybeUninit_tuple2_u8_u32, copy$tuple2_u8_u32 } from "./lib.ts"; import { thenSome$1gvhopg } from "./core/bool.ts"; import { mapOr$17z0l8m as option_mapOr$17z0l8m } from "./core/option.ts"; import * as Noted from "./spare.Noted.ts"; @@ -365,7 +365,7 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { if (_35 === 1) { rs.ptrCopy(rs.vecAsPtr(rs.iterToVec(rs.iterMap(new rs.Range(0, k), (i5: rs.usize): rs.String => { return of$spare_Item_String(n ^ (i5 & 0xff)); - }), undefined, 12, true), 12), rs.ptrAdd(rs.vecAsPtr(v, 12), len), k); + }), undefined, 12, true), 12), rs.ptrAdd(rs.vecAsPtr(v, 12), len), k, copy$String); rs.vecSetLen(v, rs.usizeAdd(len, k), 12, (): rs.String => { return (undefined as unknown) as rs.String; }); @@ -389,7 +389,7 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { rs.vecReserve(v, 1, 12); const at: rs.usize = rs.usizeRem(n as rs.usize, rs.usizeAdd(len, 1)); const p: rs.Place = rs.ptrAdd(rs.vecAsPtr(v, 12), at); - rs.ptrCopy(p, rs.ptrAdd(p, 1), rs.usizeSub(len, at)); + rs.ptrCopy(p, rs.ptrAdd(p, 1), rs.usizeSub(len, at), copy$String); p.v = of$spare_Item_String(n); rs.vecSetLen(v, rs.usizeAdd(len, 1), 12, (): rs.String => { return (undefined as unknown) as rs.String; @@ -407,9 +407,9 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { let iterStart2: rs.usize = at2; const iterEnd2: rs.usize = end2; while ((_165 = iterStart2++) < iterEnd2) { - put$spare_Item_String(rs.ptrAdd(p2, _165).v, out); + put$spare_Item_String(copy$String(rs.ptrAdd(p2, _165).v), out); } - rs.ptrCopy(rs.ptrAdd(p2, end2), rs.ptrAdd(p2, at2), rs.usizeSub(len, end2)); + rs.ptrCopy(rs.ptrAdd(p2, end2), rs.ptrAdd(p2, at2), rs.usizeSub(len, end2), copy$String); rs.vecSetLen(v, rs.usizeSub(len, rs.usizeSub(end2, at2)), 12, (): rs.String => { return (undefined as unknown) as rs.String; }); @@ -426,7 +426,7 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { const iterEnd3: rs.usize = len; while ((_194 = iterStart3++) < iterEnd3) { const i4: rs.usize = _194; - const x: rs.String = rs.ptrAdd(p3, i4).v; + const x: rs.String = copy$String(rs.ptrAdd(p3, i4).v); if (rs.usizeRem(rs.usizeAdd(i4, n as rs.usize), 3) !== 0) { rs.ptrAdd(p3, kept).v = x; kept = rs.usizeAdd(kept, 1); diff --git a/ts/fixtures/lang/locals/expected/locals.ts b/ts/fixtures/lang/locals/expected/locals.ts index 0e82c8bd5..b64244784 100644 --- a/ts/fixtures/lang/locals/expected/locals.ts +++ b/ts/fixtures/lang/locals/expected/locals.ts @@ -295,7 +295,7 @@ export function keys(data: rs.ByteSlice, out: rs.ByteVec): void { } else if (_12 === 1) { const old = key.v; key.v = RIGHT; - rs.byteVecPush(out, get$09fnt4b({ v: old }.v)); + rs.byteVecPush(out, get$09fnt4b(old)); } else if (_12 === 2) { const other: rs.Place>> = { v: RIGHT }; const swapped = key.v; diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 9244a4c57..67a3f757d 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -83,6 +83,13 @@ export function map$070fw7m(self: rs.u32 | undefined, f: (a0: rs.u32) => rs.u32) return f(self); } +export function map$1px2rk4(self: rs.u8 | undefined, f: (a0: rs.u8) => rs.ByteVec): rs.ByteVec | undefined { + if (self === undefined) { + return undefined; + } + return f(self); +} + export function mapOr$078vnqn(self: Pair.Pair | undefined, default_: rs.u8, f: (a0: Pair.Pair) => rs.u8): rs.u8 { if (self === undefined) { return default_; @@ -97,6 +104,13 @@ export function mapOr$00zno7m(self: Spot.Spot | undefined, default_: Spot.Spot, return f(copy$pointers_Spot(self)); } +export function mapOr$18adzvj(self: rs.Cell | undefined, default_: rs.u32, _: (a0: rs.Cell) => rs.u32): rs.u32 { + if (self === undefined) { + return default_; + } + return self.v; +} + export function mapOr$173j2gr(self: [rs.u8, rs.Arc] | undefined, default_: rs.u8, f: (a0: [rs.u8, rs.Arc]) => rs.u8): rs.u8 { if (self === undefined) { return default_; diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 261f55ad2..16f3e9201 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, behindRefs, arrayCasts, genericUnions } from "./pointers.ts"; +import { lists, shared, places, unchecked, nested, copies, sentinels, cells, rebound, lapsed, writers, made, segments, uninitRuns, iteratedRuns, besideWritten, waiters, taken, bytes, loaded, reattached, partWritten, aimed, distinct, rawArcs, rawPlaces, arrays, behindRefs, movedOut, arrayCasts, genericUnions } from "./pointers.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -18,8 +18,21 @@ import type * as Outer from "./pointers.Outer.ts"; import type * as Pair from "./pointers.Pair.ts"; import type * as Paired from "./pointers.Paired.ts"; import type * as Spot from "./pointers.Spot.ts"; +import type * as TaggedVec from "./pointers.TaggedVec.ts"; import type * as Twin from "./pointers.Twin.ts"; +export function copy$Cell_u32(v: rs.Cell): rs.Cell { + return rs.objectCopy(v); +} + +export function copy$Option_Cell_u32(v: rs.Cell | undefined): rs.Cell | undefined { + return v === undefined ? v : copy$Cell_u32(v); +} + +export function copy$Option_Vec_u8(v: rs.ByteVec | undefined): rs.ByteVec | undefined { + return v === undefined ? v : copy$Vec_u8(v); +} + export function copy$Option_pointers_Pair(v: Pair.Pair | undefined): Pair.Pair | undefined { return v === undefined ? v : copy$pointers_Pair(v); } @@ -28,6 +41,14 @@ export function copy$Option_tuple2_u16_u8(v: [rs.u16, rs.u8] | undefined): [rs.u return v === undefined ? v : copy$tuple2_u16_u8(v); } +export function copy$String(v: rs.String): rs.String { + return rs.objectCopy(v); +} + +export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { + return rs.objectCopy(v); +} + export function copy$array_array2_u32(v: rs.u32[][]): rs.u32[][] { return v.map(copy$array_u32); } @@ -52,6 +73,13 @@ export function copy$array_u8(v: Uint8Array): Uint8Array { return v.slice(); } +export function copy$mem_MaybeUninit_Vec_u8(v: rs.ByteVec): rs.ByteVec { + if (v === undefined) { + return v; + } + return rs.objectCopy(v); +} + export function copy$mem_MaybeUninit_array_u32(v: rs.u32[]): rs.u32[] { if (v === undefined) { return v; @@ -145,10 +173,18 @@ export function copy$pointers_Spot(v: Spot.Spot): Spot.Spot { return { at: copy$tuple2_u16_u8(v.at), tag: v.tag }; } +export function copy$pointers_TaggedVec(v: TaggedVec.TaggedVec): TaggedVec.TaggedVec { + return { v: copy$Vec_u8(v.v), tag: v.tag }; +} + export function copy$pointers_Twin(v: Twin.Twin): Twin.Twin { return { _0: v._0, _1: v._1 }; } +export function copy$tuple2_Vec_u8_u8(v: [rs.ByteVec, rs.u8]): [rs.ByteVec, rs.u8] { + return [copy$Vec_u8(v[0]), v[1]]; +} + export function copy$tuple2_array2_u32_u8(v: [rs.u32[], rs.u8]): [rs.u32[], rs.u8] { return [copy$array_u32(v[0]), v[1]]; } @@ -190,7 +226,7 @@ export function copy$tuple2_u32_u32(v: [rs.u32, rs.u32]): [rs.u32, rs.u32] { } export function copy$tuple2_u8_String(v: [rs.u8, rs.String]): [rs.u8, rs.String] { - return [v[0], v[1]]; + return [v[0], copy$String(v[1])]; } export function copy$tuple2_u8_sync_Arc_u8(v: [rs.u8, rs.Arc]): [rs.u8, rs.Arc] { @@ -308,6 +344,8 @@ export function cases(): rs.Vec<[string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.By arrayCasts(data29, out29); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::generic_unions", (_239: rs.u8, data30: rs.ByteSlice, out30: rs.ByteVec): undefined => { genericUnions(data30, out30); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::moved_out", (_240: rs.u8, data31: rs.ByteSlice, out31: rs.ByteVec): undefined => { + movedOut(data31, out31); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts index 66be08b9e..344d8eaf7 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts @@ -1,6 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; +import { copy$String, copy$Vec_u8 } from "./lib.ts"; import { map$00buaky as option_map$00buaky, map$1lzqy5c as option_map$1lzqy5c, map$1h9tk16 as option_map$1h9tk16, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; /** @@ -13,49 +14,49 @@ export interface Drained { export function next$iter_Iterator_Option_u16(self: Drained): rs.u16 | undefined | rs.Done { return rs.iterFromOption(option_map$00buaky(rs.iterToOption((self.iter as rs.Iter).next(), rs.None), (r: rs.u16 | undefined): rs.u16 | undefined => { - return { v: r }.v; + return r; }), rs.None); } export function next$iter_Iterator_String(self: Drained): rs.String | rs.Done { return rs.iterFromOption(option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.String): rs.String => { - return r; + return copy$String(r); })); } export function next$iter_Iterator_Vec_u8(self: Drained): rs.ByteVec | rs.Done { return rs.iterFromOption(option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.ByteVec): rs.ByteVec => { - return r; + return copy$Vec_u8(r); })); } export function next$iter_Iterator_u32(self: Drained): rs.u32 | rs.Done { return rs.iterFromOption(option_map$070fw7m(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.u32): rs.u32 => { - return { v: r }.v; + return r; })); } export function nextBack$iter_DoubleEndedIterator_Option_u16(self: Drained): rs.u16 | undefined | rs.None { return option_map$00buaky(rs.iterToOption((self.iter as rs.Iter).nextBack(), rs.None), (r: rs.u16 | undefined): rs.u16 | undefined => { - return { v: r }.v; + return r; }); } export function nextBack$iter_DoubleEndedIterator_String(self: Drained): rs.String | undefined { return option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.String): rs.String => { - return r; + return copy$String(r); }); } export function nextBack$iter_DoubleEndedIterator_Vec_u8(self: Drained): rs.ByteVec | undefined { return option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.ByteVec): rs.ByteVec => { - return r; + return copy$Vec_u8(r); }); } export function nextBack$iter_DoubleEndedIterator_u32(self: Drained): rs.u32 | undefined { return option_map$070fw7m(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.u32): rs.u32 => { - return { v: r }.v; + return r; }); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.Halves32.ts b/ts/fixtures/lang/pointers/expected/pointers.Halves32.ts new file mode 100644 index 000000000..2698afc9d --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.Halves32.ts @@ -0,0 +1,11 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; + +/** Two numbers, the second of which a pointer from a reference to them points at. */ +export interface Halves32 { + first: rs.u32; + second: rs.u32; +} + +//# sourceMappingURL=pointers.Halves32.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.TaggedVec.ts b/ts/fixtures/lang/pointers/expected/pointers.TaggedVec.ts new file mode 100644 index 000000000..4c23eb3aa --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.TaggedVec.ts @@ -0,0 +1,11 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; + +/** A `Vec` with a tag, which a pointer to one moves out whole. */ +export interface TaggedVec { + v: rs.ByteVec; + tag: rs.u8; +} + +//# sourceMappingURL=pointers.TaggedVec.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 4054d996b..50cc2073d 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,9 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_Twin, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; -import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; +import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -16,6 +16,7 @@ import type * as Either from "./pointers.Either.ts"; import type * as Enclosed from "./pointers.Enclosed.ts"; import type * as Gauge from "./pointers.Gauge.ts"; import type * as Halves from "./pointers.Halves.ts"; +import type * as Halves32 from "./pointers.Halves32.ts"; import * as Held from "./pointers.Held.ts"; import type * as Holder from "./pointers.Holder.ts"; import * as Housed from "./pointers.Housed.ts"; @@ -41,6 +42,7 @@ import type * as Span from "./pointers.Span.ts"; import type * as Spot from "./pointers.Spot.ts"; import type * as Stable from "./pointers.Stable.ts"; import type * as Swapped from "./pointers.Swapped.ts"; +import type * as TaggedVec from "./pointers.TaggedVec.ts"; import type * as To from "./pointers.To.ts"; import type * as Twin from "./pointers.Twin.ts"; import type * as Viewed from "./pointers.Viewed.ts"; @@ -2839,13 +2841,13 @@ export function rawPlaces(data: rs.ByteSlice, out: rs.ByteVec): void { })) { shelf = second; } - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of({ v: value }.v & 0xff, shelf._0.value & 0xff)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(value & 0xff, shelf._0.value & 0xff)), undefined, true); let shelf2: Shelf.Shelf = first; const value2: rs.u32 = shelf2._0.value; if (rs.usizeRem(data.length, 3) === 0) { shelf2 = second; } - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of({ v: value2 }.v & 0xff, shelf2._0.value & 0xff)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(value2 & 0xff, shelf2._0.value & 0xff)), undefined, true); second[Symbol.dispose](); first[Symbol.dispose](); flag[Symbol.dispose](); @@ -2984,7 +2986,8 @@ export function lent(n: rs.u8): rs.ByteVec { /** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ export function drained$Option_u16(v: rs.Vec, bits: rs.u8): rs.Vec { let next!: rs.u16 | undefined | rs.None; - const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const v2: rs.Vec = v; + const iter: Drained.Drained = { iter: rs.iterSlice(v2.items) as rs.Iter> }; const out: rs.Vec = new rs.Vec(); let i: rs.i32 = 0; while (true) { @@ -2997,6 +3000,9 @@ export function drained$Option_u16(v: rs.Vec, bits: rs.u8): out.items.push(next); i = rs.i32Add(i, 1); } else { + rs.vecSetLen(v2, 0, 4, (): rs.u16 | undefined => { + return (undefined as unknown) as rs.u16 | undefined; + }); return out; } } @@ -3005,7 +3011,8 @@ export function drained$Option_u16(v: rs.Vec, bits: rs.u8): /** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { let next!: rs.String | undefined; - const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const v2: rs.Vec = v; + const iter: Drained.Drained = { iter: rs.iterSlice(v2.items) as rs.Iter> }; const out: rs.Vec = new rs.Vec(); let i: rs.i32 = 0; while (true) { @@ -3018,6 +3025,9 @@ export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { + return (undefined as unknown) as rs.String; + }); return out; } } @@ -3026,7 +3036,8 @@ export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec, bits: rs.u8): rs.Vec { let next!: rs.ByteVec | undefined; - const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const v2: rs.Vec = v; + const iter: Drained.Drained = { iter: rs.iterSlice(v2.items) as rs.Iter> }; const out: rs.Vec = new rs.Vec(); let i: rs.i32 = 0; while (true) { @@ -3039,6 +3050,9 @@ export function drained$Vec_u8(v: rs.Vec, bits: rs.u8): rs.Vec { + return (undefined as unknown) as rs.ByteVec; + }); return out; } } @@ -3047,7 +3061,8 @@ export function drained$Vec_u8(v: rs.Vec, bits: rs.u8): rs.Vec, bits: rs.u8): rs.Vec { let next!: rs.u32 | undefined; - const iter: Drained.Drained = { iter: rs.iterSlice(v.items) as rs.Iter> }; + const v2: rs.Vec = v; + const iter: Drained.Drained = { iter: rs.iterSlice(v2.items) as rs.Iter> }; const out: rs.Vec = new rs.Vec(); let i: rs.i32 = 0; while (true) { @@ -3060,11 +3075,39 @@ export function drained$u32(v: rs.Vec, bits: rs.u8): rs.Vec { out.items.push(next); i = rs.i32Add(i, 1); } else { + rs.vecSetLen(v2, 0, 4, (): rs.u32 => { + return (undefined as unknown) as rs.u32; + }); return out; } } } +/** + * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then + * drops or moves to the front. + */ +export function retained$Rc_pointers_Counted_pointers_behind_refs_closure10(v: rs.Vec>, keep: (a0: rs.Place>) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place> = rs.vecAsPtr(v, 4); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place> = rs.ptrAdd(p, _10); + if (!keep(_15)) { + _15.v[Symbol.dispose](); + } else { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSetLen(v, write, 4, (): rs.Arc => { + return (undefined as unknown) as rs.Arc; + }); +} + /** * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then * drops or moves to the front. @@ -3079,11 +3122,13 @@ export function retained$String_pointers_behind_refs_closure6(v: rs.Vec = rs.ptrAdd(p, _10); if (keep(_15.v)) { - rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1, copy$String); write = rs.usizeAdd(write, 1); } } - rs.vecSplitOff(v, write); + rs.vecSetLen(v, write, 12, (): rs.String => { + return (undefined as unknown) as rs.String; + }); } /** @@ -3100,11 +3145,13 @@ export function retained$Vec_u8_pointers_behind_refs_closure8(v: rs.Vec = rs.ptrAdd(p, _10); if (keep(_15.v)) { - rs.ptrCopy(_15, rs.ptrAdd(p, write), 1); + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1, copy$Vec_u8); write = rs.usizeAdd(write, 1); } } - rs.vecSplitOff(v, write); + rs.vecSetLen(v, write, 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; + }); } /** @@ -3125,12 +3172,14 @@ export function retained$u32_pointers_behind_refs_closure4(v: rs.Vec, ke write = rs.usizeAdd(write, 1); } } - rs.vecSplitOff(v, write); + rs.vecSetLen(v, write, 4, (): rs.u32 => { + return (undefined as unknown) as rs.u32; + }); } /** The length of the `Vec` a pointer from a reference the body is given points at. */ export function vecLen(v: rs.ByteVec): rs.usize { - return { v }.v.length; + return v.length; } /** @@ -3164,23 +3213,23 @@ export function replaced(v: rs.ByteVec, w: rs.ByteVec, with_: rs.ByteVec, how: r const _7: rs.u8 = rs.u8Rem(how, 3); if (_7 === 0) { const pointer = p; - const old3 = rs.ptrRead(pointer); + const old3 = copy$Vec_u8(pointer.v); pointer.v = with_; old = old3; } else if (_7 === 1) { - const old2: rs.ByteVec = rs.ptrRead(p); + const old2: rs.ByteVec = copy$Vec_u8(p.v); p.v = with_; old = old2; } else { let slot: rs.ByteVec = (undefined as unknown) as rs.ByteVec; - rs.ptrCopy(p, rs.place(() => slot, (value) => slot = value), 1); + rs.ptrCopy(p, rs.place(() => slot, (value) => slot = value), 1, copy$Vec_u8); p.v = with_; old = slot; } const x = rs.placeIn(v); const y = rs.placeIn(w); - const old4 = rs.ptrRead(x); - x.v = rs.ptrRead(y); + const old4 = copy$Vec_u8(x.v); + x.v = copy$Vec_u8(y.v); y.v = old4; return old; } @@ -3229,8 +3278,8 @@ export function cellBehind(r: rs.Cell | undefined, how: rs.u8): rs.u32 { * from references to an `Arc`'s value, measured against each other. */ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { - let _112!: rs.usize; - let _152!: rs.u8; + let _133!: rs.usize; + let _173!: rs.u8; const bits: rs.u8 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0); const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): rs.u32 => { return rs.u32Mul(_2 as rs.u32, 3); @@ -3264,9 +3313,9 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { })), undefined, true); } const nums2: rs.Vec = nums; - retained$u32_pointers_behind_refs_closure4(nums2, (n2: rs.Place): boolean => { - n2.v = rs.u32Add(n2.v, 1); - return rs.u32Rem(n2.v, 2) === 0; + retained$u32_pointers_behind_refs_closure4(nums2, (n3: rs.Place): boolean => { + n3.v = rs.u32Add(n3.v, 1); + return rs.u32Rem(n3.v, 2) === 0; }); rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(nums2.items), (_26: rs.u32): rs.u8 => { return _26 & 0xff; @@ -3285,17 +3334,30 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { return rs.u8Rem(r2.data[rs.idx(r2, 0)], 2) === 0; }); rs.iterExtendBytes(out, rs.iterFlatten(rs.iterSlice(runs2.items), (value: rs.ByteVec) => rs.iterSlice(rs.byteVecView(value)), true), "next"); - const nums3: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_27: rs.u8): rs.u32 => { - return _27 as rs.u32; + const dropped: rs.Cell = new rs.Cell(0); + const counted: rs.Vec> = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_27: rs.u8): rs.Arc => { + return new rs.Arc(new Counted.Counted(_27, dropped)); + }), undefined, 4, true); + retained$Rc_pointers_Counted_pointers_behind_refs_closure10(counted, (c: rs.Place>): boolean => { + return rs.u8Rem(c.v.value.id, 2) === 0; + }); + rs.byteVecPush(out, dropped.v); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(counted.items), (c2: rs.Arc): rs.u8 => { + return c2.value.id; + }), undefined, true); + rs.dropVec(counted); + rs.byteVecPush(out, dropped.v); + const nums3: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_28: rs.u8): rs.u32 => { + return _28 as rs.u32; }), undefined, 4, true); if (rs.sliceFrom(nums3.items).length >= 2) { const first: rs.Place = rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 0)); rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, rs.usizeSub(nums3.items.length, 1))), first) & 0xff); - const _109: rs.usize = nums3.items.length; + const _130: rs.usize = nums3.items.length; let iterStart: rs.usize = 0; - const iterEnd: rs.usize = _109; - while ((_112 = iterStart++) < iterEnd) { - rs.byteVecPush(out, rs.ptrAdd(first, _112).v & 0xff); + const iterEnd: rs.usize = _130; + while ((_133 = iterStart++) < iterEnd) { + rs.byteVecPush(out, rs.ptrAdd(first, _133).v & 0xff); } rs.byteVecPush(out, rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v & 0xff); rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v = 9; @@ -3303,42 +3365,200 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { } const v: rs.ByteVec = rs.byteSliceToByteVec(data); rs.byteVecPush(out, vecLen(v) & 0xff); - const _143: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); - refilled(v, _143, bits); + const _164: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); + refilled(v, _164, bits); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); let iterStart2: rs.u8 = 0; const iterEnd2: rs.u8 = 3; - while ((_152 = iterStart2++) < iterEnd2) { - const how: rs.u8 = _152; + while ((_173 = iterStart2++) < iterEnd2) { + const how: rs.u8 = _173; const w2: rs.ByteVec = rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b6: rs.u8): rs.u8 => { return b6 ^ how; }), undefined, true); - const _164: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); - rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w2, _164, how))), undefined, true); + const _185: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w2, _185, how))), undefined, true); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(w2)), undefined, true); rs.byteVecPush(out, 255); } const iter6: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); while (true) { - const _179: [rs.usize, rs.u8] | rs.Done = iter6.next(); - if (_179 === rs.DONE) { + const _200: [rs.usize, rs.u8] | rs.Done = iter6.next(); + if (_200 === rs.DONE) { break; } - const i2: rs.usize = _179[0]; - const b: rs.u8 = _179[1]; + const i2: rs.usize = _200[0]; + const b: rs.u8 = _200[1]; rs.byteVecPush(out, behind(b as rs.u32, i2 & 0xff) & 0xff); rs.byteVecPush(out, cellBehind(then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { return new rs.Cell(b as rs.u32); }), b) & 0xff); } const arc: rs.Arc = new rs.Arc(bits as rs.u32); + const p: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); const q: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); - rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc, rs.arcPlace(arc), "Arc"), q) & 0xff); + rs.byteVecPush(out, rs.ptrOffsetFrom(p, q) & 0xff); rs.byteVecPush(out, q.v & 0xff); + rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc, rs.arcPlace(arc), "Arc"), p) & 0xff); + rs.byteVecPush(out, from$convert_From_ref_pointers_Halves32_ptr_NonNull_u32({ first: bits as rs.u32, second: ((bits as rs.u32) ^ 85) >>> 0 }).v & 0xff); arc[Symbol.dispose](); } +export function from$convert_From_ref_pointers_Halves32_ptr_NonNull_u32(halves: Halves32.Halves32): rs.Place { + return rs.place(() => halves.second, (value) => halves.second = value); +} + +/** + * `with` written whole into the `Vec` `r` is, in place, dropping nothing, as a body that + * can't see where it is kept writes it. + */ +export function refillIn(r: rs.ByteVec, with_: rs.ByteVec): void { + rs.placeIn(r).v = with_; +} + +/** + * What the `Vec` `v` held, moved out through a pointer made each way `how` picks: from a + * shared reborrow, cast to one to a `MaybeUninit` or a `ManuallyDrop` of it, or swapped + * through such a cast with another; then `with` is written whole into `v` in place. + */ +export function movedVec(v: rs.ByteVec, with_: rs.ByteVec, how: rs.u8): rs.ByteVec { + let old!: rs.ByteVec; + const _5: rs.u8 = rs.u8Rem(how, 4); + if (_5 === 0) { + old = copy$Vec_u8(v); + } else if (_5 === 1) { + old = copy$mem_MaybeUninit_Vec_u8(rs.placeIn(v).v); + } else if (_5 === 2) { + old = copy$Vec_u8(rs.placeIn(v).v); + } else { + let other: rs.ByteVec = rs.byteVecFrom(Uint8Array.of(how)); + const x = rs.placeIn(v); + const y = rs.place(() => other, (value) => other = value); + const old2 = copy$mem_MaybeUninit_Vec_u8(x.v); + x.v = copy$mem_MaybeUninit_Vec_u8(y.v); + y.v = old2; + old = other; + } + refillIn(v, with_); + return old; +} + +/** + * What an `Option`, a tuple, and a struct each holding a `Vec` held, moved out through + * pointers from `&mut`s to them: read, or copied, replaced, and swapped, as `how` picks; + * then `with` is written whole into each `Vec` they hold, in place. + */ +export function movedNested(o: rs.Place, t: [rs.ByteVec, rs.u8], s: TaggedVec.TaggedVec, with_: rs.ByteSlice, how: rs.u8): [rs.ByteVec | undefined, [rs.ByteVec, rs.u8], TaggedVec.TaggedVec] { + let old!: [rs.ByteVec | undefined, [rs.ByteVec, rs.u8], TaggedVec.TaggedVec]; + if (rs.u8Rem(how, 2) === 0) { + old = [copy$Option_Vec_u8(o.v), copy$tuple2_Vec_u8_u8(t), copy$pointers_TaggedVec(s)] as [rs.ByteVec | undefined, [rs.ByteVec, rs.u8], TaggedVec.TaggedVec]; + } else { + let slot: rs.ByteVec | undefined = (undefined as unknown) as rs.ByteVec | undefined; + rs.ptrCopy(o, rs.place(() => slot, (value) => slot = value), 1, copy$Option_Vec_u8); + const _22: [rs.ByteVec, rs.u8] = t; + const _23: [rs.ByteVec, rs.u8] = [rs.byteVecFrom(Uint8Array.of(how)), how]; + const pointer = _22; + const old2 = copy$tuple2_Vec_u8_u8(pointer); + rs.ptrWrite(pointer, _23); + const oldT: [rs.ByteVec, rs.u8] = old2; + const oldS: TaggedVec.TaggedVec = { v: rs.byteVecFrom(Uint8Array.of(how)), tag: how }; + const x = s; + const y = oldS; + const old3 = copy$pointers_TaggedVec(x); + rs.ptrWrite(x, copy$pointers_TaggedVec(y)); + rs.ptrWrite(y, old3); + old = [slot, oldT, oldS] as [rs.ByteVec | undefined, [rs.ByteVec, rs.u8], TaggedVec.TaggedVec]; + } + if (o.v !== undefined) { + const _39: rs.ByteVec = rs.byteSliceToByteVec(with_); + refillIn(o.v, _39); + } + const _42: rs.ByteVec = rs.byteSliceToByteVec(with_); + refillIn(t[0], _42); + const _45: rs.ByteVec = rs.byteSliceToByteVec(with_); + refillIn(s.v, _45); + return old; +} + +/** + * The `Option` of a cell a shared reference points at, moved out through a pointer from + * it, whose cell is then set. + */ +export function cellMoved(r: rs.Cell | undefined): rs.Cell | undefined { + const old: rs.Cell | undefined = copy$Option_Cell_u32(r); + if (old !== undefined) { + rs.cellSet(old, rs.u32Add(old.v, 5)); + return old; + } + return old; +} + +/** + * Values holding a library object, moved out through pointers made each way, at any + * depth, and then written in place: what was moved out keeps what it held. + */ +export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { + let _7!: rs.u8; + let _28!: rs.u8; + const with_: (a0: rs.u8) => rs.ByteVec = (n: rs.u8): rs.ByteVec => { + return rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b2: rs.u8): rs.u8 => { + return rs.u8WrappingAdd(b2, n); + }), undefined, true); + }; + let iterStart: rs.u8 = 0; + const iterEnd: rs.u8 = 4; + while ((_7 = iterStart++) < iterEnd) { + const how: rs.u8 = _7; + const v: rs.ByteVec = rs.byteSliceToByteVec(data); + const _14: rs.ByteVec = with_(rs.u8Add(how, 1)); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(movedVec(v, _14, how))), undefined, true); + rs.byteVecPush(out, 254); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); + rs.byteVecPush(out, 255); + } + let iterStart2: rs.u8 = 0; + const iterEnd2: rs.u8 = 2; + while ((_28 = iterStart2++) < iterEnd2) { + const how2: rs.u8 = _28; + let o: rs.ByteVec | undefined = option_map$1px2rk4(rs.byteSliceFirst(data), (_2: rs.u8): rs.ByteVec => { + return rs.byteSliceToByteVec(data); + }); + const t: [rs.ByteVec, rs.u8] = [rs.byteSliceToByteVec(data), how2]; + const s: TaggedVec.TaggedVec = { v: rs.byteSliceToByteVec(data), tag: how2 }; + const _46: rs.ByteSlice = rs.byteVecSlice(with_(9)); + const _42: [rs.ByteVec | undefined, [rs.ByteVec, rs.u8], TaggedVec.TaggedVec] = movedNested(rs.place(() => o, (value) => o = value), t, s, _46, how2); + const oldO: rs.ByteVec | undefined = _42[0]; + const oldT: [rs.ByteVec, rs.u8] = _42[1]; + const oldS: TaggedVec.TaggedVec = _42[2]; + const iter3: rs.Iter = rs.iterFlatten(rs.iterSlice([oldO, oldT[0], oldS.v, o, t[0], s.v]), (value: rs.ByteVec | undefined) => rs.iterOption(value), true, undefined, true); + while (true) { + const _68: rs.ByteVec | rs.Done = iter3.next(); + if (_68 === rs.DONE) { + break; + } + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(_68)), undefined, true); + rs.byteVecPush(out, 254); + } + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(oldT[1], oldS.tag, t[1], s.tag)), undefined, true); + } + for (const b of rs.byteSliceView(data)) { + const cell: rs.Cell | undefined = then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { + return new rs.Cell(b as rs.u32); + }); + const old2: rs.Cell | undefined = cellMoved(cell); + const iter5: rs.Iter | undefined> = rs.iterSlice([cell, old2]); + while (true) { + const _99: rs.Cell | undefined | rs.Done = iter5.next(); + if (_99 === rs.DONE) { + break; + } + rs.byteVecPush(out, option_mapOr$18adzvj(_99, 0, (a0: rs.Cell): rs.u32 => { + return a0.v; + }) & 0xff); + } + } +} + /** * Pointers to slices cast to ones to arrays of their elements, of bytes and of numbers: * read whole, borrowed, and written through, at every offset; compared with the arrays diff --git a/ts/fixtures/lang/pointers/src/lib.rs b/ts/fixtures/lang/pointers/src/lib.rs index 793f2957c..0da7b29d8 100644 --- a/ts/fixtures/lang/pointers/src/lib.rs +++ b/ts/fixtures/lang/pointers/src/lib.rs @@ -54,5 +54,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("pointers::generic_unions", |_, data, out| { pointers::generic_unions(data, out) }), + ("pointers::moved_out", |_, data, out| pointers::moved_out(data, out)), ] } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index a4817f92c..f3dbee1c6 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2446,6 +2446,9 @@ fn drained(v: Vec, bits: u8) -> Vec { out.push(next); i += 1; } + // Its elements moved out, its room is freed without dropping them again. + let mut v = mem::ManuallyDrop::into_inner(v); + unsafe { v.set_len(0) }; out } @@ -2464,8 +2467,8 @@ fn retained(v: &mut Vec, mut keep: impl FnMut(&mut T) -> bool) { write += 1; } } - // What `set_len` leaves past the length, which nothing drops again. - mem::forget(v.split_off(write)); + // What is past the length, dropped or moved, which nothing drops again. + unsafe { v.set_len(write) }; } /// The length of the `Vec` a pointer from a reference the body is given points at. @@ -2576,6 +2579,17 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { r[0] % 2 == 0 }); out.extend(runs.iter().flatten()); + // Each element it drops, dropped once. + let dropped = Cell::new(0); + let mut counted: Vec> = data + .iter() + .map(|&id| Rc::new(Counted { id, dropped: &dropped })) + .collect(); + retained(&mut counted, |c| c.id % 2 == 0); + out.push(dropped.get()); + out.extend(counted.iter().map(|c| c.id)); + drop(counted); + out.push(dropped.get()); // From a reference the body borrowed, where the element is. let mut nums: Vec = data.iter().map(|&b| u32::from(b)).collect(); @@ -2616,6 +2630,143 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { let q = ptr::from_ref(&*arc); out.push(unsafe { p.offset_from(q) } as u8); out.push(unsafe { *q } as u8); + let n: NonNull = (&*arc).into(); + out.push(unsafe { n.as_ptr().offset_from(p) } as u8); + // A conversion of the crate's own, which runs as written. + let halves = Halves32 { + first: u32::from(bits), + second: u32::from(bits) ^ 0x55, + }; + let second: NonNull = (&halves).into(); + out.push(unsafe { *second.as_ptr() } as u8); +} + +/// Two numbers, the second of which a pointer from a reference to them points at. +struct Halves32 { + first: u32, + second: u32, +} + +impl From<&Halves32> for NonNull { + fn from(halves: &Halves32) -> Self { + let _ = halves.first; + NonNull::from(&halves.second) + } +} + +/// `with` written whole into the `Vec` `r` is, in place, dropping nothing, as a body that +/// can't see where it is kept writes it. +#[inline(never)] +fn refill_in(r: &mut Vec, with: Vec) { + unsafe { ptr::write(r, with) }; +} + +/// What the `Vec` `v` held, moved out through a pointer made each way `how` picks: from a +/// shared reborrow, cast to one to a `MaybeUninit` or a `ManuallyDrop` of it, or swapped +/// through such a cast with another; then `with` is written whole into `v` in place. +fn moved_vec(v: &mut Vec, with: Vec, how: u8) -> Vec { + let old = match how % 4 { + 0 => unsafe { ptr::read(&*v as *const Vec) }, + 1 => unsafe { ptr::read(ptr::from_mut(v).cast::>>()).assume_init() }, + 2 => mem::ManuallyDrop::into_inner(unsafe { ptr::read(ptr::from_mut(v).cast::>>()) }), + _ => { + let mut other = MaybeUninit::new(vec![how]); + unsafe { ptr::swap(ptr::from_mut(v).cast::>>(), &mut other) }; + unsafe { other.assume_init() } + } + }; + refill_in(v, with); + old +} + +/// A `Vec` with a tag, which a pointer to one moves out whole. +struct TaggedVec { + v: Vec, + tag: u8, +} + +/// What an `Option`, a tuple, and a struct each holding a `Vec` held, moved out through +/// pointers from `&mut`s to them: read, or copied, replaced, and swapped, as `how` picks; +/// then `with` is written whole into each `Vec` they hold, in place. +fn moved_nested( + o: &mut Option>, + t: &mut (Vec, u8), + s: &mut TaggedVec, + with: &[u8], + how: u8, +) -> (Option>, (Vec, u8), TaggedVec) { + let old = match how % 2 { + 0 => unsafe { (ptr::read(o), ptr::read(t), ptr::read(s)) }, + _ => unsafe { + let mut slot = MaybeUninit::uninit(); + ptr::copy_nonoverlapping(ptr::from_mut(o), slot.as_mut_ptr(), 1); + let old_t = ptr::replace(ptr::from_mut(t), (vec![how], how)); + let mut old_s = TaggedVec { v: vec![how], tag: how }; + ptr::swap(ptr::from_mut(s), &mut old_s); + (slot.assume_init(), old_t, old_s) + }, + }; + if let Some(v) = o { + refill_in(v, with.to_vec()); + } + refill_in(&mut t.0, with.to_vec()); + refill_in(&mut s.v, with.to_vec()); + old +} + +/// The `Option` of a cell a shared reference points at, moved out through a pointer from +/// it, whose cell is then set. +fn cell_moved(r: &Option>) -> Option> { + let old = unsafe { ptr::read(r) }; + if let Some(cell) = &old { + cell.set(cell.get() + 5); + } + old +} + +/// Values holding a library object, moved out through pointers made each way, at any +/// depth, and then written in place: what was moved out keeps what it held. +pub fn moved_out(data: &[u8], out: &mut Vec) { + let with = |n: u8| -> Vec { data.iter().map(|b| b.wrapping_add(n)).collect() }; + for how in 0..4 { + let mut v = data.to_vec(); + let old = moved_vec(&mut v, with(how + 1), how); + out.extend(&old); + out.push(0xfe); + out.extend(&v); + out.push(0xff); + } + for how in 0..2 { + let mut o = data.first().map(|_| data.to_vec()); + let mut t = (data.to_vec(), how); + let mut s = TaggedVec { + v: data.to_vec(), + tag: how, + }; + let (old_o, old_t, old_s) = moved_nested(&mut o, &mut t, &mut s, &with(9), how); + for v in [ + old_o.as_ref(), + Some(&old_t.0), + Some(&old_s.v), + o.as_ref(), + Some(&t.0), + Some(&s.v), + ] + .into_iter() + .flatten() + { + out.extend(v); + out.push(0xfe); + } + out.extend([old_t.1, old_s.tag, t.1, s.tag]); + } + for &b in data { + let cell = (b % 3 != 0).then(|| Cell::new(u32::from(b))); + let old = cell_moved(&cell); + for c in [&cell, &old] { + out.push(c.as_ref().map_or(0, Cell::get) as u8); + } + } } /// Pointers to slices cast to ones to arrays of their elements, of bytes and of numbers: diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index 658e1068d..f75430d95 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -143,40 +143,11 @@ export function place(get: () => T, set: (value: T) => void): Place { } /** - * A place that is the object `target`, which a write of a whole value lands in, as - * `replaceIn` writes: a raw pointer from a `&mut` to an object, which others may hold. + * The raw pointer from a `&mut` to `target`, an object others may hold, which a write of a + * whole value lands in, as `replaceIn` writes. */ -class WrittenIn implements Place { - readonly target: T; - /** The levels of arrays whose items a copy of `target` copies too, as `objectCopy`'s. */ - readonly depth: number; - - constructor(target: T, depth: number) { - this.target = target; - this.depth = depth; - } - - get v(): T { - return this.target; - } - - set v(value: T) { - writeIn(this.target, value); - } -} - -/** The raw pointer from a `&mut` to `target`, an object, as `WrittenIn` is. */ -export function placeIn(target: T, depth = 0): Place { - return new WrittenIn(target, depth); -} - -/** - * What a read through `p` moves out, as `ptr::read` does: what it holds, or a copy of the - * object a `placeIn` is, which a write through it would change. - */ -export function ptrRead(p: Place): T { - const home = ptrHome(p); - return home instanceof WrittenIn ? (objectCopy(home.target, home.depth) as T) : p.v; +export function placeIn(target: T): Place { + return place(() => target, (value) => writeIn(target, value)); } // Raw pointers @@ -483,9 +454,6 @@ export function ptrCopy(src: Place, dst: Place, n: number, copy?: (valu return; } const values = Array.from({ length: n }, (_, i) => elemGet(from.array, from.index + i)); - // What a `placeIn` holds changes with the next write through it. - const home = ptrHome(from); - if (home instanceof WrittenIn) values[0] = objectCopy(home.target, home.depth) as T; // Memory nothing wrote yet is copied as it is, which no value's copy can read. for (let i = 0; i < n; i++) elemSet(to.array, to.index + i, copy && values[i] !== undefined ? copy(values[i]) : values[i]); } diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 2bd85b9a3..48ad20b8c 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -1,10 +1,13 @@ -//! Copies of `Copy` aggregates, by their type. +//! Copies of `Copy` aggregates, and of values a read through a pointer moves out, by +//! their type. //! //! JS shares an object where Rust copies its value, so a `Copy` value that is a JS object //! the crate's code writes into in place, or compares pointers to, is copied where Rust -//! copies it. A function made for its type copies what it holds by value and shares what -//! it holds by reference, as Rust does. A value of a type nothing writes into or compares -//! pointers to never changes once made, so sharing it is a copy no one can tell apart. +//! copies it, as is each such object a read through a pointer moves out, a library +//! object such as a `Vec` included, one object deep. A function made for its type copies +//! what it holds by value and shares what it holds by reference, as Rust does. A value of +//! a type nothing writes into or compares pointers to never changes once made, so sharing +//! it is a copy no one can tell apart. use std::cell::RefCell; @@ -56,6 +59,14 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { // Records of functions and pointers, which JS shares. Some(Std::WakerVTable | Std::RawWaker) => (true, Vec::new()), Some(Std::MaybeUninit | Std::ManuallyDrop) => return self.changes(args.type_at(0)), + // A library object a whole write lands in in place, which a copy copies one + // object deep; through a shared reference too, where it has interior + // mutability, which no write the crate shows records. + Some(_) if self.write_in(ty).is_some() => { + return self.writes.contains(&self.writes.into, false, self.tcx, ty) + || self.addressed(ty) + || !ty.is_freeze(self.tcx, self.typing_env()); + } Some(_) => return false, None if self.own(adt.did()) && adt.all_fields().next().is_some() => { (true, adt.all_fields().map(|f| self.field_ty(f.did, args)).collect()) @@ -317,6 +328,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { _ => self.copy_result(value, residual, span), }) } + // A library object, which [`Self::changes`] says a whole write lands in. + Some(_) => ret(Expr::rs("objectCopy", vec![v()])), _ => { let info = self.adt(*adt, span); match info.repr { diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index d749adccc..fdc95fd7e 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -526,16 +526,16 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let done = |e: Expr, effect: bool| Some(Some((e, effect))); match (owner.as_str(), name.as_str()) { // A pointer from a reference to an `Arc`'s value, which is that value's one place. - ("ptr", "from_ref") | ("From", "from") - if (owner == "ptr" - || crate::lower::pointer::raw_pointee(tcx, dest.ty(&self.mir.local_decls, tcx).ty).is_some()) + ("ptr", "from_ref") | ("From", "from") | ("Into", "into") + if crate::lower::pointer::from_reference(tcx, def_id, gargs) && let Some(p) = self.arc_raw(&args[0].node, span, out) => { done(p, false) } // A pointer from a reference to a value JS keeps in a place, as `&raw const *r` makes. ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") - if let Some(r) = args[0].node.place() + if crate::lower::pointer::from_reference(tcx, def_id, gargs) + && let Some(r) = args[0].node.place() && crate::lower::pointer::raw_pointee(tcx, dest.ty(&self.mir.local_decls, tcx).ty) .is_some_and(|pointee| self.behind_place(r, pointee)) => { @@ -1834,7 +1834,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// How many levels of arrays of objects a write may land in `ty` nests, whose items a /// copy of a view of one copies too, unlike references, which move as they are: the /// runtime's `depth`, if any. - pub(super) fn copy_depth(&self, ty: Ty<'tcx>) -> Option { + fn copy_depth(&self, ty: Ty<'tcx>) -> Option { let mut depth = 0; let mut at = ty; while let ty::Array(elem, _) = at.kind() diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index 32a536015..e1f6b6096 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -3634,6 +3634,14 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { { Some(payload.clone()) } + // A pointer made here of a shared reference holds its value. + (Expr::Object(members), ts::Key::Name(key)) + if key == "v" + && let [(ts::Key::Name(v), value)] = members.as_slice() + && v == "v" => + { + Some(value.clone()) + } (_, ts::Key::Name(key)) if key == "v" => step::peel(base).place_read().cloned(), _ => None, }; @@ -3685,16 +3693,6 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { { *e = value.clone(); } - // A read through a pointer made here of a shared reference is the value it holds. - if let Expr::Call(f, args) = &*e - && let (Expr::Rs(read), [p]) = (&**f, args.as_slice()) - && read == "ptrRead" - && let Expr::Object(members) = step::peel(p) - && let [(ts::Key::Name(v), value)] = members.as_slice() - && v == "v" - { - *e = value.clone(); - } // The elements of a slice of all of an array are the array, and a copy of them its copy. if let Expr::Call(f, args) = &*e && let (Expr::Rs(read), [slice]) = (&**f, args.as_slice()) diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 6a21391ae..94a781cb3 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -209,10 +209,12 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { _ => None, }; } - let (def_id, _, args) = self.def_call(local)?; + let (def_id, gargs, args) = self.def_call(local)?; let (owner, name) = library::key(self.tcx, def_id)?; match (owner.as_str(), name.as_str()) { - ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") => { + ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") + if crate::lower::pointer::from_reference(self.tcx, def_id, gargs) => + { let block = self.def_block(local)?; let at = Location { block, diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index a97d2ac79..9087d0788 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -281,7 +281,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// `r` borrowed, as [`Self::borrowed_pointer`] finds it. Otherwise it is a place of its /// own, as `ptr::from_ref` makes: of the value, where `r` is shared and nothing may write /// a whole value through the pointer, and else an `rs.placeIn` writing a whole value into - /// the object `r` is in place, as `*r = v` does, which a read moving out copies. + /// the object `r` is in place, as `*r = v` does. pub(super) fn behind_reference(&mut self, r: Place<'tcx>, span: Span, out: &mut Vec) -> Expr { let tcx = self.tcx; if let Some(p) = self.borrowed_pointer(r, span, out) { @@ -309,9 +309,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { format!("a raw pointer to `{ty}` behind a reference, which JS can't write a whole value into in place"), ); } - let mut made = vec![value]; - made.extend(self.copy_depth(ty)); - Expr::rs("placeIn", made) + Expr::rs("placeIn", vec![value]) } /// The raw pointer to the place the body shows the reference `r` borrowed, if it does: diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index 63ae9914c..3c505b98a 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -292,7 +292,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// Whether std's `def_id` with `args` makes a raw pointer to what its first argument, a /// reference, points at: `UnsafeCell::get`, `MaybeUninit::as_ptr` and `as_mut_ptr`, -/// `ptr::from_ref` and `from_mut`, and `NonNull::from` and `into`. +/// `ptr::from_ref` and `from_mut`, and `NonNull::from` and `into` of a reference to its +/// pointee, which only std's impl converts, as any other would overlap it. pub fn from_reference<'tcx>( tcx: ty::TyCtxt<'tcx>, def_id: rustc_hir::def_id::DefId, @@ -311,7 +312,8 @@ pub fn from_reference<'tcx>( "From" => (args.type_at(0), args.type_at(1)), _ => (args.type_at(1), args.type_at(0)), }; - matches!(to.kind(), ty::Adt(adt, _) if Std::of(tcx, adt.did()) == Some(Std::NonNull)) && from.is_ref() + matches!(to.kind(), ty::Adt(adt, to) if Std::of(tcx, adt.did()) == Some(Std::NonNull) + && from.ref_mutability().is_some() && from.builtin_deref(true) == Some(to.type_at(0))) } _ => false, } @@ -1138,8 +1140,6 @@ fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> { value.clone() } - // It may be an `rs.placeIn`, whose object the next write through it changes. - _ if b.t.write_in(ty).is_some() => Expr::rs("ptrRead", vec![p]), _ => p.member("v"), }, Pointee::Object => p, From 925f1bc8f303c24ee6de983e76484f8ab67bfb27 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:12:31 -0800 Subject: [PATCH 16/45] fix(ts): copy what a cell or lock holds inline when moving it out A move-out read of a RefCell, Mutex, Cell, or OnceCell copied the object one deep but shared the value it holds inline, so setting a cell inside the moved-out RefCell changed the original's. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- ts/fixtures/lang/pointers/expected/lib.ts | 12 +++++ .../lang/pointers/expected/pointers.ts | 48 ++++++++++++++++++- ts/fixtures/lang/pointers/src/pointers.rs | 27 ++++++++++- ts/src/lower/copy.rs | 36 ++++++++++++-- 5 files changed, 119 insertions(+), 6 deletions(-) diff --git a/ts/README.md b/ts/README.md index b5b1c65b4..f36fdffed 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: 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 | | `&T`, `&mut T`, `Box`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, and one with interior mutability, such as a `Cell`, always | +| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, with what a cell or lock holds inline copied too, and one with interior mutability, such as a `Cell`, always | | `MaybeUninit` | `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 | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 16f3e9201..70d8923c9 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -73,6 +73,18 @@ export function copy$array_u8(v: Uint8Array): Uint8Array { return v.slice(); } +export function copy$cell_OnceCell_Cell_u32(v: rs.OnceCell>): rs.OnceCell> { + const c = rs.objectCopy(v); + c.v = copy$Option_Cell_u32(c.v); + return c; +} + +export function copy$cell_RefCell_Cell_u32(v: rs.RefCell>): rs.RefCell> { + const c = rs.objectCopy(v); + c.value = copy$Cell_u32(c.value); + return c; +} + export function copy$mem_MaybeUninit_Vec_u8(v: rs.ByteVec): rs.ByteVec { if (v === undefined) { return v; diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 50cc2073d..d7dc8220e 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; @@ -3493,6 +3493,23 @@ export function cellMoved(r: rs.Cell | undefined): rs.Cell | und return old; } +/** + * The `RefCell` and the `OnceCell` of a cell that shared references point at, moved out + * through pointers from them, whose cells, held inline, are then set. + */ +export function inlineMoved(r: rs.RefCell>, o: rs.OnceCell>): [rs.RefCell>, rs.OnceCell>] { + const old: [rs.RefCell>, rs.OnceCell>] = [copy$cell_RefCell_Cell_u32(rs.placeIn(r).v), copy$cell_OnceCell_Cell_u32(rs.placeIn(o).v)]; + const cell: rs.RefCell> = rs.refCellBorrow(old[0]); + rs.cellSet(cell.value, rs.u32Add(cell.value.v, 9)); + rs.refRelease(cell); + const _19: rs.Cell | undefined = old[1].v; + if (_19 !== undefined) { + rs.cellSet(_19, rs.u32Add(_19.v, 9)); + return old; + } + return old; +} + /** * Values holding a library object, moved out through pointers made each way, at any * depth, and then written in place: what was moved out keeps what it held. @@ -3556,6 +3573,35 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { return a0.v; }) & 0xff); } + const r: rs.RefCell> = new rs.RefCell(new rs.Cell(b as rs.u32)); + const o2: rs.OnceCell> = new rs.OnceCell(undefined); + if (rs.u8Rem(b, 2) === 0) { + const _116: rs.Cell = new rs.Cell(rs.u32Add(b as rs.u32, 1)); + rs.onceSet(o2, _116); + } + const _122: [rs.RefCell>, rs.OnceCell>] = inlineMoved(r, o2); + const oldR: rs.RefCell> = _122[0]; + const oldO2: rs.OnceCell> = _122[1]; + const iter6: rs.Iter>> = rs.iterSlice([r, oldR]); + while (true) { + const _130: rs.RefCell> | rs.Done = iter6.next(); + if (_130 === rs.DONE) { + break; + } + const _139: rs.RefCell> = rs.refCellBorrow(_130); + rs.byteVecPush(out, _139.value.v & 0xff); + rs.refRelease(_139); + } + const iter7: rs.Iter>> = rs.iterSlice([o2, oldO2]); + while (true) { + const _145: rs.OnceCell> | rs.Done = iter7.next(); + if (_145 === rs.DONE) { + break; + } + rs.byteVecPush(out, option_mapOr$18adzvj(_145.v, 0, (a02: rs.Cell): rs.u32 => { + return a02.v; + }) & 0xff); + } } } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index f3dbee1c6..e0b95024e 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -11,7 +11,7 @@ //! raw pointers of an `Arc` and an `Rc`, counted and taken back; and unions over a type //! parameter, each instance laid out at its own offsets. -use std::cell::{Cell, UnsafeCell}; +use std::cell::{Cell, OnceCell, RefCell, UnsafeCell}; use std::mem::{self, MaybeUninit}; use std::ptr::{self, NonNull}; use std::rc::Rc; @@ -2724,6 +2724,19 @@ fn cell_moved(r: &Option>) -> Option> { old } +/// The `RefCell` and the `OnceCell` of a cell that shared references point at, moved out +/// through pointers from them, whose cells, held inline, are then set. +fn inline_moved(r: &RefCell>, o: &OnceCell>) -> (RefCell>, OnceCell>) { + let old = unsafe { (ptr::read(r), ptr::read(o)) }; + let cell = old.0.borrow(); + cell.set(cell.get() + 9); + drop(cell); + if let Some(cell) = old.1.get() { + cell.set(cell.get() + 9); + } + old +} + /// Values holding a library object, moved out through pointers made each way, at any /// depth, and then written in place: what was moved out keeps what it held. pub fn moved_out(data: &[u8], out: &mut Vec) { @@ -2766,6 +2779,18 @@ pub fn moved_out(data: &[u8], out: &mut Vec) { for c in [&cell, &old] { out.push(c.as_ref().map_or(0, Cell::get) as u8); } + let r = RefCell::new(Cell::new(u32::from(b))); + let o = OnceCell::new(); + if b % 2 == 0 { + let _ = o.set(Cell::new(u32::from(b) + 1)); + } + let (old_r, old_o) = inline_moved(&r, &o); + for r in [&r, &old_r] { + out.push(r.borrow().get() as u8); + } + for o in [&o, &old_o] { + out.push(o.get().map_or(0, Cell::get) as u8); + } } } diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 48ad20b8c..5fcc66d66 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -65,7 +65,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(_) if self.write_in(ty).is_some() => { return self.writes.contains(&self.writes.into, false, self.tcx, ty) || self.addressed(ty) - || !ty.is_freeze(self.tcx, self.typing_env()); + || !ty.is_freeze(self.tcx, self.typing_env()) + || self.inline_held(ty).is_some_and(|(_, held)| self.changes(held)); } Some(_) => return false, None if self.own(adt.did()) && adt.all_fields().next().is_some() => { @@ -79,6 +80,21 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { || parts.into_iter().any(|t| self.changes(t)) } + /// The field of a library object `ty` that holds its value inline rather than on the + /// heap, which a copy of the object one object deep shares, and the type held there: a + /// cell's or a lock's value, or a one-time cell's, which holds nothing until set. + fn inline_held(&self, ty: Ty<'tcx>) -> Option<(&'static str, Ty<'tcx>)> { + let ty::Adt(adt, args) = ty.kind() else { + return None; + }; + match Std::of(self.tcx, adt.did())? { + Std::Cell => Some(("v", args.type_at(0))), + Std::RefCell | Std::Mutex => Some(("value", args.type_at(0))), + Std::OnceCell | Std::OnceLock => Some(("v", Ty::new_option(self.tcx, args.type_at(0)))), + _ => None, + } + } + /// Whether the runs before compared pointers to a `ty`, or to a type it holds by value, /// so each copy of one is at an address of its own, as in Rust, wherever it is read. pub fn held_addressed(&self, ty: Ty<'tcx>) -> bool { @@ -328,8 +344,22 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { _ => self.copy_result(value, residual, span), }) } - // A library object, which [`Self::changes`] says a whole write lands in. - Some(_) => ret(Expr::rs("objectCopy", vec![v()])), + // A library object, which [`Self::changes`] says a whole write lands in, and a + // copy of what it holds inline. + Some(_) => { + let copy = Expr::rs("objectCopy", vec![v()]); + let held = self.inline_held(ty).filter(|&(_, held)| self.changes(held)); + let Some((field, held)) = held else { + return ret(copy); + }; + let c = || Expr::ident("c"); + let read = self.snapshot(c().member(field), held, span); + vec![ + StmtKind::decl(false, "c", None, Some(copy)).at(None), + StmtKind::Expr(c().member(field).assign(read)).at(None), + StmtKind::Return(Some(c())).at(None), + ] + } _ => { let info = self.adt(*adt, span); match info.repr { From d4531da157275f6e4055664ec3d42335cece9dce Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:36:28 -0800 Subject: [PATCH 17/45] fix(ts): copy a range's ends and a borrowed cell's value when moving them out A Range of cells shared its ends with the original, and a Cell that Cell::from_mut made of a place copied its accessors. Each library object now lists what it holds inline; a cell is rebuilt from its value, and one whose inline state JS can't reach is refused where a copy would need it. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- ts/fixtures/lang/pointers/expected/lib.ts | 9 +- .../lang/pointers/expected/pointers.ts | 29 ++++++- ts/fixtures/lang/pointers/src/pointers.rs | 18 ++++ ts/src/lower/copy.rs | 85 ++++++++++++++----- 5 files changed, 118 insertions(+), 25 deletions(-) diff --git a/ts/README.md b/ts/README.md index f36fdffed..4a6b1a73d 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: 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 | | `&T`, `&mut T`, `Box`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, with what a cell or lock holds inline copied too, and one with interior mutability, such as a `Cell`, always | +| `*const T`, `*mut T`, `NonNull` | 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` 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, with what a cell, lock, or range holds inline copied too, refused where JS keeps it out of reach, and one with interior mutability, such as a `Cell`, always | | `MaybeUninit` | `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 | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 70d8923c9..7d92b735d 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -22,7 +22,7 @@ import type * as TaggedVec from "./pointers.TaggedVec.ts"; import type * as Twin from "./pointers.Twin.ts"; export function copy$Cell_u32(v: rs.Cell): rs.Cell { - return rs.objectCopy(v); + return new rs.Cell(v.v); } export function copy$Option_Cell_u32(v: rs.Cell | undefined): rs.Cell | undefined { @@ -113,6 +113,13 @@ export function copy$mem_MaybeUninit_tuple2_u8_u8(v: [rs.u8, rs.u8]): [rs.u8, rs return [v[0], v[1]]; } +export function copy$ops_Range_Cell_u32(v: rs.Range>): rs.Range> { + const c = rs.objectCopy(v); + c.start = copy$Cell_u32(c.start); + c.end = copy$Cell_u32(c.end); + return c; +} + export function copy$pointers_Aimed_Cell_u8(v: Aimed.Aimed>): Aimed.Aimed> { return { at: v.at as rs.Place>, seen: v.seen as rs.Place>, held: v.held as rs.Place>, maybe: v.maybe as rs.Place> | undefined, tagged: copy$tuple2_mutPtr_Cell_u8_u8(v.tagged as [rs.Place>, rs.u8]), to: v.to, read: (v.read as (a0: rs.Place>) => rs.u8) as (a0: rs.Ptr>) => rs.u8 }; } diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index d7dc8220e..d727b49b9 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; @@ -3510,6 +3510,18 @@ export function inlineMoved(r: rs.RefCell>, o: rs.OnceCell>, c: rs.Cell): [rs.Range>, rs.Cell] { + const old: [rs.Range>, rs.Cell] = [copy$ops_Range_Cell_u32(rs.placeIn(r).v), copy$Cell_u32(rs.placeIn(c).v)]; + rs.cellSet(old[0].start, rs.u32Add(old[0].start.v, 9)); + rs.cellSet(old[0].end, rs.u32Add(old[0].end.v, 9)); + rs.cellSet(old[1], rs.u32Add(old[1].v, 9)); + return old; +} + /** * Values holding a library object, moved out through pointers made each way, at any * depth, and then written in place: what was moved out keeps what it held. @@ -3602,6 +3614,21 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { return a02.v; }) & 0xff); } + const r2: rs.Range> = new rs.Range(new rs.Cell(b as rs.u32), new rs.Cell(rs.u32Add(b as rs.u32, 1))); + let x: rs.u32 = rs.u32Add(b as rs.u32, 2); + const _167: rs.Cell = rs.cellOfPlace(rs.place(() => x, (value2) => x = value2)); + const _165: [rs.Range>, rs.Cell] = endsMoved(r2, _167); + const oldR2: rs.Range> = _165[0]; + const oldC: rs.Cell = _165[1]; + const iter8: rs.Iter>> = rs.iterSlice([r2, oldR2]); + while (true) { + const _174: rs.Range> | rs.Done = iter8.next(); + if (_174 === rs.DONE) { + break; + } + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_174.start.v & 0xff, _174.end.v & 0xff)), undefined, true); + } + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(oldC.v & 0xff, x & 0xff)), undefined, true); } } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index e0b95024e..583632ad8 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -13,6 +13,7 @@ use std::cell::{Cell, OnceCell, RefCell, UnsafeCell}; use std::mem::{self, MaybeUninit}; +use std::ops; use std::ptr::{self, NonNull}; use std::rc::Rc; use std::sync::Arc; @@ -2737,6 +2738,16 @@ fn inline_moved(r: &RefCell>, o: &OnceCell>) -> (RefCell>, c: &Cell) -> (ops::Range>, Cell) { + let old = unsafe { (ptr::read(r), ptr::read(c)) }; + old.0.start.set(old.0.start.get() + 9); + old.0.end.set(old.0.end.get() + 9); + old.1.set(old.1.get() + 9); + old +} + /// Values holding a library object, moved out through pointers made each way, at any /// depth, and then written in place: what was moved out keeps what it held. pub fn moved_out(data: &[u8], out: &mut Vec) { @@ -2791,6 +2802,13 @@ pub fn moved_out(data: &[u8], out: &mut Vec) { for o in [&o, &old_o] { out.push(o.get().map_or(0, Cell::get) as u8); } + let r = Cell::new(u32::from(b))..Cell::new(u32::from(b) + 1); + let mut x = u32::from(b) + 2; + let (old_r, old_c) = ends_moved(&r, Cell::from_mut(&mut x)); + for r in [&r, &old_r] { + out.extend([r.start.get() as u8, r.end.get() as u8]); + } + out.extend([old_c.get() as u8, x as u8]); } } diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 5fcc66d66..7c5eae7ea 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -66,7 +66,14 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { return self.writes.contains(&self.writes.into, false, self.tcx, ty) || self.addressed(ty) || !ty.is_freeze(self.tcx, self.typing_env()) - || self.inline_held(ty).is_some_and(|(_, held)| self.changes(held)); + || { + let (fields, unknown) = self.inline_held(ty); + fields + .into_iter() + .map(|(_, t)| t) + .chain(unknown) + .any(|t| self.changes(t)) + }; } Some(_) => return false, None if self.own(adt.did()) && adt.all_fields().next().is_some() => { @@ -80,19 +87,40 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { || parts.into_iter().any(|t| self.changes(t)) } - /// The field of a library object `ty` that holds its value inline rather than on the - /// heap, which a copy of the object one object deep shares, and the type held there: a - /// cell's or a lock's value, or a one-time cell's, which holds nothing until set. - fn inline_held(&self, ty: Ty<'tcx>) -> Option<(&'static str, Ty<'tcx>)> { + /// What a library object `ty` holds inline rather than on the heap, which a copy of the + /// object one object deep shares: each field of its JS object holding such a value and + /// the type held there, a cell's, a lock's, a range's ends, or a one-time cell's, which + /// holds nothing until set; and the types it holds where no copy reaches, such as a + /// hash map's hasher or what a `LazyCell` keeps private. A collection's items are on + /// the heap, which Rust's read shares too. + fn inline_held(&self, ty: Ty<'tcx>) -> (Vec<(&'static str, Ty<'tcx>)>, Vec>) { let ty::Adt(adt, args) = ty.kind() else { - return None; + return (Vec::new(), Vec::new()); }; - match Std::of(self.tcx, adt.did())? { - Std::Cell => Some(("v", args.type_at(0))), - Std::RefCell | Std::Mutex => Some(("value", args.type_at(0))), - Std::OnceCell | Std::OnceLock => Some(("v", Ty::new_option(self.tcx, args.type_at(0)))), - _ => None, - } + let at = |i: usize| args.type_at(i); + let fields = match Std::of(self.tcx, adt.did()) { + Some(Std::Cell) => vec![("v", at(0))], + Some(Std::RefCell | Std::Mutex) => vec![("value", at(0))], + Some(Std::OnceCell | Std::OnceLock) => vec![("v", Ty::new_option(self.tcx, at(0)))], + Some(Std::Range) => vec![("start", at(0)), ("end", at(0))], + Some(Std::RangeInclusive) => vec![("lo", at(0)), ("hi", at(0))], + Some(Std::RangeFrom) => vec![("start", at(0))], + Some( + Std::Vec + | Std::String + | Std::VecDeque + | Std::LinkedList + | Std::BinaryHeap + | Std::BTreeMap + | Std::BTreeSet + | Std::HashTable + | Std::BytesMut, + ) => Vec::new(), + Some(Std::HashMap) => return (Vec::new(), vec![at(2)]), + Some(Std::HashSet) => return (Vec::new(), vec![at(1)]), + _ => return (Vec::new(), args.types().collect()), + }; + (fields, Vec::new()) } /// Whether the runs before compared pointers to a `ty`, or to a type it holds by value, @@ -346,19 +374,32 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } // A library object, which [`Self::changes`] says a whole write lands in, and a // copy of what it holds inline. - Some(_) => { + Some(std) => { + let (fields, unknown) = self.inline_held(ty); + if unknown.into_iter().any(|t| self.changes(t)) { + self.err( + span, + format!("a read moving out a `{ty}`, which holds inline a value JS can't copy apart"), + ); + } + // A cell of its own, as `Cell::from_mut`'s place reads and writes another's. + if std == Std::Cell { + let held = self.snapshot(v().member("v"), args.type_at(0), span); + return ret(Expr::New(Box::new(Expr::Rs("Cell".into())), vec![held])); + } + let fields: Vec<_> = fields.into_iter().filter(|&(_, t)| self.changes(t)).collect(); let copy = Expr::rs("objectCopy", vec![v()]); - let held = self.inline_held(ty).filter(|&(_, held)| self.changes(held)); - let Some((field, held)) = held else { + if fields.is_empty() { return ret(copy); - }; + } let c = || Expr::ident("c"); - let read = self.snapshot(c().member(field), held, span); - vec![ - StmtKind::decl(false, "c", None, Some(copy)).at(None), - StmtKind::Expr(c().member(field).assign(read)).at(None), - StmtKind::Return(Some(c())).at(None), - ] + let mut body = vec![StmtKind::decl(false, "c", None, Some(copy)).at(None)]; + for (field, t) in fields { + let read = self.snapshot(c().member(field), t, span); + body.push(StmtKind::Expr(c().member(field).assign(read)).at(None)); + } + body.push(StmtKind::Return(Some(c())).at(None)); + body } _ => { let info = self.adt(*adt, span); From 501e113232da0ad70f55d5adbcda7836f050cc6a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 08:31:11 -0800 Subject: [PATCH 18/45] fix(ts): refuse moving out a library object whose inline state only its runtime class knows A read that moves out a library object outside the known list (cells, locks, one-time cells, ranges, collections) now refuses where it would copy one, instead of copying it one object deep and sharing what its runtime class keeps inline, such as a LazyCell's initializer or a Peekable's peeked item. Co-Authored-By: Claude Opus 5.5 --- .../lang/conversions/expected/conversions.ts | 8 ++-- ts/fixtures/lang/slices/expected/lib.ts | 4 ++ ts/fixtures/lang/slices/expected/slices.ts | 4 +- ts/fixtures/reject/census.txt | 2 + ts/fixtures/reject/expected.txt | 2 + ts/fixtures/reject/src/alloc.rs | 19 ++++++++ ts/fixtures/reject/ts.toml | 2 + ts/src/lower/copy.rs | 44 +++++++++++-------- 8 files changed, 61 insertions(+), 24 deletions(-) diff --git a/ts/fixtures/lang/conversions/expected/conversions.ts b/ts/fixtures/lang/conversions/expected/conversions.ts index 09bcbafa8..38aca770d 100644 --- a/ts/fixtures/lang/conversions/expected/conversions.ts +++ b/ts/fixtures/lang/conversions/expected/conversions.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$array_Option_u16, copy$array_array0_u8, copy$array_array1_u8, copy$array_array2_u32, copy$array_array3_u8, copy$array_array40_u8, copy$array_u32, copy$array_u64, copy$array_u8, copy$conversions_Logged, copy$conversions_Named, drop$tuple2_conversions_Logged_u8 } from "./lib.ts"; +import { copy$String, copy$Vec_u32, copy$Vec_u8, copy$array_Option_u16, copy$array_array0_u8, copy$array_array1_u8, copy$array_array2_u32, copy$array_array3_u8, copy$array_array40_u8, copy$array_u32, copy$array_u64, copy$array_u8, copy$conversions_Logged, copy$conversions_Named, drop$tuple2_conversions_Logged_u8 } from "./lib.ts"; import * as Held from "./conversions.Held.ts"; import * as Kept from "./conversions.Kept.ts"; import * as Logged from "./conversions.Logged.ts"; @@ -843,7 +843,7 @@ export function lentParam$String(to: rs.String, data: rs.ByteSlice, out: rs.Byte rs.idx(s, 0); const borrowed7 = s.data; const borrowed8 = s.start; - put$conversions_Value_String(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => borrowed7[borrowed8], (value5) => borrowed7[borrowed8] = value5)), 1), rs.objectCopy), [[make$conversions_Value_String(data, 28)]])[0][0], out); + put$conversions_Value_String(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => borrowed7[borrowed8], (value5) => borrowed7[borrowed8] = value5)), 1), copy$String), [[make$conversions_Value_String(data, 28)]])[0][0], out); rs.idx(s, 0); s.data[s.start] = rs.stringCloneFrom(s.data[s.start], other); rs.idx(s, 0); @@ -905,7 +905,7 @@ export function lentParam$Vec_u32(to: rs.Vec, data: rs.ByteSlice, out: r rs.idx(s, 0); const borrowed7 = s.data; const borrowed8 = s.start; - put$conversions_Value_Vec_u32(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => borrowed7[borrowed8], (value5) => borrowed7[borrowed8] = value5)), 1), rs.objectCopy), [[make$conversions_Value_Vec_u32(data, 28)]])[0][0], out); + put$conversions_Value_Vec_u32(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => borrowed7[borrowed8], (value5) => borrowed7[borrowed8] = value5)), 1), copy$Vec_u32), [[make$conversions_Value_Vec_u32(data, 28)]])[0][0], out); rs.idx(s, 0); rs.vecCloneFrom(s.data[s.start], other, 4); rs.idx(s, 0); @@ -967,7 +967,7 @@ export function lentParam$Vec_u8(to: rs.ByteVec, data: rs.ByteSlice, out: rs.Byt rs.idx(s, 0); const borrowed7 = s.data; const borrowed8 = s.start; - put$conversions_Value_Vec_u8(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => borrowed7[borrowed8], (value5) => borrowed7[borrowed8] = value5)), 1), rs.objectCopy), [[make$conversions_Value_Vec_u8(data, 28)]])[0][0], out); + put$conversions_Value_Vec_u8(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => borrowed7[borrowed8], (value5) => borrowed7[borrowed8] = value5)), 1), copy$Vec_u8), [[make$conversions_Value_Vec_u8(data, 28)]])[0][0], out); rs.idx(s, 0); rs.byteVecCloneFrom(s.data[s.start], other); rs.idx(s, 0); diff --git a/ts/fixtures/lang/slices/expected/lib.ts b/ts/fixtures/lang/slices/expected/lib.ts index abd0a5bb5..858b97cef 100644 --- a/ts/fixtures/lang/slices/expected/lib.ts +++ b/ts/fixtures/lang/slices/expected/lib.ts @@ -13,6 +13,10 @@ export function copy$Option_array2_u8(v: Uint8Array | undefined): Uint8Array | u return v === undefined ? v : copy$array_u8(v); } +export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { + return rs.objectCopy(v); +} + export function copy$array_array2_array2_u8(v: Uint8Array[][]): Uint8Array[][] { return v.map(copy$array_array2_u8); } diff --git a/ts/fixtures/lang/slices/expected/slices.ts b/ts/fixtures/lang/slices/expected/slices.ts index 4b89d5fc8..625e354c4 100644 --- a/ts/fixtures/lang/slices/expected/slices.ts +++ b/ts/fixtures/lang/slices/expected/slices.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Option_array2_u16, copy$Option_array2_u8, copy$array_array2_array2_u8, copy$array_array2_u16, copy$array_array2_u8, copy$array_u16, copy$array_u8, copy$mem_MaybeUninit_array_u8, copy$slices_Pair, drop$tuple2_Vec_slices_Noisy_Vec_u8 } from "./lib.ts"; +import { copy$Option_array2_u16, copy$Option_array2_u8, copy$Vec_u8, copy$array_array2_array2_u8, copy$array_array2_u16, copy$array_array2_u8, copy$array_u16, copy$array_u8, copy$mem_MaybeUninit_array_u8, copy$slices_Pair, drop$tuple2_Vec_slices_Noisy_Vec_u8 } from "./lib.ts"; import { isSomeAnd$1ouf3e6, unwrapOr$1la41a6 as option_unwrapOr$1la41a6, unwrapOr$1fr498u as option_unwrapOr$1fr498u, unwrapOrDefault$1n9qa5p as option_unwrapOrDefault$1n9qa5p, unwrapOrDefault$0h81s8g as option_unwrapOrDefault$0h81s8g, map$13tab2s as option_map$13tab2s, map$0duedp1 as option_map$0duedp1, map$0q6fk7x as option_map$0q6fk7x, map$1uud0v7 as option_map$1uud0v7, map$1j0y4j0 as option_map$1j0y4j0, map$0vk4zok as option_map$0vk4zok, map$1aqm6sp as option_map$1aqm6sp, map$04ry0e1 as option_map$04ry0e1, mapOr$135q041 as option_mapOr$135q041, or$0as8tia as option_or$0as8tia, zip$0wdt346, clone$1omlcob as option_clone$1omlcob } from "./core/option.ts"; import * as Counted from "./slices.Counted.ts"; import * as Item from "./slices.Item.ts"; @@ -2970,7 +2970,7 @@ export function arrayForms(kind: rs.u8, data: rs.ByteSlice, out: rs.ByteVec): vo rs.idx(runs2, 0); const _354: rs.ByteVec[] = [rs.byteVecFrom(Uint8Array.of(byte(1), byte(2)))]; rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(rs.replaceIn(rs.sliceView(rs.arraySliceAt(runs2, 0)), _354, 1)[0])), undefined, true); - rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => item, (value2) => item = value2)), 1), rs.objectCopy), [[rs.byteVecFrom(Uint8Array.of(byte(3)))]])[0][0])), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(rs.cellReplace(rs.cellOfArraySlice(rs.arraySliceOne(rs.sliceOfPlace(rs.place(() => item, (value2) => item = value2)), 1), copy$Vec_u8), [[rs.byteVecFrom(Uint8Array.of(byte(3)))]])[0][0])), undefined, true); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(item)), undefined, true); } } diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 05998c633..864b3f227 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -11,7 +11,9 @@ fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place +fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:13: unsupported call to `std::alloc::alloc_zeroed` diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 9a6ec47ff..79fabfaa5 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -15,7 +15,9 @@ fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place +fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:13: unsupported call to `std::alloc::alloc_zeroed` diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 127ba0ea8..f4459cafa 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -31,6 +31,25 @@ pub fn infallible_cell(r: &Result, std::convert::Infallible unsafe { std::ptr::from_ref(r).cast_mut().write(Ok(std::cell::Cell::new(19))) } } +/// A `LazyCell` moved out through a pointer before its initializer, which holds a cell, +/// runs: the closure is state only `rs.LazyCell` knows, which a copy can't reach. +pub fn lazy_moved(cell: std::cell::Cell) -> u32 { + let lazy = std::cell::LazyCell::new(move || cell.get()); + let copy = unsafe { std::ptr::read(&lazy) }; + std::mem::forget(lazy); + *copy +} + +/// A `Peekable` moved out through a pointer with a cell peeked, which only its runtime +/// class holds. +pub fn peeked_moved(cells: Vec>) -> u32 { + let mut iter = cells.into_iter().peekable(); + iter.peek(); + let mut copy = unsafe { std::ptr::read(&iter) }; + std::mem::forget(iter); + copy.next().map_or(0, |cell| cell.get()) +} + /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 221faa76f..96c6f563c 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -5,7 +5,9 @@ roots = [ "alloc::allocated", "alloc::bytes_as_values", "alloc::infallible_cell", + "alloc::lazy_moved", "alloc::past_reference", + "alloc::peeked_moved", "alloc::reinterpreted", "alloc::zeroed", "bound_ranges", diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 16d4f6972..622bb1ba2 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -66,13 +66,12 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { return self.writes.contains(&self.writes.into, false, self.tcx, ty) || self.addressed(ty) || !ty.is_freeze(self.tcx, self.typing_env()) - || { - let (fields, unknown) = self.inline_held(ty); - fields - .into_iter() - .map(|(_, t)| t) - .chain(unknown) - .any(|t| self.changes(t)) + || match self.inline_held(ty) { + Some((fields, unreachable)) => { + fields.into_iter().map(|(_, t)| t).chain(unreachable).any(|t| self.changes(t)) + } + // Any type it is over may be held inline, which its copy refuses. + None => args.types().any(|t| self.changes(t)), }; } Some(_) => return false, @@ -90,12 +89,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// What a library object `ty` holds inline rather than on the heap, which a copy of the /// object one object deep shares: each field of its JS object holding such a value and /// the type held there, a cell's, a lock's, a range's ends, or a one-time cell's, which - /// holds nothing until set; and the types it holds where no copy reaches, such as a - /// hash map's hasher or what a `LazyCell` keeps private. A collection's items are on - /// the heap, which Rust's read shares too. - fn inline_held(&self, ty: Ty<'tcx>) -> (Vec<(&'static str, Ty<'tcx>)>, Vec>) { + /// holds nothing until set; and the types it holds where no copy reaches, a hash map's + /// hasher. A collection's items are on the heap, which Rust's read shares too. `None` + /// for any other library object, such as a `LazyCell` or a `Peekable`, whose inline + /// state only its runtime class knows, so a read that would copy one is refused. + fn inline_held(&self, ty: Ty<'tcx>) -> Option<(Vec<(&'static str, Ty<'tcx>)>, Vec>)> { let ty::Adt(adt, args) = ty.kind() else { - return (Vec::new(), Vec::new()); + return None; }; let at = |i: usize| args.type_at(i); let fields = match Std::of(self.tcx, adt.did()) { @@ -116,11 +116,11 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { | Std::HashTable | Std::BytesMut, ) => Vec::new(), - Some(Std::HashMap) => return (Vec::new(), vec![at(2)]), - Some(Std::HashSet) => return (Vec::new(), vec![at(1)]), - _ => return (Vec::new(), args.types().collect()), + Some(Std::HashMap) => return Some((Vec::new(), vec![at(2)])), + Some(Std::HashSet) => return Some((Vec::new(), vec![at(1)])), + _ => return None, }; - (fields, Vec::new()) + Some((fields, Vec::new())) } /// Whether the runs before compared pointers to a `ty`, or to a type it holds by value, @@ -394,8 +394,16 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { // A library object, which [`Self::changes`] says a whole write lands in, and a // copy of what it holds inline. Some(std) => { - let (fields, unknown) = self.inline_held(ty); - if unknown.into_iter().any(|t| self.changes(t)) { + let Some((fields, unreachable)) = self.inline_held(ty) else { + self.err( + span, + format!( + "a read moving out a `{ty}`, a library object whose inline state only its runtime class knows, which JS can't copy apart" + ), + ); + return ret(v()); + }; + if unreachable.into_iter().any(|t| self.changes(t)) { self.err( span, format!("a read moving out a `{ty}`, which holds inline a value JS can't copy apart"), From 20c188dfde6b4e7070b9e68a5e864d34f58c30ef Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 08:40:51 -0800 Subject: [PATCH 19/45] fix(ts): copy a Weak moved out through a pointer as the handle it is A Weak holds only its pointer to what it shares, so a copy one object deep is Rust's bitwise read. smallvec's Drain and retain over Weak in moq need it. Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/core/option.ts | 14 ++ ts/fixtures/lang/pointers/expected/lib.ts | 4 + .../pointers/expected/pointers.Drained.ts | 16 +- .../lang/pointers/expected/pointers.ts | 159 ++++++++++++++---- ts/fixtures/lang/pointers/src/pointers.rs | 26 +++ ts/src/lower/copy.rs | 6 +- 6 files changed, 187 insertions(+), 38 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 67a3f757d..92ed51a7c 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -76,6 +76,13 @@ export function map$1cfrse5(self: Loaded.Loaded | undefined, f: (a0: Loaded.Load return f(self); } +export function map$1ng7x7l(self: rs.Weak | undefined, f: (a0: rs.Weak) => rs.Weak): rs.Weak | undefined { + if (self === undefined) { + return undefined; + } + return f(self); +} + export function map$070fw7m(self: rs.u32 | undefined, f: (a0: rs.u32) => rs.u32): rs.u32 | undefined { if (self === undefined) { return undefined; @@ -111,6 +118,13 @@ export function mapOr$18adzvj(self: rs.Cell | undefined, default_: rs.u3 return self.v; } +export function mapOr$0emjeap(self: rs.Arc | undefined, default_: rs.u8, f: (a0: rs.Arc) => rs.u8): rs.u8 { + if (self === undefined) { + return default_; + } + return f(self); +} + export function mapOr$173j2gr(self: [rs.u8, rs.Arc] | undefined, default_: rs.u8, f: (a0: [rs.u8, rs.Arc]) => rs.u8): rs.u8 { if (self === undefined) { return default_; diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 7d92b735d..ca433f73b 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -200,6 +200,10 @@ export function copy$pointers_Twin(v: Twin.Twin): Twin.Twin { return { _0: v._0, _1: v._1 }; } +export function copy$sync_Weak_u32(v: rs.Weak): rs.Weak { + return rs.objectCopy(v); +} + export function copy$tuple2_Vec_u8_u8(v: [rs.ByteVec, rs.u8]): [rs.ByteVec, rs.u8] { return [copy$Vec_u8(v[0]), v[1]]; } diff --git a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts index 344d8eaf7..5e2a38872 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts @@ -1,8 +1,8 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$String, copy$Vec_u8 } from "./lib.ts"; -import { map$00buaky as option_map$00buaky, map$1lzqy5c as option_map$1lzqy5c, map$1h9tk16 as option_map$1h9tk16, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; +import { copy$String, copy$Vec_u8, copy$sync_Weak_u32 } from "./lib.ts"; +import { map$00buaky as option_map$00buaky, map$1lzqy5c as option_map$1lzqy5c, map$1h9tk16 as option_map$1h9tk16, map$1ng7x7l as option_map$1ng7x7l, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; /** * What smallvec's `Drain` is: the elements of a slice nothing will drop again, each read @@ -30,6 +30,12 @@ export function next$iter_Iterator_Vec_u8(self: Drained): rs.ByteVec })); } +export function next$iter_Iterator_sync_Weak_u32(self: Drained>): rs.Weak | rs.Done { + return rs.iterFromOption(option_map$1ng7x7l(rs.iterToOption((self.iter as rs.Iter>).next()), (r: rs.Weak): rs.Weak => { + return copy$sync_Weak_u32(r); + })); +} + export function next$iter_Iterator_u32(self: Drained): rs.u32 | rs.Done { return rs.iterFromOption(option_map$070fw7m(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.u32): rs.u32 => { return r; @@ -54,6 +60,12 @@ export function nextBack$iter_DoubleEndedIterator_Vec_u8(self: Drained>): rs.Weak | undefined { + return option_map$1ng7x7l(rs.iterToOption((self.iter as rs.Iter>).nextBack()), (r: rs.Weak): rs.Weak => { + return copy$sync_Weak_u32(r); + }); +} + export function nextBack$iter_DoubleEndedIterator_u32(self: Drained): rs.u32 | undefined { return option_map$070fw7m(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.u32): rs.u32 => { return r; diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index e196f4cac..da39e1834 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,9 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; -import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; +import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -3058,6 +3058,33 @@ export function drained$Vec_u8(v: rs.Vec, bits: rs.u8): rs.Vec>, bits: rs.u8): rs.Vec> { + let next!: rs.Weak | undefined; + const v2: rs.Vec> = v; + const iter: Drained.Drained> = { iter: rs.iterSlice(v2.items) as rs.Iter>> }; + const out: rs.Vec> = new rs.Vec(); + let i: rs.i32 = 0; + while (true) { + if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { + next = rs.iterToOption(Drained.next$iter_Iterator_sync_Weak_u32(iter)); + } else { + next = Drained.nextBack$iter_DoubleEndedIterator_sync_Weak_u32(iter); + } + if (next !== undefined) { + out.items.push(next); + i = rs.i32Add(i, 1); + } else { + const v3: rs.Vec> = v2; + rs.vecSetLen(v3, 0, 4, (): rs.Weak => { + return (undefined as unknown) as rs.Weak; + }); + rs.dropVec(v3); + return out; + } + } +} + /** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ export function drained$u32(v: rs.Vec, bits: rs.u8): rs.Vec { let next!: rs.u32 | undefined; @@ -3154,6 +3181,31 @@ export function retained$Vec_u8_pointers_behind_refs_closure8(v: rs.Vec>, keep: (a0: rs.Weak) => boolean): void { + let _10!: rs.usize; + const len: rs.usize = v.items.length; + const p: rs.Place> = rs.vecAsPtr(v, 4); + let write: rs.usize = 0; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = len; + while ((_10 = iterStart++) < iterEnd) { + const _15: rs.Place> = rs.ptrAdd(p, _10); + if (!keep(_15.v)) { + _15.v[Symbol.dispose](); + } else { + rs.ptrCopy(_15, rs.ptrAdd(p, write), 1, copy$sync_Weak_u32); + write = rs.usizeAdd(write, 1); + } + } + rs.vecSetLen(v, write, 4, (): rs.Weak => { + return (undefined as unknown) as rs.Weak; + }); +} + /** * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then * drops or moves to the front. @@ -3278,8 +3330,8 @@ export function cellBehind(r: rs.Cell | undefined, how: rs.u8): rs.u32 { * from references to an `Arc`'s value, measured against each other. */ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { - let _133!: rs.usize; - let _173!: rs.u8; + let _175!: rs.usize; + let _215!: rs.u8; const bits: rs.u8 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0); const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): rs.u32 => { return rs.u32Mul(_2 as rs.u32, 3); @@ -3321,12 +3373,12 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { return _26 & 0xff; }), undefined, true); const words2: rs.Vec = words; - retained$String_pointers_behind_refs_closure6(words2, (w3: rs.String): boolean => { - rs.stringPush(w3, 33); - return rs.usizeRem(w3.len, 2) === 0; + retained$String_pointers_behind_refs_closure6(words2, (w4: rs.String): boolean => { + rs.stringPush(w4, 33); + return rs.usizeRem(w4.len, 2) === 0; }); - rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w4: rs.String): rs.Iter => { - return rs.strBytes(w4.v); + rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w5: rs.String): rs.Iter => { + return rs.strBytes(w5.v); }, undefined, true), "next"); const runs2: rs.Vec = runs; retained$Vec_u8_pointers_behind_refs_closure8(runs2, (r2: rs.ByteVec): boolean => { @@ -3347,17 +3399,55 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { }), undefined, true); rs.dropVec(counted); rs.byteVecPush(out, dropped.v); - const nums3: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_28: rs.u8): rs.u32 => { - return _28 as rs.u32; + const arc: rs.Arc = new rs.Arc(bits as rs.u32); + const weaks: rs.Vec> = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_28: rs.u8): rs.Weak => { + if (rs.u8Rem(_28, 2) === 0) { + return new rs.Weak(arc); + } + return new rs.Weak(); + }), undefined, 4, true); + for (const w2 of drained$sync_Weak_u32(rs.vecFrom(weaks.items.map((a05: rs.Weak): rs.Weak => { + return rs.weakClone(a05); + })), bits).items) { + rs.byteVecPush(out, option_mapOr$0emjeap(rs.weakUpgrade(w2), 255, (n5: rs.Arc): rs.u8 => { + const _019: rs.u8 = n5.value & 0xff; + n5[Symbol.dispose](); + return _019; + })); + w2[Symbol.dispose](); + } + const weaks2: rs.Vec> = weaks; + let i: rs.u8 = 0; + retained$sync_Weak_u32_pointers_behind_refs_closure14(weaks2, (w6: rs.Weak): boolean => { + i = rs.u8Add(i, 1); + if (rs.u8Rem(i, 3) === 0) { + const _86: rs.Weak = new rs.Weak(arc); + w6[Symbol.dispose](); + Object.assign(w6, _86); + } + return rs.u8Rem(i, 2) === 0; + }); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(weaks2.items), (w7: rs.Weak): rs.u8 => { + return option_mapOr$0emjeap(rs.weakUpgrade(w7), 255, (n6: rs.Arc): rs.u8 => { + const _022: rs.u8 = n6.value & 0xff; + n6[Symbol.dispose](); + return _022; + }); + }), undefined, true); + rs.byteVecPush(out, arc.weak & 0xff); + rs.dropVec(weaks2); + rs.byteVecPush(out, arc.weak & 0xff); + const nums3: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_29: rs.u8): rs.u32 => { + return _29 as rs.u32; }), undefined, 4, true); if (rs.sliceFrom(nums3.items).length >= 2) { const first: rs.Place = rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 0)); rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, rs.usizeSub(nums3.items.length, 1))), first) & 0xff); - const _130: rs.usize = nums3.items.length; + const _172: rs.usize = nums3.items.length; let iterStart: rs.usize = 0; - const iterEnd: rs.usize = _130; - while ((_133 = iterStart++) < iterEnd) { - rs.byteVecPush(out, rs.ptrAdd(first, _133).v & 0xff); + const iterEnd: rs.usize = _172; + while ((_175 = iterStart++) < iterEnd) { + rs.byteVecPush(out, rs.ptrAdd(first, _175).v & 0xff); } rs.byteVecPush(out, rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v & 0xff); rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v = 9; @@ -3365,42 +3455,43 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { } const v: rs.ByteVec = rs.byteSliceToByteVec(data); rs.byteVecPush(out, vecLen(v) & 0xff); - const _164: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); - refilled(v, _164, bits); + const _206: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); + refilled(v, _206, bits); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); let iterStart2: rs.u8 = 0; const iterEnd2: rs.u8 = 3; - while ((_173 = iterStart2++) < iterEnd2) { - const how: rs.u8 = _173; - const w2: rs.ByteVec = rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b6: rs.u8): rs.u8 => { - return b6 ^ how; + while ((_215 = iterStart2++) < iterEnd2) { + const how: rs.u8 = _215; + const w3: rs.ByteVec = rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b7: rs.u8): rs.u8 => { + return b7 ^ how; }), undefined, true); - const _185: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); - rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w2, _185, how))), undefined, true); + const _227: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w3, _227, how))), undefined, true); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); - rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(w2)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(w3)), undefined, true); rs.byteVecPush(out, 255); } - const iter6: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); + const iter7: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); while (true) { - const _200: [rs.usize, rs.u8] | rs.Done = iter6.next(); - if (_200 === rs.DONE) { + const _242: [rs.usize, rs.u8] | rs.Done = iter7.next(); + if (_242 === rs.DONE) { break; } - const i2: rs.usize = _200[0]; - const b: rs.u8 = _200[1]; - rs.byteVecPush(out, behind(b as rs.u32, i2 & 0xff) & 0xff); + const i3: rs.usize = _242[0]; + const b: rs.u8 = _242[1]; + rs.byteVecPush(out, behind(b as rs.u32, i3 & 0xff) & 0xff); rs.byteVecPush(out, cellBehind(then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { return new rs.Cell(b as rs.u32); }), b) & 0xff); } - const arc: rs.Arc = new rs.Arc(bits as rs.u32); - const p: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); - const q: rs.Place = rs.arcRaw(arc, rs.arcPlace(arc), "Arc"); + const arc2: rs.Arc = new rs.Arc(bits as rs.u32); + const p: rs.Place = rs.arcRaw(arc2, rs.arcPlace(arc2), "Arc"); + const q: rs.Place = rs.arcRaw(arc2, rs.arcPlace(arc2), "Arc"); rs.byteVecPush(out, rs.ptrOffsetFrom(p, q) & 0xff); rs.byteVecPush(out, q.v & 0xff); - rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc, rs.arcPlace(arc), "Arc"), p) & 0xff); + rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc2, rs.arcPlace(arc2), "Arc"), p) & 0xff); rs.byteVecPush(out, from$convert_From_ref_pointers_Halves32_ptr_NonNull_u32({ first: bits as rs.u32, second: ((bits as rs.u32) ^ 85) >>> 0 }).v & 0xff); + arc2[Symbol.dispose](); arc[Symbol.dispose](); } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 583632ad8..2408bb83f 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2591,6 +2591,32 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { out.extend(counted.iter().map(|c| c.id)); drop(counted); out.push(dropped.get()); + // A `Weak` holds only its pointer to what it shares, which a read out copies as is, with + // one written whole in place. + let arc = Arc::new(u32::from(bits)); + let weaks: Vec> = data + .iter() + .map(|&b| match b % 2 { + 0 => Arc::downgrade(&arc), + _ => std::sync::Weak::new(), + }) + .collect(); + for w in drained(weaks.clone(), bits) { + out.push(w.upgrade().map_or(0xff, |n| *n as u8)); + } + let mut weaks = weaks; + let mut i = 0u8; + retained(&mut weaks, |w| { + i += 1; + if i % 3 == 0 { + *w = Arc::downgrade(&arc); + } + i % 2 == 0 + }); + out.extend(weaks.iter().map(|w| w.upgrade().map_or(0xff, |n| *n as u8))); + out.push(Arc::weak_count(&arc) as u8); + drop(weaks); + out.push(Arc::weak_count(&arc) as u8); // From a reference the body borrowed, where the element is. let mut nums: Vec = data.iter().map(|&b| u32::from(b)).collect(); diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 622bb1ba2..67e94b6db 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -90,7 +90,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// object one object deep shares: each field of its JS object holding such a value and /// the type held there, a cell's, a lock's, a range's ends, or a one-time cell's, which /// holds nothing until set; and the types it holds where no copy reaches, a hash map's - /// hasher. A collection's items are on the heap, which Rust's read shares too. `None` + /// hasher. A collection's items, and what a `Weak` points at, are on the heap, which + /// Rust's read shares too. `None` /// for any other library object, such as a `LazyCell` or a `Peekable`, whose inline /// state only its runtime class knows, so a read that would copy one is refused. fn inline_held(&self, ty: Ty<'tcx>) -> Option<(Vec<(&'static str, Ty<'tcx>)>, Vec>)> { @@ -114,7 +115,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { | Std::BTreeMap | Std::BTreeSet | Std::HashTable - | Std::BytesMut, + | Std::BytesMut + | Std::Weak, ) => Vec::new(), Some(Std::HashMap) => return Some((Vec::new(), vec![at(2)])), Some(Std::HashSet) => return Some((Vec::new(), vec![at(1)])), From f275248c268c485fad2908a35a9cc574f6592f79 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:11:18 -0800 Subject: [PATCH 20/45] chore(ts): recount the census, name the inline-state alias, format Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 6 +++--- quest/m3/ts-unsafe/README.md | 2 +- ts/src/lower/copy.rs | 14 ++++++++++---- ts/src/lower/mir/rvalue.rs | 4 +++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index 6af9b4f93..a1b3f1575 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -42,13 +42,13 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi - The end-to-end work stays here: the model's roots, its differential harness, and the `frames.ts` workload. moq writes the binding files `model.toml` names, in its own tree. -The census at the pin on 2026-11-07: 51 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-07: 45 refusals. `Timestamp` alone refuses nothing, and the [model fixture](/ts/README.md#tests)'s harness runs it against native moq-net at the pin; each case it adds as a type translates. Every other root reaches the rest of the model, all of it moq's, for its research quest: kio's `log.rs` reads bytes as its segments and retires them through crossbeam-epoch, whose tagged pointers and inline closures (36 -refusals) translation refuses for good, and smallvec's union over a type parameter and -raw parts (10) wait on [unsafe](/quest/m3/ts-unsafe/README.md), so moq rewrites or drops +refusals) translation refuses for good, and smallvec's union over a type parameter, its +`Array::Item`, and raw parts (5) wait on [unsafe](/quest/m3/ts-unsafe/README.md), so moq rewrites or drops them, as it does the `unsafe` slice of `model/frame.rs`'s `Filled`. Decided with the maintainer on 2026-11-04, from a census at the bumped pin diff --git a/quest/m3/ts-unsafe/README.md b/quest/m3/ts-unsafe/README.md index 2255dde9d..0162130a6 100644 --- a/quest/m3/ts-unsafe/README.md +++ b/quest/m3/ts-unsafe/README.md @@ -70,7 +70,7 @@ construct still open at `b3df54b6c`, all in m3: [#568](https://github.com/moq-dev/rust2/pull/568). `bytes`' `Buf::get_u16` and wider map to the runtime since [#447](https://github.com/moq-dev/rust2/pull/447). -Census at the moq pin on 2026-11-07: model 46, kio 72, moq-net 348 refusals. +Census at the moq pin on 2026-11-07: model 45, kio 72, moq-net 344 refusals. Public API: the translator's output. diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 67e94b6db..15b5f2296 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -29,6 +29,10 @@ use super::{AdtRepr, Translator, field_name, names}; use crate::program::{SymKind, Symbol}; use crate::ts::{self, Expr, Key, Param, Stmt, StmtKind}; +/// What a library object holds inline: each field of its JS object holding a value and +/// the type held there, and the types it holds where no copy reaches. +type Inline<'tcx> = (Vec<(&'static str, Ty<'tcx>)>, Vec>); + impl<'a, 'tcx> Translator<'a, 'tcx> { /// Whether a `ty` is copied where Rust copies it: a `Copy` value that is, or holds by /// value, a JS object the crate's code writes into in place or compares pointers to. @@ -67,9 +71,11 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { || self.addressed(ty) || !ty.is_freeze(self.tcx, self.typing_env()) || match self.inline_held(ty) { - Some((fields, unreachable)) => { - fields.into_iter().map(|(_, t)| t).chain(unreachable).any(|t| self.changes(t)) - } + Some((fields, unreachable)) => fields + .into_iter() + .map(|(_, t)| t) + .chain(unreachable) + .any(|t| self.changes(t)), // Any type it is over may be held inline, which its copy refuses. None => args.types().any(|t| self.changes(t)), }; @@ -94,7 +100,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// Rust's read shares too. `None` /// for any other library object, such as a `LazyCell` or a `Peekable`, whose inline /// state only its runtime class knows, so a read that would copy one is refused. - fn inline_held(&self, ty: Ty<'tcx>) -> Option<(Vec<(&'static str, Ty<'tcx>)>, Vec>)> { + fn inline_held(&self, ty: Ty<'tcx>) -> Option> { let ty::Adt(adt, args) = ty.kind() else { return None; }; diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index fc745d502..bd8ffcfe6 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -194,7 +194,9 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let value = self.place_expr(place, span, out); match pointee { _ if crate::lower::pointer::slice_of_bytes(pointee) => Expr::rs("rawByteSliceOf", vec![value]), - _ if crate::lower::pointer::slice_of_arrays(pointee) => Expr::rs("rawArraySliceOf", vec![value]), + _ if crate::lower::pointer::slice_of_arrays(pointee) => { + Expr::rs("rawArraySliceOf", vec![value]) + } _ => Expr::rs("rawSliceOf", vec![value]), } } From b117c7e5029a1704c4f0c65b4f7add68a3b266e4 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 10:05:38 -0800 Subject: [PATCH 21/45] fix(ts): copy a Vec's or VecDeque's storage on a move-out read, clear stale fields on a whole write, refuse unfixed offsets from a lone place A JS Vec keeps its length as its items array's, and a VecDeque clears the slot it pops, so a copy sharing that array saw the original's set_len or pop_front. ptr::write left a field the new value lacks, such as a HashMap's capacity. An offset the body doesn't fix moved a pointer from a reference out of its array of one at run time. Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/core/option.ts | 7 +++ ts/fixtures/lang/pointers/expected/lib.ts | 12 ++++ .../lang/pointers/expected/pointers.ts | 52 +++++++++++++++- ts/fixtures/lang/pointers/src/pointers.rs | 31 ++++++++++ ts/fixtures/reject/census.txt | 1 + ts/fixtures/reject/expected.txt | 1 + ts/fixtures/reject/src/alloc.rs | 6 ++ ts/fixtures/reject/ts.toml | 1 + ts/runtime/rs.ts | 8 ++- ts/src/lower/copy.rs | 60 +++++++++++++------ ts/src/lower/mir/origin.rs | 13 +++- 11 files changed, 168 insertions(+), 24 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 92ed51a7c..9ac98ea05 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -139,6 +139,13 @@ export function mapOr$0smr0aj(self: rs.u16 | undefined, default_: Uint8Array, _: return rs.u16ToBeBytes(self); } +export function mapOr$07pvkk0(self: rs.u32 | undefined, default_: rs.u8, f: (a0: rs.u32) => rs.u8): rs.u8 { + if (self === undefined) { + return default_; + } + return f(self); +} + export function filter$0wvwd70(self: rs.u32 | undefined, predicate: (a0: rs.u32) => boolean): rs.u32 | undefined { if (self !== undefined) { const x: rs.u32 = self; diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index ca433f73b..3e82145de 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -45,6 +45,12 @@ export function copy$String(v: rs.String): rs.String { return rs.objectCopy(v); } +export function copy$Vec_u32(v: rs.Vec): rs.Vec { + const c = rs.objectCopy(v); + c.items = c.items.slice(); + return c; +} + export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { return rs.objectCopy(v); } @@ -85,6 +91,12 @@ export function copy$cell_RefCell_Cell_u32(v: rs.RefCell>): rs.R return c; } +export function copy$collections_VecDeque_u32(v: rs.VecDeque): rs.VecDeque { + const c = rs.objectCopy(v); + c.b = c.b.slice(); + return c; +} + export function copy$mem_MaybeUninit_Vec_u8(v: rs.ByteVec): rs.ByteVec { if (v === undefined) { return v; diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index da39e1834..3c2680840 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,9 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u32, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$collections_VecDeque_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; -import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; +import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, mapOr$07pvkk0 as option_mapOr$07pvkk0, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -3601,6 +3601,28 @@ export function inlineMoved(r: rs.RefCell>, o: rs.OnceCell, q: rs.VecDeque): [rs.Vec, rs.VecDeque, rs.u32 | undefined] { + const v2: rs.Vec = v; + const oldV: rs.Vec = copy$Vec_u32(v2); + rs.vecSetLen(v2, 0, 4, (): rs.u32 => { + return (undefined as unknown) as rs.u32; + }); + return [oldV, copy$collections_VecDeque_u32(q), rs.dequePopFront(q)] as [rs.Vec, rs.VecDeque, rs.u32 | undefined]; +} + +/** + * A `HashMap` written whole through a pointer from the `&mut` given, which holds what the + * new one does, its capacity included. + */ +export function mapRewritten(r: rs.HashMap): void { + rs.placeIn(r).v = rs.hashNew() as rs.HashMap; +} + /** * A range of cells and a cell `Cell::from_mut` made of a variable's place, moved out * through pointers from shared references to them, whose cells are then set. @@ -3721,6 +3743,32 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { } rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(oldC.v & 0xff, x & 0xff)), undefined, true); } + const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_23: rs.u8): rs.u32 => { + return _23 as rs.u32; + }), undefined, 4, true); + const _199: [rs.Vec, rs.VecDeque, rs.u32 | undefined] = shrunkMoved(rs.vecFrom(nums.items.slice()), rs.dequeFromVec(rs.iterToVec(rs.iterSlice(nums.items), undefined, 4, true), 4) as rs.VecDeque); + const oldV: rs.Vec = _199[0]; + const oldQ: rs.VecDeque = _199[1]; + const popped: rs.u32 | undefined = _199[2]; + rs.byteVecPush(out, oldV.items.length & 0xff); + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(oldV.items), (_24: rs.u32): rs.u8 => { + return _24 & 0xff; + }), undefined, true); + rs.byteVecPush(out, option_mapOr$07pvkk0(popped, 255, (n3: rs.u32): rs.u8 => { + return n3 & 0xff; + })); + rs.byteVecPush(out, oldQ.n & 0xff); + rs.iterExtendBytes(out, rs.iterMap(rs.dequeIter(oldQ), (_27: rs.u32): rs.u8 => { + return _27 & 0xff; + }), undefined, true); + const map: rs.HashMap = rs.hashWithCapacity(rs.usizeMul(data.length, 8), 117440512) as rs.HashMap; + const _232: rs.Iter<[rs.u32, rs.u32]> = rs.iterMap(rs.iterSlice(nums.items), (_29: rs.u32): [rs.u32, rs.u32] => { + const n5: rs.u32 = _29; + return [n5, n5] as [rs.u32, rs.u32]; + }); + rs.hashExtend(map, _232, undefined, undefined, undefined, 117440512); + mapRewritten(map); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.hashLen(map) & 0xff, rs.hashCapacity(map) & 0xff)), undefined, true); } /** diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 2408bb83f..039308d4b 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2764,6 +2764,26 @@ fn inline_moved(r: &RefCell>, o: &OnceCell>) -> (RefCell, mut q: std::collections::VecDeque) -> (Vec, std::collections::VecDeque, Option) { + let mut v = mem::ManuallyDrop::new(v); + let old_v = unsafe { ptr::read(&*v) }; + unsafe { v.set_len(0) }; + let old_q = unsafe { ptr::read(&q) }; + let popped = q.pop_front(); + mem::forget(q); + (old_v, old_q, popped) +} + +/// A `HashMap` written whole through a pointer from the `&mut` given, which holds what the +/// new one does, its capacity included. +#[inline(never)] +fn map_rewritten(r: &mut std::collections::HashMap) { + unsafe { ptr::write(ptr::from_mut(r), std::collections::HashMap::new()) } +} + /// A range of cells and a cell `Cell::from_mut` made of a variable's place, moved out /// through pointers from shared references to them, whose cells are then set. fn ends_moved(r: &ops::Range>, c: &Cell) -> (ops::Range>, Cell) { @@ -2836,6 +2856,17 @@ pub fn moved_out(data: &[u8], out: &mut Vec) { } out.extend([old_c.get() as u8, x as u8]); } + let nums: Vec = data.iter().map(|&b| u32::from(b)).collect(); + let (old_v, old_q, popped) = shrunk_moved(nums.clone(), nums.iter().copied().collect()); + out.push(old_v.len() as u8); + out.extend(old_v.iter().map(|&n| n as u8)); + out.push(popped.map_or(0xff, |n| n as u8)); + out.push(old_q.len() as u8); + out.extend(old_q.iter().map(|&n| n as u8)); + let mut map = std::collections::HashMap::::with_capacity(data.len() * 8); + map.extend(nums.iter().map(|&n| (n, n))); + map_rewritten(&mut map); + out.extend([map.len() as u8, map.capacity() as u8]); } /// Pointers to slices cast to ones to arrays of their elements, of bytes and of numbers: diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 1df7b50f9..7c962d6f8 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -12,6 +12,7 @@ fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart +fixtures/reject/src/alloc.rs:offset_reference+1:34: a pointer moved by an offset the body doesn't fix from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 79fabfaa5..001e9e243 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -16,6 +16,7 @@ fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart +fixtures/reject/src/alloc.rs:offset_reference+1:34: a pointer moved by an offset the body doesn't fix from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index f4459cafa..98b108147 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -40,6 +40,12 @@ pub fn lazy_moved(cell: std::cell::Cell) -> u32 { *copy } +/// A pointer from a reference the body is given moved by an offset the body doesn't fix, +/// which may reach past the value JS keeps in no array. +pub fn offset_reference(r: &u32, n: usize) -> u32 { + unsafe { *std::ptr::from_ref(r).add(n) } +} + /// A `Peekable` moved out through a pointer with a cell peeked, which only its runtime /// class holds. pub fn peeked_moved(cells: Vec>) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 96c6f563c..80f0a63bc 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -6,6 +6,7 @@ roots = [ "alloc::bytes_as_values", "alloc::infallible_cell", "alloc::lazy_moved", + "alloc::offset_reference", "alloc::past_reference", "alloc::peeked_moved", "alloc::reinterpreted", diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index 60b74149a..439e28b6a 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -679,12 +679,16 @@ export function ptrByteSlice(p: Place, len: number, shared: boolean): By /** * `ptr::write` of `value` through a pointer to an object: `value` copied into it, one - * object deep, or moved by `$move`, so every pointer to it sees the write. + * object deep, or moved by `$move`, so every pointer to it sees the write. A field `value` + * leaves out, such as a `HashMap`'s capacity it never set, goes from `p` too. */ export function ptrWrite(p: T, value: T): void { if (ArrayBuffer.isView(p)) (p as unknown as Uint8Array).set(value as unknown as Uint8Array); else if (moved(value)) value.$move(p as T & Moved); - else Object.assign(p, value); + else { + if (!Array.isArray(p)) for (const key of Object.keys(p)) if (!Object.hasOwn(value, key)) delete (p as Record)[key]; + Object.assign(p, value); + } } /** diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 15b5f2296..363a1915b 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -29,9 +29,18 @@ use super::{AdtRepr, Translator, field_name, names}; use crate::program::{SymKind, Symbol}; use crate::ts::{self, Expr, Key, Param, Stmt, StmtKind}; -/// What a library object holds inline: each field of its JS object holding a value and -/// the type held there, and the types it holds where no copy reaches. -type Inline<'tcx> = (Vec<(&'static str, Ty<'tcx>)>, Vec>); +/// What a library object holds inline, which its copy one object deep would share. +#[derive(Default)] +struct Inline<'tcx> { + /// Each field of its JS object holding a value inline, and the type held there. + fields: Vec<(&'static str, Ty<'tcx>)>, + /// Each field holding the JS array of its storage, whose length or slots the runtime + /// changes where Rust changes only the object's own length, as a `Vec`'s `set_len` and + /// a `VecDeque`'s `pop_front` do: copied, sharing its items. + storage: Vec<&'static str>, + /// The types it holds where no copy reaches, a hash map's hasher. + unreachable: Vec>, +} impl<'a, 'tcx> Translator<'a, 'tcx> { /// Whether a `ty` is copied where Rust copies it: a `Copy` value that is, or holds by @@ -71,10 +80,11 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { || self.addressed(ty) || !ty.is_freeze(self.tcx, self.typing_env()) || match self.inline_held(ty) { - Some((fields, unreachable)) => fields + Some(inline) => inline + .fields .into_iter() .map(|(_, t)| t) - .chain(unreachable) + .chain(inline.unreachable) .any(|t| self.changes(t)), // Any type it is over may be held inline, which its copy refuses. None => args.types().any(|t| self.changes(t)), @@ -95,16 +105,21 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// What a library object `ty` holds inline rather than on the heap, which a copy of the /// object one object deep shares: each field of its JS object holding such a value and /// the type held there, a cell's, a lock's, a range's ends, or a one-time cell's, which - /// holds nothing until set; and the types it holds where no copy reaches, a hash map's - /// hasher. A collection's items, and what a `Weak` points at, are on the heap, which - /// Rust's read shares too. `None` - /// for any other library object, such as a `LazyCell` or a `Peekable`, whose inline - /// state only its runtime class knows, so a read that would copy one is refused. + /// holds nothing until set; the JS arrays holding a collection's storage whose length + /// is its own; and the types it holds where no copy reaches, a hash map's hasher. A + /// collection's items, and what a `Weak` points at, are on the heap, which Rust's read + /// shares too. `None` for any other library object, such as a `LazyCell` or a + /// `Peekable`, whose inline state only its runtime class knows, so a read that would + /// copy one is refused. fn inline_held(&self, ty: Ty<'tcx>) -> Option> { let ty::Adt(adt, args) = ty.kind() else { return None; }; let at = |i: usize| args.type_at(i); + let storage = |field: &'static str| Inline { + storage: vec![field], + ..Inline::default() + }; let fields = match Std::of(self.tcx, adt.did()) { Some(Std::Cell) => vec![("v", at(0))], Some(Std::RefCell | Std::Mutex) => vec![("value", at(0))], @@ -112,10 +127,12 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(Std::Range) => vec![("start", at(0)), ("end", at(0))], Some(Std::RangeInclusive) => vec![("lo", at(0)), ("hi", at(0))], Some(Std::RangeFrom) => vec![("start", at(0))], + // A `Vec`'s length is apart from its bytes. + Some(Std::Vec | Std::BinaryHeap) if !super::byte(at(0)) => return Some(storage("items")), + Some(Std::VecDeque) => return Some(storage("b")), Some( Std::Vec | Std::String - | Std::VecDeque | Std::LinkedList | Std::BinaryHeap | Std::BTreeMap @@ -124,11 +141,14 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { | Std::BytesMut | Std::Weak, ) => Vec::new(), - Some(Std::HashMap) => return Some((Vec::new(), vec![at(2)])), - Some(Std::HashSet) => return Some((Vec::new(), vec![at(1)])), + Some(Std::HashMap) => return Some(Inline { unreachable: vec![at(2)], ..Inline::default() }), + Some(Std::HashSet) => return Some(Inline { unreachable: vec![at(1)], ..Inline::default() }), _ => return None, }; - Some((fields, Vec::new())) + Some(Inline { + fields, + ..Inline::default() + }) } /// Whether the runs before compared pointers to a `ty`, or to a type it holds by value, @@ -402,7 +422,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { // A library object, which [`Self::changes`] says a whole write lands in, and a // copy of what it holds inline. Some(std) => { - let Some((fields, unreachable)) = self.inline_held(ty) else { + let Some(inline) = self.inline_held(ty) else { self.err( span, format!( @@ -411,7 +431,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { ); return ret(v()); }; - if unreachable.into_iter().any(|t| self.changes(t)) { + if inline.unreachable.into_iter().any(|t| self.changes(t)) { self.err( span, format!("a read moving out a `{ty}`, which holds inline a value JS can't copy apart"), @@ -422,13 +442,17 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { let held = self.snapshot(v().member("v"), args.type_at(0), span); return ret(Expr::New(Box::new(Expr::Rs("Cell".into())), vec![held])); } - let fields: Vec<_> = fields.into_iter().filter(|&(_, t)| self.changes(t)).collect(); + let fields: Vec<_> = inline.fields.into_iter().filter(|&(_, t)| self.changes(t)).collect(); let copy = Expr::rs("objectCopy", vec![v()]); - if fields.is_empty() { + if fields.is_empty() && inline.storage.is_empty() { return ret(copy); } let c = || Expr::ident("c"); let mut body = vec![StmtKind::decl(false, "c", None, Some(copy)).at(None)]; + for field in inline.storage { + let items = c().member(field).member("slice").call(Vec::new()); + body.push(StmtKind::Expr(c().member(field).assign(items)).at(None)); + } for (field, t) in fields { let read = self.snapshot(c().member(field), t, span); body.push(StmtKind::Expr(c().member(field).assign(read)).at(None)); diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index bce21d8f8..1472e2790 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -63,7 +63,8 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { /// Why the call of std's `def_id` would fail at run time where Rust's is defined, if the /// body proves it would: `offset_from` between pointers JS keeps in different arrays, a /// raw or mutable slice of a byte in no array of bytes, or a pointer, slice, or copy - /// reaching out of an array of one or a copied byte. + /// reaching out of an array of one or a copied byte, or moved by an offset the body + /// doesn't fix from an array of one. pub(super) fn stray( &self, def_id: rustc_hir::def_id::DefId, @@ -93,7 +94,15 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { return Some(super::alloc::BYTES.into()); } let out = |o: Origin<'tcx>, n: Option, what: &str| { - let index = o.index?; + // An offset the body doesn't fix may move a pointer out of an array of one. + let Some(index) = o.index else { + return matches!(o.array, Array::Lone(_)).then(|| { + format!( + "{what} moved by an offset the body doesn't fix from {}, which JS keeps apart from what holds it in Rust", + o.array.what() + ) + }); + }; let past = o.array.len().is_some_and(|len| index + n.unwrap_or(0) > len); (index < 0 || past).then(|| { format!( From 0b375b8b34690dbe88aa33b76cb9873b8ffb33d5 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 10:53:19 -0800 Subject: [PATCH 22/45] chore(ts): format, regenerate the conversions snapshot Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/conversions/expected/lib.ts | 4 +++- ts/fixtures/lang/pointers/src/pointers.rs | 5 ++++- ts/src/lower/copy.rs | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ts/fixtures/lang/conversions/expected/lib.ts b/ts/fixtures/lang/conversions/expected/lib.ts index a448b3248..c52db795d 100644 --- a/ts/fixtures/lang/conversions/expected/lib.ts +++ b/ts/fixtures/lang/conversions/expected/lib.ts @@ -10,7 +10,9 @@ export function copy$String(v: rs.String): rs.String { } export function copy$Vec_u32(v: rs.Vec): rs.Vec { - return rs.objectCopy(v); + const c = rs.objectCopy(v); + c.items = c.items.slice(); + return c; } export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 039308d4b..c7a8cd4fb 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2767,7 +2767,10 @@ fn inline_moved(r: &RefCell>, o: &OnceCell>) -> (RefCell, mut q: std::collections::VecDeque) -> (Vec, std::collections::VecDeque, Option) { +fn shrunk_moved( + v: Vec, + mut q: std::collections::VecDeque, +) -> (Vec, std::collections::VecDeque, Option) { let mut v = mem::ManuallyDrop::new(v); let old_v = unsafe { ptr::read(&*v) }; unsafe { v.set_len(0) }; diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 363a1915b..30421f638 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -141,8 +141,18 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { | Std::BytesMut | Std::Weak, ) => Vec::new(), - Some(Std::HashMap) => return Some(Inline { unreachable: vec![at(2)], ..Inline::default() }), - Some(Std::HashSet) => return Some(Inline { unreachable: vec![at(1)], ..Inline::default() }), + Some(Std::HashMap) => { + return Some(Inline { + unreachable: vec![at(2)], + ..Inline::default() + }); + } + Some(Std::HashSet) => { + return Some(Inline { + unreachable: vec![at(1)], + ..Inline::default() + }); + } _ => return None, }; Some(Inline { From 742b2bd574e893efc7f4e26587b263697961e3b0 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:00:56 -0800 Subject: [PATCH 23/45] docs(ts): note storage copies and unfixed offsets from a lone place Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/README.md b/ts/README.md index 8d1748e23..551d6bc4f 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: the `Vec` it owns, or an `rs.Borrowed` holding the slice it borrows, `rs.Cow>` 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`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, 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, copy, or write of bytes 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, with what a cell, lock, or range holds inline copied too, and one with interior mutability, such as a `Cell`, always; any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, is refused where the read would copy it | +| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, 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, copy, or write of bytes the body shows reaching past either, a pointer it shows moved from a value in no array by an offset it doesn't fix, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, with what a cell, lock, or range holds inline, and the array a `Vec`'s or `VecDeque`'s items are in, copied too, and one with interior mutability, such as a `Cell`, always; any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, is refused where the read would copy it | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined`, as `Box::new_uninit` and each of `Box::new_uninit_slice`'s is; a slice's `write_clone_of_slice` clones into its slots without dropping what they held. `Box::new_zeroed`'s holds the value of zero bytes in each part they are one of, such as a number, `false`, `None`, a null pointer, or an enum's variant of a tag or niche of zero, and `undefined` in each part they are none of, such as a reference, for a write to fill before a read | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | From c77552afc0dce7c2f1ccc18562ed2318c45a8542 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:11:35 -0800 Subject: [PATCH 24/45] fix(ts): keep a place's accessors on a whole write, refuse unfixed wrapping offsets and copy counts from a lone place Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/pointers.ts | 19 +++++++- ts/fixtures/lang/pointers/src/pointers.rs | 14 ++++++ ts/fixtures/reject/census.txt | 4 +- ts/fixtures/reject/expected.txt | 4 +- ts/fixtures/reject/src/alloc.rs | 13 ++++-- ts/fixtures/reject/ts.toml | 2 +- ts/runtime/rs.ts | 7 ++- ts/src/lower/mir/origin.rs | 43 ++++++++++++++----- 8 files changed, 85 insertions(+), 21 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 3c2680840..8a13ee3ef 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -3615,6 +3615,14 @@ export function shrunkMoved(v: rs.Vec, q: rs.VecDeque): [rs.Vec< return [oldV, copy$collections_VecDeque_u32(q), rs.dequePopFront(q)] as [rs.Vec, rs.VecDeque, rs.u32 | undefined]; } +/** + * A cell written whole through a pointer from the shared reference given, which may be + * the cell `Cell::from_mut` makes of another place. + */ +export function cellRewritten(r: rs.Cell, n: rs.u32): void { + rs.placeIn(r).v = new rs.Cell(n); +} + /** * A `HashMap` written whole through a pointer from the `&mut` given, which holds what the * new one does, its capacity included. @@ -3643,8 +3651,8 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { let _7!: rs.u8; let _28!: rs.u8; const with_: (a0: rs.u8) => rs.ByteVec = (n: rs.u8): rs.ByteVec => { - return rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b2: rs.u8): rs.u8 => { - return rs.u8WrappingAdd(b2, n); + return rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b3: rs.u8): rs.u8 => { + return rs.u8WrappingAdd(b3, n); }), undefined, true); }; let iterStart: rs.u8 = 0; @@ -3769,6 +3777,13 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { rs.hashExtend(map, _232, undefined, undefined, undefined, 117440512); mapRewritten(map); rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.hashLen(map) & 0xff, rs.hashCapacity(map) & 0xff)), undefined, true); + for (const b2 of rs.byteSliceView(data)) { + let x2: rs.u32 = b2 as rs.u32; + cellRewritten(rs.cellOfPlace(rs.place(() => x2, (value3) => x2 = value3)), rs.u32Add(b2 as rs.u32, 1)); + const cell2: rs.Cell = new rs.Cell(b2 as rs.u32); + cellRewritten(cell2, rs.u32Add(b2 as rs.u32, 2)); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(x2 & 0xff, cell2.v & 0xff)), undefined, true); + } } /** diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index c7a8cd4fb..4022201f2 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2780,6 +2780,13 @@ fn shrunk_moved( (old_v, old_q, popped) } +/// A cell written whole through a pointer from the shared reference given, which may be +/// the cell `Cell::from_mut` makes of another place. +#[inline(never)] +fn cell_rewritten(r: &Cell, n: u32) { + unsafe { ptr::from_ref(r).cast_mut().write(Cell::new(n)) } +} + /// A `HashMap` written whole through a pointer from the `&mut` given, which holds what the /// new one does, its capacity included. #[inline(never)] @@ -2870,6 +2877,13 @@ pub fn moved_out(data: &[u8], out: &mut Vec) { map.extend(nums.iter().map(|&n| (n, n))); map_rewritten(&mut map); out.extend([map.len() as u8, map.capacity() as u8]); + for &b in data { + let mut x = u32::from(b); + cell_rewritten(Cell::from_mut(&mut x), u32::from(b) + 1); + let cell = Cell::new(u32::from(b)); + cell_rewritten(&cell, u32::from(b) + 2); + out.extend([x as u8, cell.get() as u8]); + } } /// Pointers to slices cast to ones to arrays of their elements, of bytes and of numbers: diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 7c962d6f8..80bcdebea 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -12,10 +12,12 @@ fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart -fixtures/reject/src/alloc.rs:offset_reference+1:34: a pointer moved by an offset the body doesn't fix from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:unfixed_reference+3:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:unfixed_reference+4:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:unfixed_reference+5:3: a copy of a count the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:13: unsupported call to `std::alloc::alloc_zeroed` fixtures/reject/src/buffers.rs:blank_bytes+1:2: `Buf::copy_to_bytes` of `buffers::Blank`, a value JS copies, which is not supported diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 001e9e243..1201c97a4 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -16,10 +16,12 @@ fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart -fixtures/reject/src/alloc.rs:offset_reference+1:34: a pointer moved by an offset the body doesn't fix from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:unfixed_reference+3:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:unfixed_reference+4:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:unfixed_reference+5:3: a copy of a count the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:zeroed+1:12: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:zeroed+1:13: unsupported call to `std::alloc::alloc_zeroed` fixtures/reject/src/buffers.rs:blank_bytes+1:2: `Buf::copy_to_bytes` of `buffers::Blank`, a value JS copies, which is not supported diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 98b108147..62f740cd6 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -41,9 +41,16 @@ pub fn lazy_moved(cell: std::cell::Cell) -> u32 { } /// A pointer from a reference the body is given moved by an offset the body doesn't fix, -/// which may reach past the value JS keeps in no array. -pub fn offset_reference(r: &u32, n: usize) -> u32 { - unsafe { *std::ptr::from_ref(r).add(n) } +/// wrapping or not, and copied from by a count it doesn't fix, each of which may reach +/// past the value JS keeps in no array. +pub fn unfixed_reference(r: &u32, n: usize) -> [u32; 3] { + let mut out = [0u32; 3]; + unsafe { + out[0] = *std::ptr::from_ref(r).add(n); + out[1] = *std::ptr::from_ref(r).wrapping_add(n); + std::ptr::copy_nonoverlapping(std::ptr::from_ref(r), out.as_mut_ptr(), n); + } + out } /// A `Peekable` moved out through a pointer with a cell peeked, which only its runtime diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 80f0a63bc..f0dfebb92 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -6,10 +6,10 @@ roots = [ "alloc::bytes_as_values", "alloc::infallible_cell", "alloc::lazy_moved", - "alloc::offset_reference", "alloc::past_reference", "alloc::peeked_moved", "alloc::reinterpreted", + "alloc::unfixed_reference", "alloc::zeroed", "bound_ranges", "buffers::blank_bytes", diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index 439e28b6a..258ca2fad 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -680,13 +680,16 @@ export function ptrByteSlice(p: Place, len: number, shared: boolean): By /** * `ptr::write` of `value` through a pointer to an object: `value` copied into it, one * object deep, or moved by `$move`, so every pointer to it sees the write. A field `value` - * leaves out, such as a `HashMap`'s capacity it never set, goes from `p` too. + * leaves out, such as a `HashMap`'s capacity it never set, goes from `p` too, unless `p` + * is of another class, such as the place `Cell::from_mut` makes, whose setters take it. */ export function ptrWrite(p: T, value: T): void { if (ArrayBuffer.isView(p)) (p as unknown as Uint8Array).set(value as unknown as Uint8Array); else if (moved(value)) value.$move(p as T & Moved); else { - if (!Array.isArray(p)) for (const key of Object.keys(p)) if (!Object.hasOwn(value, key)) delete (p as Record)[key]; + if (!Array.isArray(p) && Object.getPrototypeOf(p) === Object.getPrototypeOf(value)) { + for (const key of Object.keys(p)) if (!Object.hasOwn(value, key)) delete (p as Record)[key]; + } Object.assign(p, value); } } diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 1472e2790..1d15ee9e1 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -93,15 +93,18 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { { return Some(super::alloc::BYTES.into()); } + // An offset or a count the body doesn't fix may reach out of an array of one. + let unfixed = |o: Origin<'tcx>, what: &str| { + matches!(o.array, Array::Lone(_)).then(|| { + format!( + "{what} the body doesn't fix, from {}, which JS keeps apart from what holds it in Rust", + o.array.what() + ) + }) + }; let out = |o: Origin<'tcx>, n: Option, what: &str| { - // An offset the body doesn't fix may move a pointer out of an array of one. let Some(index) = o.index else { - return matches!(o.array, Array::Lone(_)).then(|| { - format!( - "{what} moved by an offset the body doesn't fix from {}, which JS keeps apart from what holds it in Rust", - o.array.what() - ) - }); + return unfixed(o, &format!("{what} moved by an offset")); }; let past = o.array.len().is_some_and(|len| index + n.unwrap_or(0) > len); (index < 0 || past).then(|| { @@ -139,6 +142,14 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let o = self.moved(self.origin(arg(0)?)?, name.as_str(), arg(1)?); out(o, None, "a pointer") } + // A wrapping offset may leave the array while nothing reads through it. + ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { + let o = self.origin(arg(0)?)?; + match self.const_int(arg(1)?) { + Some(_) => None, + None => unfixed(o, "a pointer moved by an offset"), + } + } ("raw", "from_raw_parts" | "from_raw_parts_mut") | ("ptr", "slice_from_raw_parts" | "slice_from_raw_parts_mut") | ("NonNull", "slice_from_raw_parts") => { @@ -165,11 +176,21 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ) | ("NonNull", "copy_to" | "copy_to_nonoverlapping" | "copy_from" | "copy_from_nonoverlapping") => { let n = self.const_int(arg(2)?); - [arg(0)?, arg(1)?] - .into_iter() - .find_map(|p| out(self.origin(p)?, n, "a copy")) + [arg(0)?, arg(1)?].into_iter().find_map(|p| { + let o = self.origin(p)?; + match n { + Some(_) => out(o, n, "a copy"), + None => unfixed(o, "a copy of a count"), + } + }) + } + ("ptr" | "NonNull", "write_bytes") => { + let o = self.origin(arg(0)?)?; + match self.const_int(arg(2)?) { + Some(n) => out(o, Some(n), "a write"), + None => unfixed(o, "a write of a count"), + } } - ("ptr" | "NonNull", "write_bytes") => out(self.origin(arg(0)?)?, self.const_int(arg(2)?), "a write"), _ => None, } } From e2a5e8cb9034dee8e2a9e3365452f714fa78fbc2 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:18:11 -0800 Subject: [PATCH 25/45] feat(ts): refuse unsplit collection moves and offsets off a lone place A move out through a pointer (`ptr::read`, `replace`, `swap`, `copy`, `assume_init_read`, `ManuallyDrop::take`) of a collection whose JS object keeps its length with its storage (a non-byte `Vec`, `VecDeque`, `String`, map, set, or list) is refused with a span, as no JS copy splits from the original as Rust's does. The storage copy in the shim is gone; safe moves keep the shallow copy, which is exact there. From an array of one (a pointer from a reference with no visible origin), every offset, wrapping or not, must stay at its element, and every count or slice length must be fixed and stay within it, or it is refused. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- .../lang/pointers/expected/core/option.ts | 14 - ts/fixtures/lang/pointers/expected/lib.ts | 24 +- .../pointers/expected/pointers.Drained.ts | 16 +- .../lang/pointers/expected/pointers.List.ts | 88 +++--- .../lang/pointers/expected/pointers.Node.ts | 8 +- .../lang/pointers/expected/pointers.ts | 257 ++++++------------ ts/fixtures/lang/pointers/src/pointers.rs | 47 +--- ts/fixtures/reject/census.txt | 8 + ts/fixtures/reject/expected.txt | 8 + ts/fixtures/reject/src/alloc.rs | 33 +++ ts/fixtures/reject/ts.toml | 2 + ts/src/lower/copy.rs | 76 +++--- ts/src/lower/mir/origin.rs | 34 ++- ts/src/lower/pointer.rs | 29 +- 15 files changed, 287 insertions(+), 359 deletions(-) diff --git a/ts/README.md b/ts/README.md index 38930c6a0..2d0ee4e9f 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: the `Vec` it owns, or an `rs.Borrowed` holding the slice it borrows, `rs.Cow>` 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`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, and view; one to a value in no array is element 0 of an array of one, as in Rust, but a byte variable whose `&raw` the body takes, or that of a reference to it made there, as `&mut b as *mut u8` does, is element 0 of a `Uint8Array` of its own, which its raw, mutable, and shared slices view as they do any other bytes; JS keeps an array of one, and a shared subslice of a byte in no array, which JS copies, apart from what holds it in Rust, so a pointer, slice, copy, or write of bytes the body shows reaching past either, a pointer it shows moved from a value in no array by an offset, or a copy or write of a count, it doesn't fix, and `offset_from` between pointers the body shows JS keeps in different arrays, are refused, and through a pointer from elsewhere any of these, a raw or mutable slice of a byte in no array, such as a field's, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `Vec` one object deep, with what a cell, lock, or range holds inline, and the array a `Vec`'s or `VecDeque`'s items are in, copied too, and one with interior mutability, such as a `Cell`, always; any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, is refused where the read would copy it | +| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, and view; one to a value in no array is element 0 of an array of one, as in Rust, but a byte variable whose `&raw` the body takes, or that of a reference to it made there, as `&mut b as *mut u8` does, is element 0 of a `Uint8Array` of its own, which its raw, mutable, and shared slices view as they do any other bytes; JS keeps an array of one, and a shared subslice of a byte in no array, which JS copies, apart from what holds it in Rust, so a pointer, slice, copy, or write of bytes the body shows reaching past either, a pointer it shows moved from a value in no array off it, or by an offset, or a copy, write, or slice of a count or length, it doesn't fix, and `offset_from` between pointers the body shows JS keeps in different arrays, are refused, and through a pointer from elsewhere any of these, a raw or mutable slice of a byte in no array, such as a field's, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `RefCell` or a `Vec` one object deep, with what a cell, lock, or range holds inline, and one with interior mutability, such as a `Cell`, always; a collection whose JS object keeps its length with its storage, such as a `Vec` of other than bytes, a `VecDeque`, a `String`, a map, or a set, which no copy splits from the original as Rust's read does, and any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, are refused where the read would copy them | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined`, as `Box::new_uninit` and each of `Box::new_uninit_slice`'s is; a slice's `write_clone_of_slice` clones into its slots without dropping what they held. `Box::new_zeroed`'s holds the value of zero bytes in each part they are one of, such as a number, `false`, `None`, a null pointer, or an enum's variant of a tag or niche of zero, and `undefined` in each part they are none of, such as a reference, for a write to fill before a read | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 9ac98ea05..fec4d68cd 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -55,13 +55,6 @@ export function map$00buaky(self: rs.u16 | undefined | rs.None, f: (a0: rs.u16 | return f(self); } -export function map$1lzqy5c(self: rs.String | undefined, f: (a0: rs.String) => rs.String): rs.String | undefined { - if (self === undefined) { - return undefined; - } - return f(self); -} - export function map$1h9tk16(self: rs.ByteVec | undefined, f: (a0: rs.ByteVec) => rs.ByteVec): rs.ByteVec | undefined { if (self === undefined) { return undefined; @@ -139,13 +132,6 @@ export function mapOr$0smr0aj(self: rs.u16 | undefined, default_: Uint8Array, _: return rs.u16ToBeBytes(self); } -export function mapOr$07pvkk0(self: rs.u32 | undefined, default_: rs.u8, f: (a0: rs.u32) => rs.u8): rs.u8 { - if (self === undefined) { - return default_; - } - return f(self); -} - export function filter$0wvwd70(self: rs.u32 | undefined, predicate: (a0: rs.u32) => boolean): rs.u32 | undefined { if (self !== undefined) { const x: rs.u32 = self; diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index ef94d0827..564155089 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -41,16 +41,6 @@ export function copy$Option_tuple2_u16_u8(v: [rs.u16, rs.u8] | undefined): [rs.u return v === undefined ? v : copy$tuple2_u16_u8(v); } -export function copy$String(v: rs.String): rs.String { - return rs.objectCopy(v); -} - -export function copy$Vec_u32(v: rs.Vec): rs.Vec { - const c = rs.objectCopy(v); - c.items = c.items.slice(); - return c; -} - export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { return rs.objectCopy(v); } @@ -91,12 +81,6 @@ export function copy$cell_RefCell_Cell_u32(v: rs.RefCell>): rs.R return c; } -export function copy$collections_VecDeque_u32(v: rs.VecDeque): rs.VecDeque { - const c = rs.objectCopy(v); - c.b = c.b.slice(); - return c; -} - export function copy$mem_MaybeUninit_Vec_u8(v: rs.ByteVec): rs.ByteVec { if (v === undefined) { return v; @@ -260,8 +244,8 @@ export function copy$tuple2_u32_u32(v: [rs.u32, rs.u32]): [rs.u32, rs.u32] { return [v[0], v[1]]; } -export function copy$tuple2_u8_String(v: [rs.u8, rs.String]): [rs.u8, rs.String] { - return [v[0], copy$String(v[1])]; +export function copy$tuple2_u8_Vec_u8(v: [rs.u8, rs.ByteVec]): [rs.u8, rs.ByteVec] { + return [v[0], copy$Vec_u8(v[1])]; } export function copy$tuple2_u8_sync_Arc_u8(v: [rs.u8, rs.Arc]): [rs.u8, rs.Arc] { @@ -272,8 +256,8 @@ export function copy$tuple2_u8_u8(v: [rs.u8, rs.u8]): [rs.u8, rs.u8] { return [v[0], v[1]]; } -export function drop$pointers_List_String(_1: List.List): void { - List.drop$ops_Drop_String(_1); +export function drop$pointers_List_Vec_u8(_1: List.List): void { + List.drop$ops_Drop_Vec_u8(_1); } export function drop$pointers_List_sync_Arc_u8(_1: List.List>): void { diff --git a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts index 5e2a38872..36f4394f1 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Drained.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Drained.ts @@ -1,8 +1,8 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$String, copy$Vec_u8, copy$sync_Weak_u32 } from "./lib.ts"; -import { map$00buaky as option_map$00buaky, map$1lzqy5c as option_map$1lzqy5c, map$1h9tk16 as option_map$1h9tk16, map$1ng7x7l as option_map$1ng7x7l, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; +import { copy$Vec_u8, copy$sync_Weak_u32 } from "./lib.ts"; +import { map$00buaky as option_map$00buaky, map$1h9tk16 as option_map$1h9tk16, map$1ng7x7l as option_map$1ng7x7l, map$070fw7m as option_map$070fw7m } from "./core/option.ts"; /** * What smallvec's `Drain` is: the elements of a slice nothing will drop again, each read @@ -18,12 +18,6 @@ export function next$iter_Iterator_Option_u16(self: Drained) }), rs.None); } -export function next$iter_Iterator_String(self: Drained): rs.String | rs.Done { - return rs.iterFromOption(option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.String): rs.String => { - return copy$String(r); - })); -} - export function next$iter_Iterator_Vec_u8(self: Drained): rs.ByteVec | rs.Done { return rs.iterFromOption(option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).next()), (r: rs.ByteVec): rs.ByteVec => { return copy$Vec_u8(r); @@ -48,12 +42,6 @@ export function nextBack$iter_DoubleEndedIterator_Option_u16(self: Drained): rs.String | undefined { - return option_map$1lzqy5c(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.String): rs.String => { - return copy$String(r); - }); -} - export function nextBack$iter_DoubleEndedIterator_Vec_u8(self: Drained): rs.ByteVec | undefined { return option_map$1h9tk16(rs.iterToOption((self.iter as rs.Iter).nextBack()), (r: rs.ByteVec): rs.ByteVec => { return copy$Vec_u8(r); diff --git a/ts/fixtures/lang/pointers/expected/pointers.List.ts b/ts/fixtures/lang/pointers/expected/pointers.List.ts index b812f3f11..4bbebb0fb 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.List.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.List.ts @@ -1,8 +1,8 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$tuple2_u8_String, copy$tuple2_u8_sync_Arc_u8, drop$tuple2_u8_sync_Arc_u8 } from "./lib.ts"; -import { attachBefore$String, attachBefore$sync_Arc_u8, detach$String, detach$sync_Arc_u8 } from "./pointers.ts"; +import { copy$tuple2_u8_Vec_u8, copy$tuple2_u8_sync_Arc_u8, drop$tuple2_u8_sync_Arc_u8 } from "./lib.ts"; +import { attachBefore$Vec_u8, attachBefore$sync_Arc_u8, detach$Vec_u8, detach$sync_Arc_u8 } from "./pointers.ts"; import type * as Free from "./pointers.Free.ts"; import type * as Links from "./pointers.Links.ts"; import * as Node from "./pointers.Node.ts"; @@ -21,7 +21,7 @@ export class List { } } -export function new$String(): List { +export function new$Vec_u8(): List { return new List(undefined, undefined, 0); } @@ -29,13 +29,13 @@ export function new$sync_Arc_u8(): List> { return new List(undefined, undefined, 0); } -export function guard$String(self: List): Node.Node { +export function guard$Vec_u8(self: List): Node.Node { if (self.guard !== undefined) { return self.guard; } - const guard2: Node.Node = Node.new$String(); - const _8: Links.Links = { next: guard2, prev: guard2 }; - const _11: Node.Node = guard2; + const guard2: Node.Node = Node.new$Vec_u8(); + const _8: Links.Links = { next: guard2, prev: guard2 }; + const _11: Node.Node = guard2; _11.link.at0 = _8.next; _11.link.at4 = _8.prev; self.guard = guard2; @@ -55,11 +55,11 @@ export function guard$sync_Arc_u8(self: List>): Node.Node): Node.Node { +export function alloc$Vec_u8(self: List): Node.Node { if (self.free === undefined) { - return Node.new$String(); + return Node.new$Vec_u8(); } - const free: Node.Node = self.free; + const free: Node.Node = self.free; self.free = free.link.at0; return free; } @@ -73,11 +73,11 @@ export function alloc$sync_Arc_u8(self: List>): Node.Node, key: rs.u8, value: rs.String): void { - const guard: Node.Node = guard$String(self); - const node: Node.Node = alloc$String(self); - rs.ptrWrite(node.entry, [key, value] as [rs.u8, rs.String]); - attachBefore$String(node, guard); +export function pushBack$Vec_u8(self: List, key: rs.u8, value: rs.ByteVec): void { + const guard: Node.Node = guard$Vec_u8(self); + const node: Node.Node = alloc$Vec_u8(self); + rs.ptrWrite(node.entry, [key, value] as [rs.u8, rs.ByteVec]); + attachBefore$Vec_u8(node, guard); self.len = rs.usizeAdd(self.len, 1); } @@ -89,14 +89,14 @@ export function pushBack$sync_Arc_u8(self: List>, key: rs.u8, valu self.len = rs.usizeAdd(self.len, 1); } -export function find$String(self: List, key: rs.u8): Node.Node | undefined { - const _4: Node.Node | undefined = self.guard; +export function find$Vec_u8(self: List, key: rs.u8): Node.Node | undefined { + const _4: Node.Node | undefined = self.guard; if (_4 !== undefined) { - const guard: Node.Node = _4; - let at: Node.Node = guard.link.at0 as Node.Node; + const guard: Node.Node = _4; + let at: Node.Node = guard.link.at0 as Node.Node; while (at !== guard) { if (!(at.entry[0] === key)) { - at = at.link.at0 as Node.Node; + at = at.link.at0 as Node.Node; } else { return at; } @@ -123,13 +123,13 @@ export function find$sync_Arc_u8(self: List>, key: rs.u8): Node.No return undefined; } -export function remove$String(self: List, key: rs.u8): [rs.u8, rs.String] | undefined { - const _4: Node.Node | undefined = find$String(self, key); +export function remove$Vec_u8(self: List, key: rs.u8): [rs.u8, rs.ByteVec] | undefined { + const _4: Node.Node | undefined = find$Vec_u8(self, key); if (_4 !== undefined) { - const node: Node.Node = _4; - detach$String(node); - const entry: [rs.u8, rs.String] = copy$tuple2_u8_String(node.entry); - const _16: Free.Free = { next: self.free }; + const node: Node.Node = _4; + detach$Vec_u8(node); + const entry: [rs.u8, rs.ByteVec] = copy$tuple2_u8_Vec_u8(node.entry); + const _16: Free.Free = { next: self.free }; node.link.at0 = _16.next; self.free = node; self.len = rs.usizeSub(self.len, 1); @@ -153,14 +153,14 @@ export function remove$sync_Arc_u8(self: List>, key: rs.u8): [rs.u return undefined; } -export function toFront$String(self: List, key: rs.u8): boolean { - const _4: Node.Node | undefined = find$String(self, key); +export function toFront$Vec_u8(self: List, key: rs.u8): boolean { + const _4: Node.Node | undefined = find$Vec_u8(self, key); if (_4 !== undefined) { - const node: Node.Node = _4; + const node: Node.Node = _4; if (self.guard !== undefined) { - const guard: Node.Node = self.guard; - detach$String(node); - attachBefore$String(node, guard.link.at0 as Node.Node); + const guard: Node.Node = self.guard; + detach$Vec_u8(node); + attachBefore$Vec_u8(node, guard.link.at0 as Node.Node); return true; } return false; @@ -183,38 +183,38 @@ export function toFront$sync_Arc_u8(self: List>, key: rs.u8): bool return false; } -export function getMut$String(self: List, key: rs.u8): rs.String | undefined { - const _3: Node.Node | undefined = find$String(self, key); +export function getMut$Vec_u8(self: List, key: rs.u8): rs.ByteVec | undefined { + const _3: Node.Node | undefined = find$Vec_u8(self, key); if (_3 !== undefined) { return _3.entry[1]; } return undefined; } -export function each$String(self: List, out: rs.ByteVec): void { +export function each$Vec_u8(self: List, out: rs.ByteVec): void { b0: { if (self.guard !== undefined) { - const guard: Node.Node = self.guard; - let at: Node.Node = guard.link.at0 as Node.Node; + const guard: Node.Node = self.guard; + let at: Node.Node = guard.link.at0 as Node.Node; while (true) { if (!(at !== guard)) { break b0; } - const _13: [rs.u8, rs.String] = at.entry; + const _13: [rs.u8, rs.ByteVec] = at.entry; rs.byteVecPush(out, _13[0]); - rs.byteVecExtendFromSlice(out, rs.stringAsBytes(_13[1].v)); - at = at.link.at0 as Node.Node; + rs.byteVecExtendFromSlice(out, rs.byteVecSlice(_13[1])); + at = at.link.at0 as Node.Node; } } } } -export function drop$ops_Drop_String(self: List): void { +export function drop$ops_Drop_Vec_u8(self: List): void { if (self.guard !== undefined) { - const guard: Node.Node = self.guard; - let at: Node.Node = guard.link.at0 as Node.Node; + const guard: Node.Node = self.guard; + let at: Node.Node = guard.link.at0 as Node.Node; while (at !== guard) { - at = at.link.at0 as Node.Node; + at = at.link.at0 as Node.Node; } } while (self.free !== undefined) { diff --git a/ts/fixtures/lang/pointers/expected/pointers.Node.ts b/ts/fixtures/lang/pointers/expected/pointers.Node.ts index bcee03554..df628a145 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Node.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Node.ts @@ -9,10 +9,10 @@ export interface Node { link: Link.Link; } -export function new$String(): Node { - const _1: [rs.u8, rs.String] = ([undefined, undefined] as unknown) as [rs.u8, rs.String]; - const _3: Links.Links = { next: rs.DANGLING, prev: rs.DANGLING }; - return { entry: _1, link: ({ at0: _3.next, at4: _3.prev } as unknown) as Link.Link }; +export function new$Vec_u8(): Node { + const _1: [rs.u8, rs.ByteVec] = ([undefined, undefined] as unknown) as [rs.u8, rs.ByteVec]; + const _3: Links.Links = { next: rs.DANGLING, prev: rs.DANGLING }; + return { entry: _1, link: ({ at0: _3.next, at4: _3.prev } as unknown) as Link.Link }; } export function new$sync_Arc_u8(): Node> { diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 605b7e2aa..912806b0d 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,9 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$String, copy$Vec_u32, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$collections_VecDeque_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_String, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Cell_u32, copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u8, drop$pointers_List_Vec_u8, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; -import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, mapOr$07pvkk0 as option_mapOr$07pvkk0, filter$0wvwd70 } from "./core/option.ts"; +import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -53,13 +53,13 @@ import * as Wrapped from "./pointers.Wrapped.ts"; import type * as Written from "./pointers.Written.ts"; /** Put `node` in the list just before `at`. */ -export function attachBefore$String(node: Node.Node, at: Node.Node): void { - const _3: Links.Links = { next: at, prev: at.link.at4 }; - const _8: Node.Node = node; +export function attachBefore$Vec_u8(node: Node.Node, at: Node.Node): void { + const _3: Links.Links = { next: at, prev: at.link.at4 }; + const _8: Node.Node = node; _8.link.at0 = _3.next; _8.link.at4 = _3.prev; at.link.at4 = node; - (node.link.at4.link.at0 as Node.Node) = node; + (node.link.at4.link.at0 as Node.Node) = node; } /** Put `node` in the list just before `at`. */ @@ -72,9 +72,9 @@ export function attachBefore$sync_Arc_u8(node: Node.Node>, at: Nod (node.link.at4.link.at0 as Node.Node>) = node; } -export function detach$String(node: Node.Node): void { - (node.link.at4.link.at0 as Node.Node) = node.link.at0 as Node.Node; - (node.link.at0 as Node.Node).link.at4 = node.link.at4; +export function detach$Vec_u8(node: Node.Node): void { + (node.link.at4.link.at0 as Node.Node) = node.link.at0 as Node.Node; + (node.link.at0 as Node.Node).link.at4 = node.link.at4; } export function detach$sync_Arc_u8(node: Node.Node>): void { @@ -87,7 +87,7 @@ export function detach$sync_Arc_u8(node: Node.Node>): void { * finds and then the list in order. */ export function lists(dataData: Uint8Array, dataStart: number, dataLen: number, out: rs.ByteVec): void { - const list: List.List = List.new$String(); + const list: List.List = List.new$Vec_u8(); let dataData2: Uint8Array = dataData; let dataStart2: number = dataStart; let dataLen2: number = dataLen; @@ -105,36 +105,37 @@ export function lists(dataData: Uint8Array, dataStart: number, dataLen: number, const key2: rs.u8 = _13[1]; const _18: rs.u8 = rs.u8Rem(op2, 4); if (_18 === 0) { - const _26: rs.usize = rs.u8Rem(op2, 3) as rs.usize; - if (!(_26 < 3)) { - rs.panic("index out of bounds: the len is " + 3 + " but the index is " + _26); + const _22: rs.ByteSlice[] = [rs.byteSliceRange(rs.byteSliceFrom(Uint8Array.of()), 0), rs.byteSliceFrom(Uint8Array.of(118)), rs.byteSliceFrom(Uint8Array.of(118, 118))]; + const _29: rs.usize = rs.u8Rem(op2, 3) as rs.usize; + if (!(_29 < 3)) { + rs.panic("index out of bounds: the len is " + 3 + " but the index is " + _29); } - const _21: rs.String = rs.stringFrom(["", "v", "vv"][_26]); - List.pushBack$String(list, key2, _21); + const _21: rs.ByteVec = rs.byteSliceToByteVec(_22[_29]); + List.pushBack$Vec_u8(list, key2, _21); } else if (_18 === 1) { - const _30: [rs.u8, rs.String] | undefined = List.remove$String(list, key2); - if (_30 === undefined) { + const _33: [rs.u8, rs.ByteVec] | undefined = List.remove$Vec_u8(list, key2); + if (_33 === undefined) { rs.byteVecPush(out, 255); } else { - const k: rs.u8 = _30[0]; - const v: rs.String = _30[1]; + const k: rs.u8 = _33[0]; + const v: rs.ByteVec = _33[1]; rs.byteVecPush(out, k); - rs.byteVecExtendFromSlice(out, rs.stringAsBytes(v.v)); + rs.byteVecExtendFromSlice(out, rs.byteVecSlice(v)); } } else if (_18 === 2) { - rs.byteVecPush(out, List.toFront$String(list, key2) ? 1 : 0); + rs.byteVecPush(out, List.toFront$Vec_u8(list, key2) ? 1 : 0); } else { - const _42: rs.String | undefined = List.getMut$String(list, key2); - if (_42 === undefined) { + const _45: rs.ByteVec | undefined = List.getMut$Vec_u8(list, key2); + if (_45 === undefined) { rs.byteVecPush(out, 254); } else { - rs.stringPush(_42, 119); + rs.byteVecPush(_45, 119); } } } rs.byteVecPush(out, list.len & 0xff); - List.each$String(list, out); - drop$pointers_List_String(list); + List.each$Vec_u8(list, out); + drop$pointers_List_Vec_u8(list); } /** @@ -3009,31 +3010,6 @@ export function drained$Option_u16(v: rs.Vec, bits: rs.u8): } } -/** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ -export function drained$String(v: rs.Vec, bits: rs.u8): rs.Vec { - let next!: rs.String | undefined; - const v2: rs.Vec = v; - const iter: Drained.Drained = { iter: rs.iterSlice(v2.items) as rs.Iter> }; - const out: rs.Vec = new rs.Vec(); - let i: rs.i32 = 0; - while (true) { - if ((rs.u8Shr(bits, rs.i32Rem(i, 8)) & 1) === 0) { - next = rs.iterToOption(Drained.next$iter_Iterator_String(iter)); - } else { - next = Drained.nextBack$iter_DoubleEndedIterator_String(iter); - } - if (next !== undefined) { - out.items.push(next); - i = rs.i32Add(i, 1); - } else { - rs.vecSetLen(v2, 0, 12, (): rs.String => { - return (undefined as unknown) as rs.String; - }); - return out; - } - } -} - /** `v`'s elements, read out from both ends as `bits` picks, which nothing drops in `v`. */ export function drained$Vec_u8(v: rs.Vec, bits: rs.u8): rs.Vec { let next!: rs.ByteVec | undefined; @@ -3115,7 +3091,7 @@ export function drained$u32(v: rs.Vec, bits: rs.u8): rs.Vec { * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then * drops or moves to the front. */ -export function retained$Rc_pointers_Counted_pointers_behind_refs_closure10(v: rs.Vec>, keep: (a0: rs.Place>) => boolean): void { +export function retained$Rc_pointers_Counted_pointers_behind_refs_closure7(v: rs.Vec>, keep: (a0: rs.Place>) => boolean): void { let _10!: rs.usize; const len: rs.usize = v.items.length; const p: rs.Place> = rs.vecAsPtr(v, 4); @@ -3140,30 +3116,7 @@ export function retained$Rc_pointers_Counted_pointers_behind_refs_closure10(v: r * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then * drops or moves to the front. */ -export function retained$String_pointers_behind_refs_closure6(v: rs.Vec, keep: (a0: rs.String) => boolean): void { - let _10!: rs.usize; - const len: rs.usize = v.items.length; - const p: rs.Place = rs.vecAsPtr(v, 12); - let write: rs.usize = 0; - let iterStart: rs.usize = 0; - const iterEnd: rs.usize = len; - while ((_10 = iterStart++) < iterEnd) { - const _15: rs.Place = rs.ptrAdd(p, _10); - if (keep(_15.v)) { - rs.ptrCopy(_15, rs.ptrAdd(p, write), 1, copy$String); - write = rs.usizeAdd(write, 1); - } - } - rs.vecSetLen(v, write, 12, (): rs.String => { - return (undefined as unknown) as rs.String; - }); -} - -/** - * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then - * drops or moves to the front. - */ -export function retained$Vec_u8_pointers_behind_refs_closure8(v: rs.Vec, keep: (a0: rs.ByteVec) => boolean): void { +export function retained$Vec_u8_pointers_behind_refs_closure5(v: rs.Vec, keep: (a0: rs.ByteVec) => boolean): void { let _10!: rs.usize; const len: rs.usize = v.items.length; const p: rs.Place = rs.vecAsPtr(v, 12); @@ -3186,7 +3139,7 @@ export function retained$Vec_u8_pointers_behind_refs_closure8(v: rs.Vec>, keep: (a0: rs.Weak) => boolean): void { +export function retained$sync_Weak_u32_pointers_behind_refs_closure11(v: rs.Vec>, keep: (a0: rs.Weak) => boolean): void { let _10!: rs.usize; const len: rs.usize = v.items.length; const p: rs.Place> = rs.vecAsPtr(v, 4); @@ -3211,7 +3164,7 @@ export function retained$sync_Weak_u32_pointers_behind_refs_closure14(v: rs.Vec< * What smallvec's `retain` is: each element lent as a `&mut`, which a pointer from it then * drops or moves to the front. */ -export function retained$u32_pointers_behind_refs_closure4(v: rs.Vec, keep: (a0: rs.Place) => boolean): void { +export function retained$u32_pointers_behind_refs_closure3(v: rs.Vec, keep: (a0: rs.Place) => boolean): void { let _10!: rs.usize; const len: rs.usize = v.items.length; const p: rs.Place = rs.vecAsPtr(v, 4); @@ -3331,8 +3284,8 @@ export function cellBehind(r: rs.Cell | undefined, how: rs.u8): rs.u32 { * from references to an `Arc`'s value, measured against each other. */ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { - let _175!: rs.usize; - let _215!: rs.u8; + let _151!: rs.usize; + let _191!: rs.u8; const bits: rs.u8 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0); const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): rs.u32 => { return rs.u32Mul(_2 as rs.u32, 3); @@ -3340,49 +3293,33 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(drained$u32(rs.vecFrom(nums.items.slice()), bits).items), (_23: rs.u32): rs.u8 => { return _23 & 0xff; }), undefined, true); - const words: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b3: rs.u8): rs.String => { - return rs.fmtToString((f: rs.Formatter) => rs.u8Display(b3, f), 0); - }), undefined, 12, true); - for (const _25 of drained$String(rs.vecFrom(words.items.map((a0: rs.String): rs.String => { - return rs.stringClone(a0); - })), bits).items) { - rs.iterExtendBytes(out, rs.strBytes(_25.v), undefined, true); - } - const runs: rs.Vec = rs.iterToVec(rs.iterMap(rs.byteSliceChunks(data, 3), (a02: rs.ByteSlice): rs.ByteVec => { - return rs.byteSliceToByteVec(a02); + const runs: rs.Vec = rs.iterToVec(rs.iterMap(rs.byteSliceChunks(data, 3), (a0: rs.ByteSlice): rs.ByteVec => { + return rs.byteSliceToByteVec(a0); }), undefined, 12, true); - for (const r of drained$Vec_u8(rs.vecFrom(runs.items.map((a03: rs.ByteVec): rs.ByteVec => { - return rs.byteVecFrom(rs.byteVecView(a03)); + for (const r of drained$Vec_u8(rs.vecFrom(runs.items.map((a02: rs.ByteVec): rs.ByteVec => { + return rs.byteVecFrom(rs.byteVecView(a02)); })), bits).items) { rs.byteVecPush(out, r.length & 0xff); rs.iterExtendBytes(out, rs.byteVecIntoIter(r), undefined, true); } - for (const _56 of drained$Option_u16(rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_24: rs.u8): rs.u16 | undefined => { - const b4: rs.u8 = _24; - return thenSome$1gvhopg(rs.u8Rem(b4, 3) !== 0, b4 as rs.u16); + for (const _40 of drained$Option_u16(rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_24: rs.u8): rs.u16 | undefined => { + const b3: rs.u8 = _24; + return thenSome$1gvhopg(rs.u8Rem(b3, 3) !== 0, b3 as rs.u16); }), undefined, 4, true), bits).items) { - rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0smr0aj(_56, Uint8Array.of(255, 255), (a04: rs.u16): Uint8Array => { - return rs.u16ToBeBytes(a04); + rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0smr0aj(_40, Uint8Array.of(255, 255), (a03: rs.u16): Uint8Array => { + return rs.u16ToBeBytes(a03); })), undefined, true); } const nums2: rs.Vec = nums; - retained$u32_pointers_behind_refs_closure4(nums2, (n3: rs.Place): boolean => { + retained$u32_pointers_behind_refs_closure3(nums2, (n3: rs.Place): boolean => { n3.v = rs.u32Add(n3.v, 1); return rs.u32Rem(n3.v, 2) === 0; }); rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(nums2.items), (_26: rs.u32): rs.u8 => { return _26 & 0xff; }), undefined, true); - const words2: rs.Vec = words; - retained$String_pointers_behind_refs_closure6(words2, (w4: rs.String): boolean => { - rs.stringPush(w4, 33); - return rs.usizeRem(w4.len, 2) === 0; - }); - rs.iterExtendBytes(out, rs.iterFlatMap(rs.iterSlice(words2.items), (w5: rs.String): rs.Iter => { - return rs.strBytes(w5.v); - }, undefined, true), "next"); const runs2: rs.Vec = runs; - retained$Vec_u8_pointers_behind_refs_closure8(runs2, (r2: rs.ByteVec): boolean => { + retained$Vec_u8_pointers_behind_refs_closure5(runs2, (r2: rs.ByteVec): boolean => { rs.byteVecPush(r2, 7); return rs.u8Rem(r2.data[rs.idx(r2, 0)], 2) === 0; }); @@ -3391,7 +3328,7 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { const counted: rs.Vec> = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_27: rs.u8): rs.Arc => { return new rs.Arc(new Counted.Counted(_27, dropped)); }), undefined, 4, true); - retained$Rc_pointers_Counted_pointers_behind_refs_closure10(counted, (c: rs.Place>): boolean => { + retained$Rc_pointers_Counted_pointers_behind_refs_closure7(counted, (c: rs.Place>): boolean => { return rs.u8Rem(c.v.value.id, 2) === 0; }); rs.byteVecPush(out, dropped.v); @@ -3407,32 +3344,32 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { } return new rs.Weak(); }), undefined, 4, true); - for (const w2 of drained$sync_Weak_u32(rs.vecFrom(weaks.items.map((a05: rs.Weak): rs.Weak => { - return rs.weakClone(a05); + for (const w of drained$sync_Weak_u32(rs.vecFrom(weaks.items.map((a04: rs.Weak): rs.Weak => { + return rs.weakClone(a04); })), bits).items) { - rs.byteVecPush(out, option_mapOr$0emjeap(rs.weakUpgrade(w2), 255, (n5: rs.Arc): rs.u8 => { - const _019: rs.u8 = n5.value & 0xff; + rs.byteVecPush(out, option_mapOr$0emjeap(rs.weakUpgrade(w), 255, (n5: rs.Arc): rs.u8 => { + const _015: rs.u8 = n5.value & 0xff; n5[Symbol.dispose](); - return _019; + return _015; })); - w2[Symbol.dispose](); + w[Symbol.dispose](); } const weaks2: rs.Vec> = weaks; let i: rs.u8 = 0; - retained$sync_Weak_u32_pointers_behind_refs_closure14(weaks2, (w6: rs.Weak): boolean => { + retained$sync_Weak_u32_pointers_behind_refs_closure11(weaks2, (w3: rs.Weak): boolean => { i = rs.u8Add(i, 1); if (rs.u8Rem(i, 3) === 0) { - const _86: rs.Weak = new rs.Weak(arc); - w6[Symbol.dispose](); - Object.assign(w6, _86); + const _84: rs.Weak = new rs.Weak(arc); + w3[Symbol.dispose](); + Object.assign(w3, _84); } return rs.u8Rem(i, 2) === 0; }); - rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(weaks2.items), (w7: rs.Weak): rs.u8 => { - return option_mapOr$0emjeap(rs.weakUpgrade(w7), 255, (n6: rs.Arc): rs.u8 => { - const _022: rs.u8 = n6.value & 0xff; + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(weaks2.items), (w4: rs.Weak): rs.u8 => { + return option_mapOr$0emjeap(rs.weakUpgrade(w4), 255, (n6: rs.Arc): rs.u8 => { + const _018: rs.u8 = n6.value & 0xff; n6[Symbol.dispose](); - return _022; + return _018; }); }), undefined, true); rs.byteVecPush(out, arc.weak & 0xff); @@ -3444,11 +3381,11 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { if (rs.sliceFrom(nums3.items).length >= 2) { const first: rs.Place = rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 0)); rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, rs.usizeSub(nums3.items.length, 1))), first) & 0xff); - const _172: rs.usize = nums3.items.length; + const _148: rs.usize = nums3.items.length; let iterStart: rs.usize = 0; - const iterEnd: rs.usize = _172; - while ((_175 = iterStart++) < iterEnd) { - rs.byteVecPush(out, rs.ptrAdd(first, _175).v & 0xff); + const iterEnd: rs.usize = _148; + while ((_151 = iterStart++) < iterEnd) { + rs.byteVecPush(out, rs.ptrAdd(first, _151).v & 0xff); } rs.byteVecPush(out, rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v & 0xff); rs.ptrAdd(rs.arrayAsPtr(nums3.items, rs.idx(nums3.items, 1)), -1).v = 9; @@ -3456,30 +3393,30 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { } const v: rs.ByteVec = rs.byteSliceToByteVec(data); rs.byteVecPush(out, vecLen(v) & 0xff); - const _206: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); - refilled(v, _206, bits); + const _182: rs.ByteVec = rs.iterToByteVec(rs.iterRev(rs.iterSlice(rs.byteSliceView(data))), undefined, true); + refilled(v, _182, bits); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); let iterStart2: rs.u8 = 0; const iterEnd2: rs.u8 = 3; - while ((_215 = iterStart2++) < iterEnd2) { - const how: rs.u8 = _215; - const w3: rs.ByteVec = rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b7: rs.u8): rs.u8 => { - return b7 ^ how; + while ((_191 = iterStart2++) < iterEnd2) { + const how: rs.u8 = _191; + const w2: rs.ByteVec = rs.iterToByteVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (b6: rs.u8): rs.u8 => { + return b6 ^ how; }), undefined, true); - const _227: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); - rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w3, _227, how))), undefined, true); + const _203: rs.ByteVec = rs.byteVecOf(bits, rs.usizeAdd(how as rs.usize, 1)); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(replaced(v, w2, _203, how))), undefined, true); rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(v)), undefined, true); - rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(w3)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.byteVecView(w2)), undefined, true); rs.byteVecPush(out, 255); } - const iter7: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); + const iter6: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); while (true) { - const _242: [rs.usize, rs.u8] | rs.Done = iter7.next(); - if (_242 === rs.DONE) { + const _218: [rs.usize, rs.u8] | rs.Done = iter6.next(); + if (_218 === rs.DONE) { break; } - const i3: rs.usize = _242[0]; - const b: rs.u8 = _242[1]; + const i3: rs.usize = _218[0]; + const b: rs.u8 = _218[1]; rs.byteVecPush(out, behind(b as rs.u32, i3 & 0xff) & 0xff); rs.byteVecPush(out, cellBehind(then$01qpu7s(rs.u8Rem(b, 3) !== 0, (): rs.Cell => { return new rs.Cell(b as rs.u32); @@ -3602,20 +3539,6 @@ export function inlineMoved(r: rs.RefCell>, o: rs.OnceCell, q: rs.VecDeque): [rs.Vec, rs.VecDeque, rs.u32 | undefined] { - const v2: rs.Vec = v; - const oldV: rs.Vec = copy$Vec_u32(v2); - rs.vecSetLen(v2, 0, 4, (): rs.u32 => { - return (undefined as unknown) as rs.u32; - }); - return [oldV, copy$collections_VecDeque_u32(q), rs.dequePopFront(q)] as [rs.Vec, rs.VecDeque, rs.u32 | undefined]; -} - /** * A cell written whole through a pointer from the shared reference given, which may be * the cell `Cell::from_mut` makes of another place. @@ -3752,30 +3675,12 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { } rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(oldC.v & 0xff, x & 0xff)), undefined, true); } - const nums: rs.Vec = rs.iterToVec(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_23: rs.u8): rs.u32 => { - return _23 as rs.u32; - }), undefined, 4, true); - const _199: [rs.Vec, rs.VecDeque, rs.u32 | undefined] = shrunkMoved(rs.vecFrom(nums.items.slice()), rs.dequeFromVec(rs.iterToVec(rs.iterSlice(nums.items), undefined, 4, true), 4) as rs.VecDeque); - const oldV: rs.Vec = _199[0]; - const oldQ: rs.VecDeque = _199[1]; - const popped: rs.u32 | undefined = _199[2]; - rs.byteVecPush(out, oldV.items.length & 0xff); - rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(oldV.items), (_24: rs.u32): rs.u8 => { - return _24 & 0xff; - }), undefined, true); - rs.byteVecPush(out, option_mapOr$07pvkk0(popped, 255, (n3: rs.u32): rs.u8 => { - return n3 & 0xff; - })); - rs.byteVecPush(out, oldQ.n & 0xff); - rs.iterExtendBytes(out, rs.iterMap(rs.dequeIter(oldQ), (_27: rs.u32): rs.u8 => { - return _27 & 0xff; - }), undefined, true); const map: rs.HashMap = rs.hashWithCapacity(rs.usizeMul(data.length, 8), 117440512) as rs.HashMap; - const _232: rs.Iter<[rs.u32, rs.u32]> = rs.iterMap(rs.iterSlice(nums.items), (_29: rs.u32): [rs.u32, rs.u32] => { - const n5: rs.u32 = _29; - return [n5, n5] as [rs.u32, rs.u32]; + const _199: rs.Iter<[rs.u32, rs.u32]> = rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_23: rs.u8): [rs.u32, rs.u32] => { + const b4: rs.u8 = _23; + return [b4 as rs.u32, b4 as rs.u32] as [rs.u32, rs.u32]; }); - rs.hashExtend(map, _232, undefined, undefined, undefined, 117440512); + rs.hashExtend(map, _199, undefined, undefined, undefined, 117440512); mapRewritten(map); rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.hashLen(map) & 0xff, rs.hashCapacity(map) & 0xff)), undefined, true); for (const b2 of rs.byteSliceView(data)) { diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index e9021abe1..c3ddb6092 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -153,7 +153,7 @@ impl List { } } -impl List { +impl List> { fn each(&self, out: &mut Vec) { let Some(guard) = self.guard else { return; @@ -162,7 +162,7 @@ impl List { while at != guard { let (key, value) = unsafe { at.as_ref().entry.assume_init_ref() }; out.push(*key); - out.extend_from_slice(value.as_bytes()); + out.extend_from_slice(value); at = unsafe { at.as_ref().link.value.next }; } } @@ -227,23 +227,23 @@ impl Drop for List { /// Steps on a list of the operation and key each pair of bytes picks, putting what each /// finds and then the list in order. pub fn lists(data: &[u8], out: &mut Vec) { - let mut list = List::::new(); + let mut list = List::>::new(); let mut data = data; while let [op, key, rest @ ..] = data { data = rest; let (op, key) = (*op, *key % 8); match op % 4 { - 0 => list.push_back(key, String::from(["", "v", "vv"][usize::from(op % 3)])), + 0 => list.push_back(key, [&b""[..], b"v", b"vv"][usize::from(op % 3)].to_vec()), 1 => match list.remove(key) { Some((k, v)) => { out.push(k); - out.extend_from_slice(v.as_bytes()); + out.extend_from_slice(&v); } None => out.push(0xff), }, 2 => out.push(list.to_front(key) as u8), _ => match list.get_mut(key) { - Some(value) => value.push('w'), + Some(value) => value.push(b'w'), None => out.push(0xfe), }, } @@ -2548,10 +2548,6 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { let bits = data.first().copied().unwrap_or(0); let nums: Vec = data.iter().map(|&b| u32::from(b) * 3).collect(); out.extend(drained(nums.clone(), bits).iter().map(|&n| n as u8)); - let words: Vec = data.iter().map(|b| b.to_string()).collect(); - for w in drained(words.clone(), bits) { - out.extend(w.bytes()); - } let runs: Vec> = data.chunks(3).map(<[u8]>::to_vec).collect(); for r in drained(runs.clone(), bits) { out.push(r.len() as u8); @@ -2568,12 +2564,6 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { *n % 2 == 0 }); out.extend(nums.iter().map(|&n| n as u8)); - let mut words = words; - retained(&mut words, |w| { - w.push('!'); - w.len() % 2 == 0 - }); - out.extend(words.iter().flat_map(|w| w.bytes())); let mut runs = runs; retained(&mut runs, |r| { r.push(7); @@ -2764,22 +2754,6 @@ fn inline_moved(r: &RefCell>, o: &OnceCell>) -> (RefCell, - mut q: std::collections::VecDeque, -) -> (Vec, std::collections::VecDeque, Option) { - let mut v = mem::ManuallyDrop::new(v); - let old_v = unsafe { ptr::read(&*v) }; - unsafe { v.set_len(0) }; - let old_q = unsafe { ptr::read(&q) }; - let popped = q.pop_front(); - mem::forget(q); - (old_v, old_q, popped) -} - /// A cell written whole through a pointer from the shared reference given, which may be /// the cell `Cell::from_mut` makes of another place. #[inline(never)] @@ -2866,15 +2840,8 @@ pub fn moved_out(data: &[u8], out: &mut Vec) { } out.extend([old_c.get() as u8, x as u8]); } - let nums: Vec = data.iter().map(|&b| u32::from(b)).collect(); - let (old_v, old_q, popped) = shrunk_moved(nums.clone(), nums.iter().copied().collect()); - out.push(old_v.len() as u8); - out.extend(old_v.iter().map(|&n| n as u8)); - out.push(popped.map_or(0xff, |n| n as u8)); - out.push(old_q.len() as u8); - out.extend(old_q.iter().map(|&n| n as u8)); let mut map = std::collections::HashMap::::with_capacity(data.len() * 8); - map.extend(nums.iter().map(|&n| (n, n))); + map.extend(data.iter().map(|&b| (u32::from(b), u32::from(b)))); map_rewritten(&mut map); out.extend([map.len() as u8, map.capacity() as u8]); for &b in data { diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index f5c70022b..f96c6f6c3 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -6,10 +6,18 @@ fixtures/reject/src/alloc.rs:alloc_retyped+8:21: memory from `alloc` cast to poi fixtures/reject/src/alloc.rs:alloc_retyped+10:17: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:allocated+4:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:allocated+5:11: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as +fixtures/reject/src/alloc.rs:beside_reference+2:14: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:35: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:44: a slice of a length the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+4:23: a read moving out a `std::collections::VecDeque`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+10:11: a copy between pointers to `(std::string::String, u8)`, which JS keeps as an object that knows no array it is in +fixtures/reject/src/alloc.rs:collection_moved+11:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+14:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 6feb42e25..295079e33 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -10,10 +10,18 @@ fixtures/reject/src/alloc.rs:alloc_retyped+8:21: memory from `alloc` cast to poi fixtures/reject/src/alloc.rs:alloc_retyped+10:17: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:allocated+4:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:allocated+5:11: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as +fixtures/reject/src/alloc.rs:beside_reference+2:14: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:35: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:44: a slice of a length the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` to one to `pointers::Halves`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another +fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+4:23: a read moving out a `std::collections::VecDeque`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+10:11: a copy between pointers to `(std::string::String, u8)`, which JS keeps as an object that knows no array it is in +fixtures/reject/src/alloc.rs:collection_moved+11:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+14:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 62f740cd6..2e4e7de72 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -63,6 +63,39 @@ pub fn peeked_moved(cells: Vec>) -> u32 { copy.next().map_or(0, |cell| cell.get()) } +/// Collections whose JS object keeps its length with its storage, moved out through +/// pointers while the original lives on: by `ptr::read`, `ptr::copy` within a `Vec` as +/// smallvec's `retain` moves its elements, `MaybeUninit::assume_init_read` of a tuple +/// holding one, and `ManuallyDrop::take`. Rust's copy shares the storage and keeps a +/// length of its own, which no JS copy does. +pub fn collection_moved(v: Vec, mut q: std::collections::VecDeque, s: String) -> usize { + let mut v = std::mem::ManuallyDrop::new(v); + let old_v = unsafe { std::ptr::read(&*v) }; + unsafe { v.set_len(0) }; + let old_q = unsafe { std::ptr::read(&q) }; + q.pop_front(); + std::mem::forget(q); + let mut words = vec![s.clone(), s.clone()]; + words[1].push('!'); + unsafe { + std::ptr::copy(words.as_ptr().add(1), words.as_mut_ptr(), 1); + words.set_len(1); + } + let pair = std::mem::MaybeUninit::new((s, 0u8)); + let old_s = unsafe { pair.assume_init_read() }; + let mut taken = std::mem::ManuallyDrop::new(vec![1u32]); + let old_t = unsafe { std::mem::ManuallyDrop::take(&mut taken) }; + old_v.len() + old_q.len() + words[0].len() + old_s.0.len() + old_t.len() +} + +/// A pointer from a reference the body is given, which JS keeps in no array, read through +/// past it by a constant wrapping offset and one past it, and viewed as a slice of a +/// length the body doesn't fix: each reaches the neighbour Tree Borrows allows it to. +pub fn beside_reference(r: &u32, n: usize) -> u32 { + let p = std::ptr::from_ref(r); + unsafe { *p.wrapping_add(1) + *p.add(1) + std::slice::from_raw_parts(p, n).iter().sum::() } +} + /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 51b96c113..aa68b8a22 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -3,7 +3,9 @@ roots = [ "alloc::addressed", "alloc::alloc_retyped", "alloc::allocated", + "alloc::beside_reference", "alloc::bytes_as_values", + "alloc::collection_moved", "alloc::infallible_cell", "alloc::lazy_moved", "alloc::past_reference", diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 30421f638..616514c8a 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -4,7 +4,7 @@ //! JS shares an object where Rust copies its value, so a `Copy` value that is a JS object //! the crate's code writes into in place, or compares pointers to, is copied where Rust //! copies it, as is each such object a read through a pointer moves out, a library -//! object such as a `Vec` included, one object deep. A function made for its type copies +//! object such as a `RefCell` included, one object deep. A function made for its type copies //! what it holds by value and shares what it holds by reference, as Rust does. A value of //! a type nothing writes into or compares pointers to never changes once made, so sharing //! it is a copy no one can tell apart. @@ -34,12 +34,9 @@ use crate::ts::{self, Expr, Key, Param, Stmt, StmtKind}; struct Inline<'tcx> { /// Each field of its JS object holding a value inline, and the type held there. fields: Vec<(&'static str, Ty<'tcx>)>, - /// Each field holding the JS array of its storage, whose length or slots the runtime - /// changes where Rust changes only the object's own length, as a `Vec`'s `set_len` and - /// a `VecDeque`'s `pop_front` do: copied, sharing its items. - storage: Vec<&'static str>, - /// The types it holds where no copy reaches, a hash map's hasher. - unreachable: Vec>, + /// The types it holds inline that its copy shares, a hash map's hasher, which a move + /// out of it through a pointer counts as it counts the collection. + shared: Vec>, } impl<'a, 'tcx> Translator<'a, 'tcx> { @@ -84,7 +81,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { .fields .into_iter() .map(|(_, t)| t) - .chain(inline.unreachable) + .chain(inline.shared) .any(|t| self.changes(t)), // Any type it is over may be held inline, which its copy refuses. None => args.types().any(|t| self.changes(t)), @@ -105,21 +102,16 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// What a library object `ty` holds inline rather than on the heap, which a copy of the /// object one object deep shares: each field of its JS object holding such a value and /// the type held there, a cell's, a lock's, a range's ends, or a one-time cell's, which - /// holds nothing until set; the JS arrays holding a collection's storage whose length - /// is its own; and the types it holds where no copy reaches, a hash map's hasher. A - /// collection's items, and what a `Weak` points at, are on the heap, which Rust's read - /// shares too. `None` for any other library object, such as a `LazyCell` or a - /// `Peekable`, whose inline state only its runtime class knows, so a read that would + /// holds nothing until set; and the types it holds where no copy reaches, a hash map's + /// hasher. A collection's items, and what a `Weak` points at, are on the heap, which + /// Rust's read shares too. `None` for any other library object, such as a `LazyCell` or + /// a `Peekable`, whose inline state only its runtime class knows, so a read that would /// copy one is refused. fn inline_held(&self, ty: Ty<'tcx>) -> Option> { let ty::Adt(adt, args) = ty.kind() else { return None; }; let at = |i: usize| args.type_at(i); - let storage = |field: &'static str| Inline { - storage: vec![field], - ..Inline::default() - }; let fields = match Std::of(self.tcx, adt.did()) { Some(Std::Cell) => vec![("v", at(0))], Some(Std::RefCell | Std::Mutex) => vec![("value", at(0))], @@ -127,11 +119,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(Std::Range) => vec![("start", at(0)), ("end", at(0))], Some(Std::RangeInclusive) => vec![("lo", at(0)), ("hi", at(0))], Some(Std::RangeFrom) => vec![("start", at(0))], - // A `Vec`'s length is apart from its bytes. - Some(Std::Vec | Std::BinaryHeap) if !super::byte(at(0)) => return Some(storage("items")), - Some(Std::VecDeque) => return Some(storage("b")), Some( Std::Vec + | Std::VecDeque | Std::String | Std::LinkedList | Std::BinaryHeap @@ -143,13 +133,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { ) => Vec::new(), Some(Std::HashMap) => { return Some(Inline { - unreachable: vec![at(2)], + shared: vec![at(2)], ..Inline::default() }); } Some(Std::HashSet) => { return Some(Inline { - unreachable: vec![at(1)], + shared: vec![at(1)], ..Inline::default() }); } @@ -161,6 +151,36 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }) } + /// The collection held by value in a `ty` that a move of one out of where it is kept, as + /// `ptr::read` makes, would copy, if any: a `Vec` of other than bytes, a `VecDeque`, a + /// `String`, a map, a set, or a list, whose JS object keeps its length with its storage, + /// so no copy splits from the original as Rust's read does, sharing the storage while + /// each keeps a length of its own. A safe move leaves nothing pointing into the + /// original, which a copy one object deep is exact for. + pub fn unsplit(&self, ty: Ty<'tcx>) -> Option> { + by_value(self.tcx, self.typing_env(), ty).find(|&part| { + let ty::Adt(adt, args) = part.kind() else { + return false; + }; + let joined = match Std::of(self.tcx, adt.did()) { + Some(Std::Vec) => !super::byte(args.type_at(0)), + Some( + Std::VecDeque + | Std::BinaryHeap + | Std::String + | Std::LinkedList + | Std::BTreeMap + | Std::BTreeSet + | Std::HashMap + | Std::HashSet + | Std::HashTable, + ) => true, + _ => false, + }; + joined && self.changes(part) + }) + } + /// Whether the runs before compared pointers to a `ty`, or to a type it holds by value, /// so each copy of one is at an address of its own, as in Rust, wherever it is read. pub fn held_addressed(&self, ty: Ty<'tcx>) -> bool { @@ -441,12 +461,6 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { ); return ret(v()); }; - if inline.unreachable.into_iter().any(|t| self.changes(t)) { - self.err( - span, - format!("a read moving out a `{ty}`, which holds inline a value JS can't copy apart"), - ); - } // A cell of its own, as `Cell::from_mut`'s place reads and writes another's. if std == Std::Cell { let held = self.snapshot(v().member("v"), args.type_at(0), span); @@ -454,15 +468,11 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } let fields: Vec<_> = inline.fields.into_iter().filter(|&(_, t)| self.changes(t)).collect(); let copy = Expr::rs("objectCopy", vec![v()]); - if fields.is_empty() && inline.storage.is_empty() { + if fields.is_empty() { return ret(copy); } let c = || Expr::ident("c"); let mut body = vec![StmtKind::decl(false, "c", None, Some(copy)).at(None)]; - for field in inline.storage { - let items = c().member(field).member("slice").call(Vec::new()); - body.push(StmtKind::Expr(c().member(field).assign(items)).at(None)); - } for (field, t) in fields { let read = self.snapshot(c().member(field), t, span); body.push(StmtKind::Expr(c().member(field).assign(read)).at(None)); diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 4de0a3b80..4b6fcd2bf 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -64,8 +64,10 @@ struct Origin<'tcx> { impl<'tcx> Fx<'_, '_, '_, 'tcx> { /// Why the call of std's `def_id` would fail at run time where Rust's is defined, if the /// body proves it would: `offset_from` between pointers JS keeps in different arrays, or - /// a pointer, slice, copy, or write reaching out of an array of one or a copied byte, or - /// moved by an offset or a count the body doesn't fix from an array of one. + /// a pointer, slice, copy, or write reaching out of an array of one or a copied byte. + /// From an array of one, every offset, count, and length must be one the body fixes and + /// that stays within it, a pointer still at its element: past it, Rust may reach the + /// neighbour the reference was borrowed beside, which no body here shows. pub(super) fn stray( &self, def_id: rustc_hir::def_id::DefId, @@ -102,6 +104,12 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let Some(index) = o.index else { return unfixed(o, &format!("{what} moved by an offset")); }; + // A pointer from an array of one stays at its element: one past it is Rust's + // too, but a read through it, which reaches the neighbour, is where no check is. + let n = match o.array { + Array::Lone(_) => n.or(Some(1)), + _ => n, + }; let past = o.array.len().is_some_and(|len| index + n.unwrap_or(0) > len); (index < 0 || past).then(|| { format!( @@ -138,23 +146,25 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let o = self.moved(self.origin(arg(0)?)?, name.as_str(), arg(1)?); out(o, None, "a pointer") } - // A wrapping offset may leave the array while nothing reads through it. + // A wrapping offset may leave any other array while nothing reads through it. ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { let o = self.origin(arg(0)?)?; - match self.const_int(arg(1)?) { - Some(_) => None, - None => unfixed(o, "a pointer moved by an offset"), - } + let lone = matches!(o.array, Array::Lone(_)); + lone.then(|| out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer"))? } ("raw", "from_raw_parts" | "from_raw_parts_mut") | ("ptr", "slice_from_raw_parts" | "slice_from_raw_parts_mut") | ("NonNull", "slice_from_raw_parts") => { let o = self.origin(arg(0)?)?; - // A raw pointer to a slice reads nothing until a read through it. - if owner != "raw" { + // A raw pointer to a slice of any other array reads nothing until a read + // through it. + if owner != "raw" && !matches!(o.array, Array::Lone(_)) { return None; } - out(o, Some(self.const_int(arg(1)?)?), "a slice") + match self.const_int(arg(1)?) { + Some(n) => out(o, Some(n), "a slice"), + None => unfixed(o, "a slice of a length"), + } } ( "ptr", @@ -347,10 +357,10 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { }) } - /// `o` moved by the offset `op` as `add`, `sub`, or `offset` moves it. + /// `o` moved by the offset `op` as `add`, `sub`, or `offset`, or a wrapping one, moves it. fn moved(&self, o: Origin<'tcx>, name: &str, op: &Operand<'tcx>) -> Origin<'tcx> { let n = self.const_int(op).map(|n| match name { - "sub" => -n, + "sub" | "wrapping_sub" => -n, _ => n, }); Origin { diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index a48e9f130..69046dd1a 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -846,7 +846,10 @@ pub fn call<'tcx>( } } // A shared reference to a value JS copies is the value. - ("MaybeUninit", "assume_init_read") => b.t.snapshot(arg(), pointee(), span), + ("MaybeUninit", "assume_init_read") => match unsplit(b, pointee(), span) { + Some(refused) => refused, + None => b.t.snapshot(arg(), pointee(), span), + }, ("MaybeUninit", "assume_init_drop") | ("ManuallyDrop", "drop") => { let p = arg(); let value = match kind(b, pointee()) { @@ -858,6 +861,9 @@ pub fn call<'tcx>( // What it holds, which it leaves for nothing to read or drop again. ("ManuallyDrop", "take") => { let p = arg(); + if let Some(refused) = unsplit(b, pointee(), span) { + return Ok(Some(refused)); + } let value = match kind(b, pointee()) { Some(Pointee::Place) => p.member("v"), _ => p, @@ -945,6 +951,8 @@ pub fn call<'tcx>( Expr::undefined() } else if let Some(why) = unmodeled(b, pointee(), "a copy between pointers to") { b.err(span, why) + } else if let Some(refused) = unsplit(b, pointee(), span) { + refused } else { let mut args = vec![src, dst, n]; args.extend(b.t.snapshot_fn(pointee(), span)); @@ -1175,8 +1183,27 @@ pub fn raw_pointee<'tcx>(tcx: ty::TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option } } +/// The refusal of a move of a `ty` out of where it is kept, as `ptr::read` makes, that +/// would copy a collection whose JS storage can't split from the original as Rust's does. +fn unsplit<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Option { + let held = b.t.unsplit(ty)?; + let what = match held == ty { + true => String::new(), + false => format!(", which holds a `{held}`"), + }; + Some(b.err( + span, + format!( + "a read moving out a `{ty}`{what}, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does" + ), + )) +} + /// The value `p`, a pointer to `ty`, points at, as a value of its own. fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> Expr { + if let Some(refused) = unsplit(b, ty, span) { + return refused; + } let value = match b.t.pointee(ty) { // The value a pointer made here of a shared reference holds. Pointee::Place => match p.unlocated() { From 3d5165c718802c231d73864aaaa9bc6d2ee3f177 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:21:39 -0800 Subject: [PATCH 26/45] test(ts): refuse a collection's ptr::copy within a Vec Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 6 +++--- ts/fixtures/reject/expected.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index f96c6f6c3..079e84442 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -15,9 +15,9 @@ fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+4:23: a read moving out a `std::collections::VecDeque`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does -fixtures/reject/src/alloc.rs:collection_moved+10:11: a copy between pointers to `(std::string::String, u8)`, which JS keeps as an object that knows no array it is in -fixtures/reject/src/alloc.rs:collection_moved+11:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does -fixtures/reject/src/alloc.rs:collection_moved+14:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 295079e33..cf3a07a2d 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -19,9 +19,9 @@ fixtures/reject/src/alloc.rs:bytes_as_values+5:16: a cast from a pointer to `u8` fixtures/reject/src/alloc.rs:bytes_as_values+7:15: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+4:23: a read moving out a `std::collections::VecDeque`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does -fixtures/reject/src/alloc.rs:collection_moved+10:11: a copy between pointers to `(std::string::String, u8)`, which JS keeps as an object that knows no array it is in -fixtures/reject/src/alloc.rs:collection_moved+11:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does -fixtures/reject/src/alloc.rs:collection_moved+14:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust From 44bfc2a1a9f2f618e97e9e81cc54aeeb7353aafc Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:34:23 -0800 Subject: [PATCH 27/45] fix(ts): check reads one past a lone place, not the pointer A pointer one past an array of one is valid Rust, which main's elements fixture moves through and back; a read or write there, by a call or a deref, is what reaches the neighbour, so that is refused, and a wrapping offset from one is traced so the read through it is checked. Fixtures whose pointer moves of `String`s are now refused move `Vec`s. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- ts/fixtures/lang/conversions/expected/lib.ts | 4 +- ts/fixtures/lang/elements/expected/lib.ts | 2 +- ts/fixtures/lang/elements/expected/spare.ts | 114 ++++++++++++------- ts/fixtures/lang/elements/src/spare.rs | 19 +++- ts/fixtures/reject/census.txt | 4 +- ts/fixtures/reject/expected.txt | 4 +- ts/src/lower/mir/origin.rs | 111 ++++++++++++------ ts/src/lower/mir/place.rs | 4 +- 9 files changed, 176 insertions(+), 88 deletions(-) diff --git a/ts/README.md b/ts/README.md index 2d0ee4e9f..2e63bbf4c 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: the `Vec` it owns, or an `rs.Borrowed` holding the slice it borrows, `rs.Cow>` 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`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, and view; one to a value in no array is element 0 of an array of one, as in Rust, but a byte variable whose `&raw` the body takes, or that of a reference to it made there, as `&mut b as *mut u8` does, is element 0 of a `Uint8Array` of its own, which its raw, mutable, and shared slices view as they do any other bytes; JS keeps an array of one, and a shared subslice of a byte in no array, which JS copies, apart from what holds it in Rust, so a pointer, slice, copy, or write of bytes the body shows reaching past either, a pointer it shows moved from a value in no array off it, or by an offset, or a copy, write, or slice of a count or length, it doesn't fix, and `offset_from` between pointers the body shows JS keeps in different arrays, are refused, and through a pointer from elsewhere any of these, a raw or mutable slice of a byte in no array, such as a field's, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `RefCell` or a `Vec` one object deep, with what a cell, lock, or range holds inline, and one with interior mutability, such as a `Cell`, always; a collection whose JS object keeps its length with its storage, such as a `Vec` of other than bytes, a `VecDeque`, a `String`, a map, or a set, which no copy splits from the original as Rust's read does, and any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, are refused where the read would copy them | +| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, and view; one to a value in no array is element 0 of an array of one, as in Rust, but a byte variable whose `&raw` the body takes, or that of a reference to it made there, as `&mut b as *mut u8` does, is element 0 of a `Uint8Array` of its own, which its raw, mutable, and shared slices view as they do any other bytes; JS keeps an array of one, and a shared subslice of a byte in no array, which JS copies, apart from what holds it in Rust, so a pointer, slice, copy, or write of bytes the body shows reaching past either, a read or write it shows one past a value in no array, a pointer it shows moved from one by an offset, wrapping or not, or a copy, write, or slice of a count or length, it doesn't fix, and `offset_from` between pointers the body shows JS keeps in different arrays, are refused, and through a pointer from elsewhere any of these, a raw or mutable slice of a byte in no array, such as a field's, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `RefCell` or a `Vec` one object deep, with what a cell, lock, or range holds inline, and one with interior mutability, such as a `Cell`, always; a collection whose JS object keeps its length with its storage, such as a `Vec` of other than bytes, a `VecDeque`, a `String`, a map, or a set, which no copy splits from the original as Rust's read does, and any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, are refused where the read would copy them | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined`, as `Box::new_uninit` and each of `Box::new_uninit_slice`'s is; a slice's `write_clone_of_slice` clones into its slots without dropping what they held. `Box::new_zeroed`'s holds the value of zero bytes in each part they are one of, such as a number, `false`, `None`, a null pointer, or an enum's variant of a tag or niche of zero, and `undefined` in each part they are none of, such as a reference, for a write to fill before a read | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | diff --git a/ts/fixtures/lang/conversions/expected/lib.ts b/ts/fixtures/lang/conversions/expected/lib.ts index c52db795d..a448b3248 100644 --- a/ts/fixtures/lang/conversions/expected/lib.ts +++ b/ts/fixtures/lang/conversions/expected/lib.ts @@ -10,9 +10,7 @@ export function copy$String(v: rs.String): rs.String { } export function copy$Vec_u32(v: rs.Vec): rs.Vec { - const c = rs.objectCopy(v); - c.items = c.items.slice(); - return c; + return rs.objectCopy(v); } export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { diff --git a/ts/fixtures/lang/elements/expected/lib.ts b/ts/fixtures/lang/elements/expected/lib.ts index 408c432b1..fb0fc40fd 100644 --- a/ts/fixtures/lang/elements/expected/lib.ts +++ b/ts/fixtures/lang/elements/expected/lib.ts @@ -9,7 +9,7 @@ export function copy$Option_tuple2_u8_u8(v: [rs.u8, rs.u8] | undefined): [rs.u8, return v === undefined ? v : copy$tuple2_u8_u8(v); } -export function copy$String(v: rs.String): rs.String { +export function copy$Vec_u8(v: rs.ByteVec): rs.ByteVec { return rs.objectCopy(v); } diff --git a/ts/fixtures/lang/elements/expected/spare.ts b/ts/fixtures/lang/elements/expected/spare.ts index 103efd939..84498f4b6 100644 --- a/ts/fixtures/lang/elements/expected/spare.ts +++ b/ts/fixtures/lang/elements/expected/spare.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$String, copy$mem_MaybeUninit_tuple2_u8_u32, copy$tuple2_u8_u32 } from "./lib.ts"; +import { copy$Vec_u8, copy$mem_MaybeUninit_tuple2_u8_u32, copy$tuple2_u8_u32 } from "./lib.ts"; import { thenSome$1gvhopg } from "./core/bool.ts"; import { mapOr$17z0l8m as option_mapOr$17z0l8m } from "./core/option.ts"; import * as Noted from "./spare.Noted.ts"; @@ -25,6 +25,15 @@ export function put$spare_Item_String(self: rs.String, out: rs.ByteVec): void { rs.byteVecPush(out, 255); } +export function of$spare_Item_Vec_u8(n: rs.u8): rs.ByteVec { + return rs.byteVecOf(n, rs.u8Rem(n, 3) as rs.usize); +} + +export function put$spare_Item_Vec_u8(self: rs.ByteVec, out: rs.ByteVec): void { + rs.byteVecExtendFromSlice(out, rs.byteVecSlice(self)); + rs.byteVecPush(out, 255); +} + export function of$spare_Item_Option_u16(n: rs.u8): rs.u16 | undefined { return thenSome$1gvhopg(rs.u8Rem(n, 3) !== 0, ((n as rs.u16) << 4) & 0xffff); } @@ -92,6 +101,29 @@ export function state$String(v: rs.Vec, out: rs.ByteVec): void { } } +/** The length, capacity, and room of `v`, how many `Noted`s dropped, and its items. */ +export function state$Vec_u8(v: rs.Vec, out: rs.ByteVec): void { + const iter: rs.Iter = rs.iterSlice([v.items.length, rs.vecCapacity(v, 12), rs.vecSpare(v, 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; + }).length]); + while (true) { + const _13: rs.usize | rs.Done = iter.next(); + if (_13 === rs.DONE) { + break; + } + rs.byteVecExtendFromSlice(out, rs.byteSliceFrom(rs.u32ToBeBytes(_13 as rs.u32).reverse())); + } + rs.byteVecPush(out, rs.atomicLoad(DROPS, 0)); + const iter2: rs.Iter = rs.iterSlice(v.items); + while (true) { + const _31: rs.ByteVec | rs.Done = iter2.next(); + if (_31 === rs.DONE) { + break; + } + put$spare_Item_Vec_u8(_31, out); + } +} + /** The length, capacity, and room of `v`, how many `Noted`s dropped, and its items. */ export function state$spare_Noted(v: rs.Vec, out: rs.ByteVec): void { const iter: rs.Iter = rs.iterSlice([v.items.length, rs.vecCapacity(v, 1), rs.vecSpare(v, 1, (): Noted.Noted => { @@ -324,12 +356,12 @@ export function pointers$Option_u16(data: rs.ByteSlice, out: rs.ByteVec): void { * `Drain` does; a filter that shortens it to nothing and writes back what it keeps, as * std's `retain` does; grown, cut, and refilled. */ -export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { +export function pointers$Vec_u8(data: rs.ByteSlice, out: rs.ByteVec): void { let _43!: rs.usize; let _165!: rs.usize; let _194!: rs.usize; const ops: Ops.Ops = { _0Data: data.data, _0Start: data.start, _0Len: data.length }; - let v: rs.Vec = rs.vecWithCapacity(rs.u8Rem(Ops.byte(ops), 12) as rs.usize, 12); + let v: rs.Vec = rs.vecWithCapacity(rs.u8Rem(Ops.byte(ops), 12) as rs.usize, 12); while (!(ops._0Len === 0)) { const _13: [rs.u8, rs.u8] = [Ops.byte(ops), Ops.byte(ops)]; const op: rs.u8 = _13[0]; @@ -340,59 +372,59 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { b2: { const _35: rs.u8 = rs.u8Rem(op, 8); if (_35 === 0) { - const end: rs.Place = rs.ptrAdd(rs.vecAsPtr(v, 12), len); + const end: rs.Place = rs.ptrAdd(rs.vecAsPtr(v, 12), len); let iterStart: rs.usize = 0; const iterEnd: rs.usize = k; while ((_43 = iterStart++) < iterEnd) { const i: rs.usize = _43; - rs.ptrAdd(end, i).v = of$spare_Item_String(rs.u8WrappingAdd(n, i & 0xff)); + rs.ptrAdd(end, i).v = of$spare_Item_Vec_u8(rs.u8WrappingAdd(n, i & 0xff)); } if (k >= 2) { - const _54: rs.Slice = new rs.RawSlice(end, k).array(2); + const _54: rs.Slice = new rs.RawSlice(end, k).array(2); rs.sliceSwap(_54, 0, 1); - put$spare_Item_String(_54.data[_54.start], out); + put$spare_Item_Vec_u8(_54.data[_54.start], out); } - const moved: rs.Vec = rs.replaceIn(v, new rs.Vec()); + const moved: rs.Vec = rs.replaceIn(v, new rs.Vec()); for (const _71 of rs.sliceItems(rs.ptrSlice(rs.ptrAdd(end, -len), rs.usizeAdd(len, k)))) { - put$spare_Item_String(_71, out); + put$spare_Item_Vec_u8(_71, out); } - rs.vecSetLen(moved, rs.usizeAdd(len, k), 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.vecSetLen(moved, rs.usizeAdd(len, k), 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); v = moved; break b2; } if (_35 === 1) { - rs.ptrCopy(rs.vecAsPtr(rs.iterToVec(rs.iterMap(new rs.Range(0, k), (i5: rs.usize): rs.String => { - return of$spare_Item_String(n ^ (i5 & 0xff)); - }), undefined, 12, true), 12), rs.ptrAdd(rs.vecAsPtr(v, 12), len), k, copy$String); - rs.vecSetLen(v, rs.usizeAdd(len, k), 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.ptrCopy(rs.vecAsPtr(rs.iterToVec(rs.iterMap(new rs.Range(0, k), (i5: rs.usize): rs.ByteVec => { + return of$spare_Item_Vec_u8(n ^ (i5 & 0xff)); + }), undefined, 12, true), 12), rs.ptrAdd(rs.vecAsPtr(v, 12), len), k, copy$Vec_u8); + rs.vecSetLen(v, rs.usizeAdd(len, k), 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); break b2; } if (_35 === 2) { - const iter3: rs.Iter<[rs.usize, rs.Place]> = rs.iterRev(rs.iterEnumerate(rs.iterSliceMut(rs.ptrSlice(rs.ptrAdd(rs.vecAsPtr(v, 12), len), k)))); + const iter3: rs.Iter<[rs.usize, rs.Place]> = rs.iterRev(rs.iterEnumerate(rs.iterSliceMut(rs.ptrSlice(rs.ptrAdd(rs.vecAsPtr(v, 12), len), k)))); while (true) { - const _108: [rs.usize, rs.Place] | rs.Done = iter3.next(); + const _108: [rs.usize, rs.Place] | rs.Done = iter3.next(); if (_108 === rs.DONE) { break; } - _108[1].v = of$spare_Item_String(rs.u8WrappingSub(n, _108[0] & 0xff)); + _108[1].v = of$spare_Item_Vec_u8(rs.u8WrappingSub(n, _108[0] & 0xff)); } - rs.vecSetLen(v, rs.usizeAdd(len, k), 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.vecSetLen(v, rs.usizeAdd(len, k), 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); break b2; } if (_35 === 3) { rs.vecReserve(v, 1, 12); const at: rs.usize = rs.usizeRem(n as rs.usize, rs.usizeAdd(len, 1)); - const p: rs.Place = rs.ptrAdd(rs.vecAsPtr(v, 12), at); - rs.ptrCopy(p, rs.ptrAdd(p, 1), rs.usizeSub(len, at), copy$String); - p.v = of$spare_Item_String(n); - rs.vecSetLen(v, rs.usizeAdd(len, 1), 12, (): rs.String => { - return (undefined as unknown) as rs.String; + const p: rs.Place = rs.ptrAdd(rs.vecAsPtr(v, 12), at); + rs.ptrCopy(p, rs.ptrAdd(p, 1), rs.usizeSub(len, at), copy$Vec_u8); + p.v = of$spare_Item_Vec_u8(n); + rs.vecSetLen(v, rs.usizeAdd(len, 1), 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); break b2; } @@ -400,40 +432,40 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { if (len > 0) { const at2: rs.usize = rs.usizeRem(n as rs.usize, len); const end2: rs.usize = rs.usizeAdd(at2, rs.usizeRem(n >> 4 as rs.usize, rs.usizeAdd(rs.usizeSub(len, at2), 1))); - rs.vecSetLen(v, at2, 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.vecSetLen(v, at2, 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); - const p2: rs.Place = rs.vecAsPtr(v, 12); + const p2: rs.Place = rs.vecAsPtr(v, 12); let iterStart2: rs.usize = at2; const iterEnd2: rs.usize = end2; while ((_165 = iterStart2++) < iterEnd2) { - put$spare_Item_String(copy$String(rs.ptrAdd(p2, _165).v), out); + put$spare_Item_Vec_u8(copy$Vec_u8(rs.ptrAdd(p2, _165).v), out); } - rs.ptrCopy(rs.ptrAdd(p2, end2), rs.ptrAdd(p2, at2), rs.usizeSub(len, end2), copy$String); - rs.vecSetLen(v, rs.usizeSub(len, rs.usizeSub(end2, at2)), 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.ptrCopy(rs.ptrAdd(p2, end2), rs.ptrAdd(p2, at2), rs.usizeSub(len, end2), copy$Vec_u8); + rs.vecSetLen(v, rs.usizeSub(len, rs.usizeSub(end2, at2)), 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); break b2; } } else { if (_35 === 5) { - rs.vecSetLen(v, 0, 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.vecSetLen(v, 0, 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); - const p3: rs.Place = rs.vecAsPtr(v, 12); + const p3: rs.Place = rs.vecAsPtr(v, 12); let kept: rs.usize = 0; let iterStart3: rs.usize = 0; const iterEnd3: rs.usize = len; while ((_194 = iterStart3++) < iterEnd3) { const i4: rs.usize = _194; - const x: rs.String = copy$String(rs.ptrAdd(p3, i4).v); + const x: rs.ByteVec = copy$Vec_u8(rs.ptrAdd(p3, i4).v); if (rs.usizeRem(rs.usizeAdd(i4, n as rs.usize), 3) !== 0) { rs.ptrAdd(p3, kept).v = x; kept = rs.usizeAdd(kept, 1); } } - rs.vecSetLen(v, kept, 12, (): rs.String => { - return (undefined as unknown) as rs.String; + rs.vecSetLen(v, kept, 12, (): rs.ByteVec => { + return (undefined as unknown) as rs.ByteVec; }); break b2; } @@ -444,7 +476,7 @@ export function pointers$String(data: rs.ByteSlice, out: rs.ByteVec): void { } rs.vecTruncate(v, rs.usizeRem(n as rs.usize, rs.usizeAdd(len, 1))); } - state$String(v, out); + state$Vec_u8(v, out); } } @@ -1161,7 +1193,7 @@ export function spares(kind: rs.u8, data: rs.ByteSlice, out: rs.ByteVec): void { if (_8 === 0) { pointers$u32(data, out); } else if (_8 === 1) { - pointers$String(data, out); + pointers$Vec_u8(data, out); } else if (_8 === 2) { pointers$Option_u16(data, out); } else if (_8 === 3) { diff --git a/ts/fixtures/lang/elements/src/spare.rs b/ts/fixtures/lang/elements/src/spare.rs index 3faaf2765..4408ccd8c 100644 --- a/ts/fixtures/lang/elements/src/spare.rs +++ b/ts/fixtures/lang/elements/src/spare.rs @@ -3,8 +3,8 @@ //! through the slots `spare_capacity_mut` lends, then taken in by `set_len`; shortened by //! `set_len` and read back past its length, as std's `Drain` and `retain` read theirs, then //! lengthened over what it gave up; and slots never written, copied as they are. Of numbers, -//! `String`s, `Option`s, values that count their drops, and values JS keeps as objects, by -//! a generic function too. +//! bytes, `String`s, `Option`s, values that count their drops, and values JS keeps as +//! objects, by a generic function too. use std::mem::MaybeUninit; use std::ptr; @@ -38,6 +38,18 @@ impl Item for String { } } +/// Bytes, whose length JS keeps apart from them, as Rust does, so a pointer moves one exactly. +impl Item for Vec { + fn of(n: u8) -> Self { + vec![n; usize::from(n % 3)] + } + + fn put(&self, out: &mut Vec) { + out.extend_from_slice(self); + out.push(0xff); + } +} + impl Item for Option { fn of(n: u8) -> Self { (n % 3 != 0).then_some(u16::from(n) << 4) @@ -301,7 +313,8 @@ pub fn spares(kind: u8, data: &[u8], out: &mut Vec) { DROPS.store(0, Ordering::Relaxed); match kind % 10 { 0 => pointers::(data, out), - 1 => pointers::(data, out), + // A `String`'s JS object keeps its length with its text, so a pointer can't move one. + 1 => pointers::>(data, out), 2 => pointers::>(data, out), 3 => slots::(data, out), 4 => slots::(data, out), diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 079e84442..0a012ab15 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -6,8 +6,8 @@ fixtures/reject/src/alloc.rs:alloc_retyped+8:21: memory from `alloc` cast to poi fixtures/reject/src/alloc.rs:alloc_retyped+10:17: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:allocated+4:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:allocated+5:11: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as -fixtures/reject/src/alloc.rs:beside_reference+2:14: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust -fixtures/reject/src/alloc.rs:beside_reference+2:35: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:11: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:32: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:beside_reference+2:44: a slice of a length the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index cf3a07a2d..e70a4c0a2 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -10,8 +10,8 @@ fixtures/reject/src/alloc.rs:alloc_retyped+8:21: memory from `alloc` cast to poi fixtures/reject/src/alloc.rs:alloc_retyped+10:17: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:allocated+4:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:allocated+5:11: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as -fixtures/reject/src/alloc.rs:beside_reference+2:14: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust -fixtures/reject/src/alloc.rs:beside_reference+2:35: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:11: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:beside_reference+2:32: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:beside_reference+2:44: a slice of a length the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:bytes_as_values+3:3: a byte of memory from `alloc`, which JS keeps as the values a cast of its pointer types it as fixtures/reject/src/alloc.rs:bytes_as_values+4:15: a cast from a pointer to `u8` to one to `u32`, which reads the bytes of one type as another diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 4b6fcd2bf..61ad7f9c2 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -65,9 +65,9 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { /// Why the call of std's `def_id` would fail at run time where Rust's is defined, if the /// body proves it would: `offset_from` between pointers JS keeps in different arrays, or /// a pointer, slice, copy, or write reaching out of an array of one or a copied byte. - /// From an array of one, every offset, count, and length must be one the body fixes and - /// that stays within it, a pointer still at its element: past it, Rust may reach the - /// neighbour the reference was borrowed beside, which no body here shows. + /// From an array of one, every offset, count, length, and access, wrapping or not, must + /// be one the body fixes and proves stays within it: past it, Rust may reach the + /// neighbour the reference was borrowed beside, which JS keeps apart. pub(super) fn stray( &self, def_id: rustc_hir::def_id::DefId, @@ -91,33 +91,7 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { { return Some(super::alloc::BYTES.into()); } - // An offset or a count the body doesn't fix may reach out of an array of one. - let unfixed = |o: Origin<'tcx>, what: &str| { - matches!(o.array, Array::Lone(_)).then(|| { - format!( - "{what} the body doesn't fix, from {}, which JS keeps apart from what holds it in Rust", - o.array.what() - ) - }) - }; - let out = |o: Origin<'tcx>, n: Option, what: &str| { - let Some(index) = o.index else { - return unfixed(o, &format!("{what} moved by an offset")); - }; - // A pointer from an array of one stays at its element: one past it is Rust's - // too, but a read through it, which reaches the neighbour, is where no check is. - let n = match o.array { - Array::Lone(_) => n.or(Some(1)), - _ => n, - }; - let past = o.array.len().is_some_and(|len| index + n.unwrap_or(0) > len); - (index < 0 || past).then(|| { - format!( - "{what} reaching past {}, which JS keeps apart from what holds it in Rust", - o.array.what() - ) - }) - }; + let (unfixed, out) = (Self::unfixed, Self::out); match (owner.as_str(), name.as_str()) { ("ptr" | "NonNull", "cast") => self.erased_borrow(arg(0)?, pointee, gargs.types().nth(1)?), ("Arc" | "Rc", "from_raw" | "increment_strong_count" | "decrement_strong_count") @@ -148,10 +122,27 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { } // A wrapping offset may leave any other array while nothing reads through it. ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { - let o = self.origin(arg(0)?)?; - let lone = matches!(o.array, Array::Lone(_)); - lone.then(|| out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer"))? + let o = self.lone_origin(arg(0)?)?; + out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer") } + // An access through a pointer, which the body shows one element at. + ( + "ptr" | "NonNull", + "read" + | "read_unaligned" + | "read_volatile" + | "write" + | "write_unaligned" + | "write_volatile" + | "replace" + | "swap" + | "as_ref" + | "as_mut" + | "drop_in_place", + ) => [arg(0), arg(1)] + .into_iter() + .flatten() + .find_map(|p| self.access(self.lone_origin(p)?)), ("raw", "from_raw_parts" | "from_raw_parts_mut") | ("ptr", "slice_from_raw_parts" | "slice_from_raw_parts_mut") | ("NonNull", "slice_from_raw_parts") => { @@ -196,6 +187,56 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { } } + /// Why a read or write through the raw pointer `place` derefs would fail at run time, if + /// the body shows that pointer off the element of an array of one. + pub(super) fn beside(&self, place: mir::Place<'tcx>) -> Option { + let [ProjectionElem::Deref, ..] = place.projection.as_slice() else { + return None; + }; + if !self.mir.local_decls[place.local].ty.is_raw_ptr() { + return None; + } + self.access(self.lone_origin(&Operand::Copy(mir::Place::from(place.local)))?) + } + + /// Why an access of the one element a pointer at `o` reads or writes reaches out of its + /// array, if the body fixes where it is: one past it, as a pointer further out, or at + /// an offset the body doesn't fix, is refused where it is made. + fn access(&self, o: Origin<'tcx>) -> Option { + let len = o.array.len()?; + (o.index? == len).then(|| Self::out(o, Some(1), "a read or write"))? + } + + /// Where the pointer `op` points, if the body shows it in an array of one. + fn lone_origin(&self, op: &Operand<'tcx>) -> Option> { + self.origin(op).filter(|o| matches!(o.array, Array::Lone(_))) + } + + /// Why an offset or a count the body doesn't fix, from `o`, may reach out of it: only + /// from an array of one. + fn unfixed(o: Origin<'tcx>, what: &str) -> Option { + matches!(o.array, Array::Lone(_)).then(|| { + format!( + "{what} the body doesn't fix, from {}, which JS keeps apart from what holds it in Rust", + o.array.what() + ) + }) + } + + /// Why `what`, at `o` and reaching `n` elements past it if a count, leaves `o`'s array. + fn out(o: Origin<'tcx>, n: Option, what: &str) -> Option { + let Some(index) = o.index else { + return Self::unfixed(o, &format!("{what} moved by an offset")); + }; + let past = o.array.len().is_some_and(|len| index + n.unwrap_or(0) > len); + (index < 0 || past).then(|| { + format!( + "{what} reaching past {}, which JS keeps apart from what holds it in Rust", + o.array.what() + ) + }) + } + /// Whether JS keeps `a` and `b` in different arrays. Arrays of different variables are; /// of one variable's, those at different fields or constant indices are, where Rust's /// are one allocation. @@ -265,6 +306,10 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ("ptr" | "NonNull", "add" | "sub" | "offset") => { Some(self.moved(self.origin(&args[0].node)?, name.as_str(), &args[1].node)) } + // Only an array of one's, which every wrapping offset must stay within. + ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { + Some(self.moved(self.lone_origin(&args[0].node)?, name.as_str(), &args[1].node)) + } ("NonNull", "as_ptr" | "new_unchecked") => self.origin(&args[0].node), ("slice" | "Vec", "as_ptr" | "as_mut_ptr") => self.start(&args[0].node), // The start of its own text's encoding, though a substring's bytes start past its diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index 7818dca0b..b76111e19 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -64,7 +64,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// The JS expression of `place`, read without a copy. pub fn place_expr(&mut self, place: Place<'tcx>, span: Span, out: &mut Vec) -> Expr { let tcx = self.tcx; - if let Some(why) = self.alloc_bytes(place) { + if let Some(why) = self.alloc_bytes(place).map(String::from).or_else(|| self.beside(place)) { return self.err(span, why); } // A local held as its parts reads them, as far as they go. @@ -827,7 +827,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { pub(super) fn store(&mut self, place: Place<'tcx>, value: Expr, span: Span) -> Vec { let (place, value) = self.rebuilt(place, value, span); let mut out = Vec::new(); - if let Some(why) = self.alloc_bytes(place) { + if let Some(why) = self.alloc_bytes(place).map(String::from).or_else(|| self.beside(place)) { out.push(StmtKind::Expr(self.err(span, why)).at(None)); return out; } From 521d90137251122843ca344188d99a6fa14d7a6a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:42:58 -0800 Subject: [PATCH 28/45] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/mir/origin.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 61ad7f9c2..e80472dfe 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -128,17 +128,8 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { // An access through a pointer, which the body shows one element at. ( "ptr" | "NonNull", - "read" - | "read_unaligned" - | "read_volatile" - | "write" - | "write_unaligned" - | "write_volatile" - | "replace" - | "swap" - | "as_ref" - | "as_mut" - | "drop_in_place", + "read" | "read_unaligned" | "read_volatile" | "write" | "write_unaligned" | "write_volatile" + | "replace" | "swap" | "as_ref" | "as_mut" | "drop_in_place", ) => [arg(0), arg(1)] .into_iter() .flatten() From 8ca09a4fb7ad9d15fa3174184e6c857c07453db6 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 13:07:23 -0800 Subject: [PATCH 29/45] fix(ts): check only accessed pointers, and none to a value of no bytes Co-Authored-By: Claude Opus 5.5 --- .../pointers/expected/pointers.Nothing.ts | 7 +++++++ .../lang/pointers/expected/pointers.ts | 13 ++++++++++++ ts/fixtures/lang/pointers/src/pointers.rs | 20 +++++++++++++++++++ ts/src/lower/mir/origin.rs | 11 +++++++--- 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 ts/fixtures/lang/pointers/expected/pointers.Nothing.ts diff --git a/ts/fixtures/lang/pointers/expected/pointers.Nothing.ts b/ts/fixtures/lang/pointers/expected/pointers.Nothing.ts new file mode 100644 index 000000000..eb46c21cc --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.Nothing.ts @@ -0,0 +1,7 @@ +// Generated by rust2-ts. Do not edit. + +/** A value of no bytes, whose every pointer is where it was. */ +export interface Nothing { +} + +//# sourceMappingURL=pointers.Nothing.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 912806b0d..9f1762893 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -29,6 +29,7 @@ import type * as Loaded from "./pointers.Loaded.ts"; import type * as Log from "./pointers.Log.ts"; import * as Named from "./pointers.Named.ts"; import type * as Node from "./pointers.Node.ts"; +import type * as Nothing from "./pointers.Nothing.ts"; import type * as Outer from "./pointers.Outer.ts"; import type * as Pair from "./pointers.Pair.ts"; import * as Paired from "./pointers.Paired.ts"; @@ -3429,10 +3430,22 @@ export function behindRefs(data: rs.ByteSlice, out: rs.ByteVec): void { rs.byteVecPush(out, q.v & 0xff); rs.byteVecPush(out, rs.ptrOffsetFrom(rs.arcRaw(arc2, rs.arcPlace(arc2), "Arc"), p) & 0xff); rs.byteVecPush(out, from$convert_From_ref_pointers_Halves32_ptr_NonNull_u32({ first: bits as rs.u32, second: ((bits as rs.u32) ^ 85) >>> 0 }).v & 0xff); + let lone: rs.u32 = bits as rs.u32; + const at: rs.Place = rs.place(() => lone, (value) => lone = value); + let end: rs.Place = rs.NULL; + end = rs.ptrAdd(at, 1); + rs.byteVecPush(out, rs.ptrOffsetFrom(end, at) & 0xff); + const _282: Nothing.Nothing = {}; + rs.byteVecPush(out, nothingTag(_282, bits)); arc2[Symbol.dispose](); arc[Symbol.dispose](); } +/** `bits`, given beside a `Nothing` borrowed through a pointer moved off one. */ +export function nothingTag(_nothing: Nothing.Nothing, bits: rs.u8): rs.u8 { + return bits ^ 60; +} + export function from$convert_From_ref_pointers_Halves32_ptr_NonNull_u32(halves: Halves32.Halves32): rs.Place { return rs.place(() => halves.second, (value) => halves.second = value); } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index c3ddb6092..9642a37f0 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2656,6 +2656,26 @@ pub fn behind_refs(data: &[u8], out: &mut Vec) { }; let second: NonNull = (&halves).into(); out.push(unsafe { *second.as_ptr() } as u8); + // A pointer one past a variable, stored whole through a pointer to a pointer, and one + // moved off a value of no bytes and borrowed: valid, as neither reads past its value. + let lone = u32::from(bits); + let at = &raw const lone; + let mut end = ptr::null::(); + unsafe { ptr::write(&raw mut end, at.add(1)) }; + out.push(unsafe { end.offset_from(at) } as u8); + let none = Nothing; + let past = unsafe { (&raw const none).add(1) }; + out.push(nothing_tag(unsafe { &*past }, bits)); +} + +/// A value of no bytes, whose every pointer is where it was. +struct Nothing; + +/// `bits`, given beside a `Nothing` borrowed through a pointer moved off one. +#[inline(never)] +fn nothing_tag(nothing: &Nothing, bits: u8) -> u8 { + let Nothing = nothing; + bits ^ 0x3c } /// Two numbers, the second of which a pointer from a reference to them points at. diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index e80472dfe..b1467b132 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -125,12 +125,13 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let o = self.lone_origin(arg(0)?)?; out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer") } - // An access through a pointer, which the body shows one element at. + // An access through a pointer, which the body shows one element at: both of a + // swap's, and else the first argument, as the second is the value written. ( "ptr" | "NonNull", "read" | "read_unaligned" | "read_volatile" | "write" | "write_unaligned" | "write_volatile" | "replace" | "swap" | "as_ref" | "as_mut" | "drop_in_place", - ) => [arg(0), arg(1)] + ) => [Some(arg(0)?), arg(1).filter(|_| name == "swap")] .into_iter() .flatten() .find_map(|p| self.access(self.lone_origin(p)?)), @@ -184,7 +185,11 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let [ProjectionElem::Deref, ..] = place.projection.as_slice() else { return None; }; - if !self.mir.local_decls[place.local].ty.is_raw_ptr() { + // Every pointer to a value of no bytes is where it was, as `stray` leaves it. + let ty::RawPtr(pointee, _) = *self.mir.local_decls[place.local].ty.kind() else { + return None; + }; + if self.b.t.zero_sized(pointee) || self.b.t.pointee(pointee) != Pointee::Place { return None; } self.access(self.lone_origin(&Operand::Copy(mir::Place::from(place.local)))?) From 1a767f3f5da169c8ab321a4e60703ed54b16aa38 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 13:26:53 -0800 Subject: [PATCH 30/45] quest: finish the ts-unsafe epic Its last children, the UnsafeCell accessors (#625) and this raw pointer behind a reference, are done, and its plan holds no other work: what is left of `unsafe` stays refused under m3's unsafe policy, which the remaining mentions now name. Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 8 ++-- quest/m3/README.md | 1 - quest/m3/ts-assoc-type-fields.md | 4 +- quest/m3/ts-runtime-throws.md | 4 -- quest/m3/ts-unsafe/README.md | 75 -------------------------------- ts/README.md | 2 +- 6 files changed, 7 insertions(+), 87 deletions(-) delete mode 100644 quest/m3/ts-unsafe/README.md diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index a1b3f1575..02ab1154b 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -33,8 +33,8 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi the translator is specific to moq: dependencies follow general rules, and moq's own changes (to its `unsafe` and its address keys, dropping or binding `hashlink`, `slab`, and `smallvec`) are one research-tier quest in moq's tree. -- A dependency containing `unsafe` is bound by the consumer until - [unsafe code the translator can model](/quest/m3/ts-unsafe/README.md) translates it: decided +- A dependency containing `unsafe` is bound by the consumer unless it uses only the + `unsafe` [m3's unsafe policy](/quest/m3/README.md) translates: decided with the maintainer on 2026-10-27, a stock crate translates with no annotations or config. Only true platform boundaries (clocks and DEFLATE) stay bound. - Each child measures this target by the census of `ts/moq/model.toml`, rooted at those @@ -48,7 +48,7 @@ pin; each case it adds as a type translates. Every other root reaches the rest o model, all of it moq's, for its research quest: kio's `log.rs` reads bytes as its segments and retires them through crossbeam-epoch, whose tagged pointers and inline closures (36 refusals) translation refuses for good, and smallvec's union over a type parameter, its -`Array::Item`, and raw parts (5) wait on [unsafe](/quest/m3/ts-unsafe/README.md), so moq rewrites or drops +`Array::Item`, and raw parts (5) stay refused under [m3's unsafe policy](/quest/m3/README.md), so moq rewrites or drops them, as it does the `unsafe` slice of `model/frame.rs`'s `Filled`. Decided with the maintainer on 2026-11-04, from a census at the bumped pin @@ -71,6 +71,6 @@ or pointers whose comparison disagreed with Rust after an address was reused: addresses (`model/cache.rs`, `model/frame.rs`). - ✅ moq gives its `unsafe` no `cfg(rust2_ts)` twins; it decides per item, when its quest starts, whether to rewrite one in safe Rust for both builds or wait for this translator. - Rejected: twins until [unsafe](/quest/m3/ts-unsafe/README.md) translates them. + Rejected: twins until rust2-ts translates them, which [m3's unsafe policy](/quest/m3/README.md) mostly refuses. Public API: the translator's output for moq-net's model. diff --git a/quest/m3/README.md b/quest/m3/README.md index 7f1e5ac48..69c45ec3e 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -42,7 +42,6 @@ memory model for values whose address is reinterpreted, without a consumer. - [Retrial Cranelift on moq's tests](/quest/m3/cranelift-unwind.md) - waits on upstream: moq's tests pass under Cranelift, so its CI can weigh it again - [Relink without rebuilding](/quest/m3/relink.md) - measure upstream's skip of dependency-only rebuilds on moq's edits once it reaches nightly -- [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 - [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` diff --git a/quest/m3/ts-assoc-type-fields.md b/quest/m3/ts-assoc-type-fields.md index 1efd288ba..5279934bc 100644 --- a/quest/m3/ts-assoc-type-fields.md +++ b/quest/m3/ts-assoc-type-fields.md @@ -10,8 +10,8 @@ tests against native Rust. ## Plan Suggested on [#600](https://github.com/moq-dev/rust2/pull/600) (rangemap): it is 1 of -smallvec's 10 model refusals at the moq pin. The rest are `unsafe` that the -[ts-unsafe epic](/quest/m3/ts-unsafe/README.md) owns, and moq drops or binds smallvec, so +smallvec's 10 model refusals at the moq pin. The rest are `unsafe` that +[m3's unsafe policy](/quest/m3/README.md) refuses, and moq drops or binds smallvec, so this is a language gap rather than a consumer's. Decided with the maintainer on 2026-11-07: ✅ m3 (rejected: m2, drop), as a core language diff --git a/quest/m3/ts-runtime-throws.md b/quest/m3/ts-runtime-throws.md index f3aa2146c..9a108b3e1 100644 --- a/quest/m3/ts-runtime-throws.md +++ b/quest/m3/ts-runtime-throws.md @@ -64,7 +64,3 @@ that are each a JS array of their own keep their identity. Look for an exact slo with the `*const [T]` comparisons above. Public API: the translator's output, and which crates translate. - -## Related - -- [Unsafe code the translator can model](/quest/m3/ts-unsafe/README.md) - the raw-pointer model several of these throws guard; its 2026-11-01 run-time failures now follow this quest's rule diff --git a/quest/m3/ts-unsafe/README.md b/quest/m3/ts-unsafe/README.md deleted file mode 100644 index b7cf46bf2..000000000 --- a/quest/m3/ts-unsafe/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Unsafe code the translator can model - -## Goal - -[rust2-ts](/ts/README.md) translates the `unsafe` constructs that fit its pointer forms -with exact semantics in JS, each proven by differential tests against native Rust, and -refuses the rest with a span, as [m3's unsafe policy](/quest/m3/README.md) decides. - -## Plan - -Decided with the maintainer on 2026-10-27: a dependency containing `unsafe` is bound by -the consumer [at first](/ts/README.md#bindings), a stopgap, since a stock crate should -translate with no annotations or config; this epic is what retires it. Decided on 2026-11-07 -in [the model epic](/quest/m2/ts-model/README.md): moq writes no `cfg(rust2_ts)` twins of -its own `unsafe`. - -Decided with the maintainer on 2026-10-28, typed pointers: a raw pointer is what its -reference is, the object, an `rs.Place`, or `rs.NULL`, and a pointer into a slice or `Vec` -is `{ array, index }`. Byte reinterpretation, pointer-to-integer casts, and raw allocation -are refused with a span. [#201](https://github.com/moq-dev/rust2/pull/201) landed `Box` -to raw and back, `NonNull`, `MaybeUninit`, raw reads and writes, `get_unchecked`, and -unions whose overlapping fields share a type, which took hashlink from 375 refusals to -none of its own `unsafe`. A union over a type parameter lays out each instance since -[#620](https://github.com/moq-dev/rust2/pull/620); smallvec's `SmallVecData` overlaps a -`MaybeUninit<[T; N]>` with a pointer and a length, byte reinterpretation, so it stays refused. - -- An `unsafe` operation outside the modeled set is refused with its span. -- Undefined behavior in Rust need not be reproduced, but a translated program never - silently differs from native where Rust's behavior is defined. -- Element pointers into arrays of values JS copies are `rs.AtIndex`, `{ array, index }`, - which `add`, `offset`, `offset_from`, `ptr::copy`, and `slice::from_raw_parts` act on. - Decided with the maintainer on 2026-11-01: where one body shows a pointer crossing what - JS keeps apart (a value in no array), translation refuses it with a span. A pointer to - an element JS keeps as an object, such as a tuple or the crate's struct, is that object, - which knows no array it is in, so arithmetic on one stays refused: hashbrown's buckets - and kio's `log.rs` reach it. -- Decided with the maintainer on 2026-11-01, typed allocation: memory from `alloc` for a - `Layout::array::(n)`, or an equal layout of `T`'s size and alignment, cast to `*mut T` - is a fresh JS array of `T`s, which `realloc` copies and `dealloc` frees. Untyped and - byte allocation stay refused. smallvec spills to it. `Vec::from_raw_parts` of it stays - refused, deleted on 2026-11-07 under m3's unsafe policy: a `Vec` adopting that array - needs storage tracked apart from the pointer - ([#635](https://github.com/moq-dev/rust2/pull/635)). -- Decided with the maintainer on 2026-11-02: a raw slice pointer is `rs.RawSlice`, its - first element's pointer and its length, read through only where Rust reads, and an empty - slice from a pointer into no array keeps that pointer through every part of it. - -Decided with the maintainer on 2026-11-06, splitting this quest into one child per -construct still open at `b3df54b6c`, all in m3: - -- Goal: every `unsafe` construct rust2-ts can make exact, one quest each. Rejected: only - the constructs moq's dependencies hit. Narrowed on 2026-11-07 by m3's unsafe policy to - the constructs that fit the existing pointer forms. -- ✅ All children in m3. Rejected: spare capacity in m2; smallvec's constructs in m2. - No child blocks moq: the model's only hits are smallvec's (7 of its 10 refusals), and - moq drops or binds smallvec. -- `Vec::from_raw_parts` of a typed allocation is its own child, restored on review of - [#618](https://github.com/moq-dev/rust2/pull/618): the spare-capacity child fills an - existing `Vec`, not one adopting an allocation. Deleted on 2026-11-07 under m3's unsafe - policy, above. -- The 2026-11-01 clauses that let a pointer from elsewhere, a `Vec`'s spare capacity, and - a byte pointer past its view fail loudly at run time are superseded by - [Throw only where Rust panics](/quest/m3/ts-runtime-throws.md) (2026-11-04, #597): where - Rust defines the access it translates exactly or is refused with a span, and only a - throw guarding UB stays, as a precondition abort. -- Done and dropped: a byte pointer past its own `Uint8Array` view. Byte pointers index the - whole buffer since [#400](https://github.com/moq-dev/rust2/pull/400) and - [#576](https://github.com/moq-dev/rust2/pull/576), and a byte `Vec` keeps its - capacity since [#555](https://github.com/moq-dev/rust2/pull/555) and - [#568](https://github.com/moq-dev/rust2/pull/568). `bytes`' `Buf::get_u16` and wider - map to the runtime since [#447](https://github.com/moq-dev/rust2/pull/447). - -Census at the moq pin on 2026-11-07: model 45, kio 72, moq-net 344 refusals. - -Public API: the translator's output. diff --git a/ts/README.md b/ts/README.md index 1cb2bb70f..168ea5811 100644 --- a/ts/README.md +++ b/ts/README.md @@ -1145,7 +1145,7 @@ they need. ## Not yet Byte reinterpretation, pointer addresses, pointer arithmetic on a value JS keeps as an object (other than wrapping arithmetic on a null or dangling pointer), memory from `alloc` other than an array of values JS copies, and unions whose -fields overlap in different types ([unsafe](/quest/m3/ts-unsafe/README.md)), and trait objects of a trait +fields overlap in different types ([m3's unsafe policy](/quest/m3/README.md)), and trait objects of a trait with a const parameter, of an array, or behind a pointer other than `&`, `&mut`, `Box`, `Arc`, `Rc`, `Weak`, or `Pin`, fail translation. Until they translate, an item that needs one, such as a `Box` made `From` a string, binds to hand-written TS ([bindings](#bindings)). From 0af963e81ebbeb4eb8cb9fdf14e752a28b03ac69 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:51:19 -0800 Subject: [PATCH 31/45] fix(ts): address the automated reviews of the merged head - origin.rs follows a cast JS keeps the same pointer (to a MaybeUninit or ManuallyDrop, and cast, cast_mut, cast_const), so a read one past a reference through one is refused with a span (reject's recast_reference). - An assignment's target folds a member only to another slot, so a one-use pointer `{ v: x }` written through stays `{ v: x }.v = y`, not `x = y`. - A move-out copy of an uninitialized value snapshots what a cell or a library object holds as uninitialized too, as a shell's `rs.Cell` holds nothing yet (pointers' uninit_moved). - pointers' moved_out frees what its whole writes replace, and ts-assoc-type-fields counts smallvec's 5 model refusals. Co-Authored-By: Claude Opus 5.5 --- quest/m3/ts-assoc-type-fields.md | 2 +- .../lang/conversions/expected/conversions.ts | 14 ++++ .../lang/conversions/src/conversions.rs | 2 + ts/fixtures/lang/pointers/expected/lib.ts | 22 ++++++ .../pointers/expected/pointers.HeldCell.ts | 10 +++ .../lang/pointers/expected/pointers.ts | 64 ++++++++++++---- ts/fixtures/lang/pointers/src/pointers.rs | 41 ++++++++-- ts/fixtures/reject/census.txt | 3 + ts/fixtures/reject/expected.txt | 3 + ts/fixtures/reject/src/alloc.rs | 9 +++ ts/fixtures/reject/ts.toml | 1 + ts/src/lower/copy.rs | 7 +- ts/src/lower/mir/mod.rs | 74 ++++++++++++------- ts/src/lower/mir/origin.rs | 14 +++- 14 files changed, 212 insertions(+), 54 deletions(-) create mode 100644 ts/fixtures/lang/pointers/expected/pointers.HeldCell.ts diff --git a/quest/m3/ts-assoc-type-fields.md b/quest/m3/ts-assoc-type-fields.md index 5279934bc..50ac6e3d1 100644 --- a/quest/m3/ts-assoc-type-fields.md +++ b/quest/m3/ts-assoc-type-fields.md @@ -10,7 +10,7 @@ tests against native Rust. ## Plan Suggested on [#600](https://github.com/moq-dev/rust2/pull/600) (rangemap): it is 1 of -smallvec's 10 model refusals at the moq pin. The rest are `unsafe` that +smallvec's 5 model refusals at the moq pin. The rest are `unsafe` that [m3's unsafe policy](/quest/m3/README.md) refuses, and moq drops or binds smallvec, so this is a language gap rather than a consumer's. diff --git a/ts/fixtures/lang/conversions/expected/conversions.ts b/ts/fixtures/lang/conversions/expected/conversions.ts index f6df00f85..40071645a 100644 --- a/ts/fixtures/lang/conversions/expected/conversions.ts +++ b/ts/fixtures/lang/conversions/expected/conversions.ts @@ -2340,6 +2340,7 @@ export function converted$Option_u16(data: rs.ByteSlice, out: rs.ByteVec): void put$conversions_Value_Option_u16(b, out); c = make$conversions_Value_Option_u16(data, 16); put$conversions_Value_Option_u16(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2430,6 +2431,7 @@ export function converted$String(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_String(b, out); c = make$conversions_Value_String(data, 16); put$conversions_Value_String(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2518,6 +2520,7 @@ export function converted$Vec_u32(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_Vec_u32(b, out); c = make$conversions_Value_Vec_u32(data, 16); put$conversions_Value_Vec_u32(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2606,6 +2609,7 @@ export function converted$Vec_u8(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_Vec_u8(b, out); c = make$conversions_Value_Vec_u8(data, 16); put$conversions_Value_Vec_u8(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2716,6 +2720,7 @@ export function converted$array0_u8(data: rs.ByteSlice, out: rs.ByteVec): void { rs.byteSliceFrom(c); copy$array_u8(make$conversions_Value_array0_u8(data, 16)); put$conversions_Value_array0_u8(rs.byteSliceFrom(c), out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2834,6 +2839,7 @@ export function converted$array1_u8(data: rs.ByteSlice, out: rs.ByteVec): void { const _211: Uint8Array = copy$array_u8(make$conversions_Value_array1_u8(data, 16)); q.data.set(_211, q.start); put$conversions_Value_array1_u8(rs.byteSliceFrom(c), out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2944,6 +2950,7 @@ export function converted$array2_u32(data: rs.ByteSlice, out: rs.ByteVec): void put$conversions_Value_array2_u32(rs.sliceFrom(b), out); rs.sliceSet(rs.sliceFrom(c), copy$array_u32(make$conversions_Value_array2_u32(data, 16))); put$conversions_Value_array2_u32(rs.sliceFrom(c), out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3062,6 +3069,7 @@ export function converted$array3_u8(data: rs.ByteSlice, out: rs.ByteVec): void { const _211: Uint8Array = copy$array_u8(make$conversions_Value_array3_u8(data, 16)); q.data.set(_211, q.start); put$conversions_Value_array3_u8(rs.byteSliceFrom(c), out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3180,6 +3188,7 @@ export function converted$array40_u8(data: rs.ByteSlice, out: rs.ByteVec): void const _211: Uint8Array = copy$array_u8(make$conversions_Value_array40_u8(data, 16)); q.data.set(_211, q.start); put$conversions_Value_array40_u8(rs.byteSliceFrom(c), out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3302,6 +3311,7 @@ export function converted$conversions_Logged(data: rs.ByteSlice, out: rs.ByteVec q[Symbol.dispose](); rs.ptrWrite(q, _211); Logged.put$conversions_Value(c, out); + ({ v: byte(data, 17) }.v = 2); rs.dropVec(none); rs.dropVec(list); rs.dropAll(array); @@ -3408,6 +3418,7 @@ export function converted$conversions_Named(data: rs.ByteSlice, out: rs.ByteVec) Named.put$conversions_Value(b, out); rs.ptrWrite(c, Named.make$conversions_Value(data, 16)); Named.put$conversions_Value(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3495,6 +3506,7 @@ export function converted$u32(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u32(b, out); c = make$conversions_Value_u32(data, 16); put$conversions_Value_u32(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3582,6 +3594,7 @@ export function converted$u64(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u64(b, out); c = make$conversions_Value_u64(data, 16); put$conversions_Value_u64(c, out); + ({ v: byte(data, 17) }.v = 2); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3667,6 +3680,7 @@ export function converted$u8(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u8(b, out); c = make$conversions_Value_u8(data, 16); put$conversions_Value_u8(c, out); + ({ v: byte(data, 17) }.v = 2); } /** diff --git a/ts/fixtures/lang/conversions/src/conversions.rs b/ts/fixtures/lang/conversions/src/conversions.rs index 1bf23ecd9..43ba3bf37 100644 --- a/ts/fixtures/lang/conversions/src/conversions.rs +++ b/ts/fixtures/lang/conversions/src/conversions.rs @@ -467,6 +467,8 @@ fn converted(data: &[u8], out: &mut Vec) { let q = std::ptr::from_mut(&mut c); unsafe { *q = T::make(data, 16) }; c.put(out); + // A box's pointer written through once, the only use of it, leaking the box. + unsafe { *Box::into_raw(Box::new(byte(data, 17))) = 2 }; } /// Boxes, which clone what they hold by its own `clone_from`: a box, one in a box, in an diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index e90490b4a..f706a9769 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -10,6 +10,7 @@ import type * as ByObject from "./pointers.ByObject.ts"; import type * as Duo from "./pointers.Duo.ts"; import type * as Either from "./pointers.Either.ts"; import type * as Halves from "./pointers.Halves.ts"; +import type * as HeldCell from "./pointers.HeldCell.ts"; import type * as Housed from "./pointers.Housed.ts"; import type * as Inner from "./pointers.Inner.ts"; import type * as Leaning from "./pointers.Leaning.ts"; @@ -103,6 +104,27 @@ export function copy$mem_MaybeUninit_array_u8(v: Uint8Array): Uint8Array { return v.slice(); } +export function copy$mem_MaybeUninit_cell_RefCell_u32(v: rs.RefCell): rs.RefCell { + if (v === undefined) { + return v; + } + return rs.objectCopy(v); +} + +export function copy$mem_MaybeUninit_cell_UnsafeCell_cell_RefCell_u32(v: rs.Cell>): rs.Cell> { + if (v === undefined) { + return v; + } + return new rs.Cell(copy$mem_MaybeUninit_cell_RefCell_u32(v.v)); +} + +export function copy$mem_MaybeUninit_pointers_HeldCell(v: HeldCell.HeldCell): HeldCell.HeldCell { + if (v === undefined) { + return v; + } + return { c: copy$mem_MaybeUninit_cell_UnsafeCell_cell_RefCell_u32(v.c) }; +} + export function copy$mem_MaybeUninit_tuple2_u8_u8(v: [rs.u8, rs.u8]): [rs.u8, rs.u8] { if (v === undefined) { return v; diff --git a/ts/fixtures/lang/pointers/expected/pointers.HeldCell.ts b/ts/fixtures/lang/pointers/expected/pointers.HeldCell.ts new file mode 100644 index 000000000..c56e811a2 --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.HeldCell.ts @@ -0,0 +1,10 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; + +/** A cell of a `RefCell`, which an uninitialized shell holds nothing in. */ +export interface HeldCell { + c: rs.Cell>; +} + +//# sourceMappingURL=pointers.HeldCell.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 3f9b49a03..c5f878a41 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Reentrant, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u16, copy$tuple2_u8_u8, drop$pointers_List_Vec_u8, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_pointers_HeldCell, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Reentrant, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u16, copy$tuple2_u8_u8, drop$pointers_List_Vec_u8, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; @@ -19,6 +19,7 @@ import type * as Gauge from "./pointers.Gauge.ts"; import type * as Halves from "./pointers.Halves.ts"; import type * as Halves32 from "./pointers.Halves32.ts"; import * as Held from "./pointers.Held.ts"; +import type * as HeldCell from "./pointers.HeldCell.ts"; import type * as Holder from "./pointers.Holder.ts"; import * as Housed from "./pointers.Housed.ts"; import type * as Inner from "./pointers.Inner.ts"; @@ -3483,7 +3484,7 @@ export function movedVec(v: rs.ByteVec, with_: rs.ByteVec, how: rs.u8): rs.ByteV } else if (_5 === 2) { old = copy$Vec_u8(rs.placeIn(v).v); } else { - let other: rs.ByteVec = rs.byteVecFrom(Uint8Array.of(how)); + let other: rs.ByteVec = new rs.ByteVec(); const x = rs.placeIn(v); const y = rs.place(() => other, (value) => other = value); const old2 = copy$mem_MaybeUninit_Vec_u8(x.v); @@ -3508,12 +3509,12 @@ export function movedNested(o: rs.Place, t: [rs.ByteVec, let slot: rs.ByteVec | undefined = (undefined as unknown) as rs.ByteVec | undefined; rs.ptrCopy(o, rs.place(() => slot, (value) => slot = value), 1, copy$Option_Vec_u8); const _22: [rs.ByteVec, rs.u8] = t; - const _23: [rs.ByteVec, rs.u8] = [rs.byteVecFrom(Uint8Array.of(how)), how]; + const _23: [rs.ByteVec, rs.u8] = [new rs.ByteVec(), how]; const pointer = _22; const old2 = rs.objectCopy(pointer); rs.ptrWrite(pointer, _23); const oldT: [rs.ByteVec, rs.u8] = old2; - const oldS: TaggedVec.TaggedVec = { v: rs.byteVecFrom(Uint8Array.of(how)), tag: how }; + const oldS: TaggedVec.TaggedVec = { v: new rs.ByteVec(), tag: how }; const x = s; const y = oldS; const old3 = rs.objectCopy(x); @@ -3522,13 +3523,13 @@ export function movedNested(o: rs.Place, t: [rs.ByteVec, old = [slot, oldT, oldS] as [rs.ByteVec | undefined, [rs.ByteVec, rs.u8], TaggedVec.TaggedVec]; } if (o.v !== undefined) { - const _39: rs.ByteVec = rs.byteSliceToByteVec(with_); - refillIn(o.v, _39); + const _37: rs.ByteVec = rs.byteSliceToByteVec(with_); + refillIn(o.v, _37); } - const _42: rs.ByteVec = rs.byteSliceToByteVec(with_); - refillIn(t[0], _42); - const _45: rs.ByteVec = rs.byteSliceToByteVec(with_); - refillIn(s.v, _45); + const _40: rs.ByteVec = rs.byteSliceToByteVec(with_); + refillIn(t[0], _40); + const _43: rs.ByteVec = rs.byteSliceToByteVec(with_); + refillIn(s.v, _43); return old; } @@ -3578,11 +3579,21 @@ export function cellRewritten(r: rs.Cell, n: rs.u32): void { } /** - * A `HashMap` written whole through a pointer from the `&mut` given, which holds what the - * new one does, its capacity included. + * A `HashMap` dropped in place and written whole through a pointer from the `&mut` given, + * which holds what the new one does, its capacity included. */ export function mapRewritten(r: rs.HashMap): void { - rs.placeIn(r).v = rs.hashNew() as rs.HashMap; + const p: rs.Place> = rs.placeIn(r); + undefined; + p.v = rs.hashNew() as rs.HashMap; +} + +/** + * What the uninitialized `HeldCell` a shared reference points at holds, moved out through + * a pointer from it. + */ +export function uninitMoved(r: HeldCell.HeldCell): HeldCell.HeldCell { + return copy$mem_MaybeUninit_pointers_HeldCell(r); } /** @@ -3720,6 +3731,33 @@ export function movedOut(data: rs.ByteSlice, out: rs.ByteVec): void { cellRewritten(cell2, rs.u32Add(b2 as rs.u32, 2)); rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(x2 & 0xff, cell2.v & 0xff)), undefined, true); } + const held: HeldCell.HeldCell = ({ c: new rs.Cell(undefined) } as unknown) as HeldCell.HeldCell; + const old3: HeldCell.HeldCell = uninitMoved(held); + const iter10: rs.Iter<[rs.usize, HeldCell.HeldCell]> = rs.iterEnumerate(rs.iterSlice([held, old3])); + while (true) { + const _249: [rs.usize, HeldCell.HeldCell] | rs.Done = iter10.next(); + if (_249 === rs.DONE) { + break; + } + const i: rs.usize = _249[0]; + const _259: rs.u32 = option_unwrapOr$1fr498u(rs.byteSliceFirst(data), 0) as rs.u32; + const object = _249[1]; + const value4 = { c: new rs.Cell(new rs.RefCell(rs.u32Add(_259, i as rs.u32))) }; + const cell3 = object.c; + rs.ptrWrite(object, value4); + if (cell3 !== undefined) { + object.c = cell3; + cell3.v = value4.c.v; + } + } + const iter11: rs.Iter = rs.iterSlice([held, old3]); + while (true) { + const _270: HeldCell.HeldCell | rs.Done = iter11.next(); + if (_270 === rs.DONE) { + break; + } + rs.byteVecPush(out, rs.valuePlace(_270.c.v).v & 0xff); + } } /** diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 7244e770f..246686266 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2715,7 +2715,7 @@ fn moved_vec(v: &mut Vec, with: Vec, how: u8) -> Vec { 1 => unsafe { ptr::read(ptr::from_mut(v).cast::>>()).assume_init() }, 2 => mem::ManuallyDrop::into_inner(unsafe { ptr::read(ptr::from_mut(v).cast::>>()) }), _ => { - let mut other = MaybeUninit::new(vec![how]); + let mut other = MaybeUninit::new(Vec::new()); unsafe { ptr::swap(ptr::from_mut(v).cast::>>(), &mut other) }; unsafe { other.assume_init() } } @@ -2745,8 +2745,11 @@ fn moved_nested( _ => unsafe { let mut slot = MaybeUninit::uninit(); ptr::copy_nonoverlapping(ptr::from_mut(o), slot.as_mut_ptr(), 1); - let old_t = ptr::replace(ptr::from_mut(t), (vec![how], how)); - let mut old_s = TaggedVec { v: vec![how], tag: how }; + let old_t = ptr::replace(ptr::from_mut(t), (Vec::new(), how)); + let mut old_s = TaggedVec { + v: Vec::new(), + tag: how, + }; ptr::swap(ptr::from_mut(s), &mut old_s); (slot.assume_init(), old_t, old_s) }, @@ -2789,11 +2792,27 @@ fn cell_rewritten(r: &Cell, n: u32) { unsafe { ptr::from_ref(r).cast_mut().write(Cell::new(n)) } } -/// A `HashMap` written whole through a pointer from the `&mut` given, which holds what the -/// new one does, its capacity included. +/// A `HashMap` dropped in place and written whole through a pointer from the `&mut` given, +/// which holds what the new one does, its capacity included. #[inline(never)] fn map_rewritten(r: &mut std::collections::HashMap) { - unsafe { ptr::write(ptr::from_mut(r), std::collections::HashMap::new()) } + let p = ptr::from_mut(r); + unsafe { + ptr::drop_in_place(p); + ptr::write(p, std::collections::HashMap::new()); + } +} + +/// A cell of a `RefCell`, which an uninitialized shell holds nothing in. +struct HeldCell { + c: UnsafeCell>, +} + +/// What the uninitialized `HeldCell` a shared reference points at holds, moved out through +/// a pointer from it. +#[inline(never)] +fn uninit_moved(r: &MaybeUninit) -> MaybeUninit { + unsafe { ptr::read(r) } } /// A range of cells and a cell `Cell::from_mut` made of a variable's place, moved out @@ -2879,6 +2898,16 @@ pub fn moved_out(data: &[u8], out: &mut Vec) { cell_rewritten(&cell, u32::from(b) + 2); out.extend([x as u8, cell.get() as u8]); } + let mut held = Box::::new_uninit(); + let mut old = uninit_moved(&held); + for (i, h) in [&mut *held, &mut old].into_iter().enumerate() { + h.write(HeldCell { + c: UnsafeCell::new(RefCell::new(u32::from(data.first().copied().unwrap_or(0)) + i as u32)), + }); + } + for h in [&mut *held, &mut old] { + out.push(*unsafe { h.assume_init_mut() }.c.get_mut().get_mut() as u8); + } } /// Pointers to slices cast to ones to arrays of their elements, of bytes and of numbers: diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 7c2d04f3a..2be49c939 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -43,6 +43,9 @@ fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a valu fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:pinned_lent+1:32: `UnsafeCell::get_mut` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address fixtures/reject/src/alloc.rs:pinned_lent+2:43: `UnsafeCell::as_mut_unchecked` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address +fixtures/reject/src/alloc.rs:recast_reference+4:11: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:recast_reference+4:48: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:recast_reference+4:76: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:unfixed_reference+3:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:unfixed_reference+4:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 821176e3f..70e19d0e8 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -47,6 +47,9 @@ fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a valu fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:pinned_lent+1:32: `UnsafeCell::get_mut` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address fixtures/reject/src/alloc.rs:pinned_lent+2:43: `UnsafeCell::as_mut_unchecked` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address +fixtures/reject/src/alloc.rs:recast_reference+4:11: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:recast_reference+4:48: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:recast_reference+4:76: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:reinterpreted+1:27: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:unfixed_reference+3:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:unfixed_reference+4:35: a pointer moved by an offset the body doesn't fix, from a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index eee41cd67..5b8161627 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -98,6 +98,15 @@ pub fn beside_reference(r: &u32, n: usize) -> u32 { unsafe { *p.wrapping_add(1) + *p.add(1) + std::slice::from_raw_parts(p, n).iter().sum::() } } +/// `beside_reference`'s read one past, through a pointer cast to one JS keeps the same: +/// to a `MaybeUninit`, by `cast_mut`, and by `cast` to a `ManuallyDrop`. +pub fn recast_reference(r: &u32) -> u32 { + let p = (&raw const *r) as *const std::mem::MaybeUninit; + let q = std::ptr::from_ref(r).cast_mut(); + let m = std::ptr::from_ref(r).cast::>(); + unsafe { (*p.add(1)).assume_init() + q.add(1).read() + *m.wrapping_add(1).read() } +} + /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index ae60cc880..48b90ca21 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -17,6 +17,7 @@ roots = [ "alloc::past_reference", "alloc::peeked_moved", "alloc::pinned_lent", + "alloc::recast_reference", "alloc::reinterpreted", "alloc::unfixed_reference", "alloc::uninit_cell_lent", diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 961a66aae..0f3e3faef 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -382,7 +382,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } /// The body of the function copying `v`, a `ty`, or a `MaybeUninit` of one if `uninit`: - /// then any of its parts kept as objects may hold nothing yet. + /// then any of its parts kept as objects may hold nothing yet, as a cell in an + /// uninitialized shell holds nothing. fn copy_parts(&mut self, ty: Ty<'tcx>, uninit: bool, span: Span) -> Vec { let v = || Expr::ident("v"); let ret = |e: Expr| vec![StmtKind::Return(Some(e.typed())).at(None)]; @@ -464,7 +465,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }; // A cell of its own, as `Cell::from_mut`'s place reads and writes another's. if std == Std::Cell { - let held = self.snapshot(v().member("v"), args.type_at(0), span); + let held = self.snapshot(v().member("v"), part(args.type_at(0)), span); return ret(Expr::New(Box::new(Expr::Rs("Cell".into())), vec![held])); } let fields: Vec<_> = inline.fields.into_iter().filter(|&(_, t)| self.changes(t)).collect(); @@ -475,7 +476,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { let c = || Expr::ident("c"); let mut body = vec![StmtKind::decl(false, "c", None, Some(copy)).at(None)]; for (field, t) in fields { - let read = self.snapshot(c().member(field), t, span); + let read = self.snapshot(c().member(field), part(t), span); body.push(StmtKind::Expr(c().member(field).assign(read)).at(None)); } body.push(StmtKind::Return(Some(c())).at(None)); diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index de4f69550..8394c4c96 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -3814,6 +3814,36 @@ fn located(value: Expr, loc: Option) -> Expr { } } +/// What the member `key` of `base`, which a substitution made, reads, if `base` shows it. +fn folded_member(base: &Expr, key: &ts::Key) -> Option { + // The payload of a `Result` made here is what it was made of, which `tsc` reads where it + // can't narrow the `Result`, cast to its variant or not, and so is what an `rs.Place` + // made here holds. + let made = match step::peel(base) { + Expr::As(made, _) => step::peel(made), + base => base, + }; + match (made, key) { + (Expr::Call(f, args), ts::Key::Name(key)) + if let Expr::Rs(ctor) = step::peel(f) + && matches!((ctor.as_str(), key.as_str()), ("Ok", "value") | ("Err", "error")) + && let [payload] = args.as_slice() => + { + Some(payload.clone()) + } + // A pointer made here of a shared reference holds its value. + (Expr::Object(members), ts::Key::Name(key)) + if key == "v" + && let [(ts::Key::Name(v), value)] = members.as_slice() + && v == "v" => + { + Some(value.clone()) + } + (_, ts::Key::Name(key)) if key == "v" => step::peel(base).place_read(), + _ => None, + } +} + /// Replace the one read of `name` in `stmt` with `value`; whether it was there. fn subst_stmt(stmt: &mut Stmt, name: &str, value: &Expr) -> bool { match &mut stmt.kind { @@ -3837,33 +3867,7 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { if !subst(base, name, value) { return false; } - // The payload of a `Result` made here is what it was made of, which `tsc` reads - // where it can't narrow the `Result`, cast to its variant or not, and so is what an - // `rs.Place` made here holds. - let made = match step::peel(base) { - Expr::As(made, _) => step::peel(made), - base => base, - }; - let folded = match (made, &*key) { - (Expr::Call(f, args), ts::Key::Name(key)) - if let Expr::Rs(ctor) = step::peel(f) - && matches!((ctor.as_str(), key.as_str()), ("Ok", "value") | ("Err", "error")) - && let [payload] = args.as_slice() => - { - Some(payload.clone()) - } - // A pointer made here of a shared reference holds its value. - (Expr::Object(members), ts::Key::Name(key)) - if key == "v" - && let [(ts::Key::Name(v), value)] = members.as_slice() - && v == "v" => - { - Some(value.clone()) - } - (_, ts::Key::Name(key)) if key == "v" => step::peel(base).place_read(), - _ => None, - }; - if let Some(folded) = folded { + if let Some(folded) = folded_member(base, key) { *e = folded; } true @@ -3939,7 +3943,21 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { } true } - Expr::Assign(l, r) => subst(l, name, value) || subst(r, name, value), + // A member assigned to folds only to another slot, as `{ v: x }.v` does to `x`. + Expr::Assign(l, r) => { + let Expr::Member(base, key) = &mut **l else { + return subst(l, name, value) || subst(r, name, value); + }; + if !subst(base, name, value) { + return subst(r, name, value); + } + if let Some(folded) = folded_member(base, key) + && matches!(folded.unlocated(), Expr::Ident(_) | Expr::Member(..) | Expr::Index(..)) + { + **l = folded; + } + true + } Expr::Cond(c, a, b) => subst(c, name, value) || subst(a, name, value) || subst(b, name, value), Expr::Arrow(..) | Expr::Lambda(..) => false, } diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 6bcbc4e99..564f29810 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -306,7 +306,14 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { Some(self.moved(self.lone_origin(&args[0].node)?, name.as_str(), &args[1].node)) } - ("NonNull", "as_ptr" | "new_unchecked") => self.origin(&args[0].node), + ("NonNull", "as_ptr" | "new_unchecked") | ("ptr" | "NonNull", "cast_const" | "cast_mut") => { + self.origin(&args[0].node) + } + ("ptr" | "NonNull", "cast") + if self.b.t.uninit_inner(gargs.type_at(0)) == self.b.t.uninit_inner(gargs.type_at(1)) => + { + self.origin(&args[0].node) + } ("slice" | "Vec", "as_ptr" | "as_mut_ptr") => self.start(&args[0].node), // The start of its own text's encoding, though a substring's bytes start past its // text's in Rust, so a move back before it is refused, as one off a copy is. @@ -601,9 +608,10 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { (self.defs[local] == 1 && !self.is_arg(local)).then_some(local) } - /// Whether `op` is a pointer to what `local` points at. + /// Whether `op` is a pointer to what `local` points at, which JS keeps the same pointer, + /// as `pointer::cast` does one to what a `MaybeUninit` or `ManuallyDrop` holds. fn same_pointee(&self, op: &Operand<'tcx>, local: Local) -> bool { - let pointee = |ty: ty::Ty<'tcx>| ty.builtin_deref(true); + let pointee = |ty: ty::Ty<'tcx>| ty.builtin_deref(true).map(|t| self.b.t.uninit_inner(t)); let from = op.ty(&self.mir.local_decls, self.tcx); pointee(from).is_some() && pointee(from) == pointee(self.mir.local_decls[local].ty) } From 3281e7d38da1c76740c8c33b4e1eb087ad328018 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:16:09 -0800 Subject: [PATCH 32/45] fix(ts): refuse a branch-joined reference pointer's stray, and fold no copy as a target - origin.rs reads a pointer assigned more than once as the array of one any assignment the body shows makes from a reference, so `offset_from` against another and a read past it are refused with a span rather than thrown at run time (reject's joined_reference). - An assignment's member target folds only to the place an `rs.place` reads, as `{ v: x }` holds a copy of `x` (conversions' leaked box). Co-Authored-By: Claude Opus 5.5 --- .../lang/conversions/expected/conversions.ts | 56 +++++++--- .../lang/conversions/src/conversions.rs | 7 +- ts/fixtures/reject/census.txt | 2 + ts/fixtures/reject/expected.txt | 2 + ts/fixtures/reject/src/alloc.rs | 12 +++ ts/fixtures/reject/ts.toml | 1 + ts/src/lower/mir/mod.rs | 13 ++- ts/src/lower/mir/origin.rs | 100 +++++++++++++----- 8 files changed, 149 insertions(+), 44 deletions(-) diff --git a/ts/fixtures/lang/conversions/expected/conversions.ts b/ts/fixtures/lang/conversions/expected/conversions.ts index 40071645a..243c9307f 100644 --- a/ts/fixtures/lang/conversions/expected/conversions.ts +++ b/ts/fixtures/lang/conversions/expected/conversions.ts @@ -2340,7 +2340,9 @@ export function converted$Option_u16(data: rs.ByteSlice, out: rs.ByteVec): void put$conversions_Value_Option_u16(b, out); c = make$conversions_Value_Option_u16(data, 16); put$conversions_Value_Option_u16(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2431,7 +2433,9 @@ export function converted$String(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_String(b, out); c = make$conversions_Value_String(data, 16); put$conversions_Value_String(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2520,7 +2524,9 @@ export function converted$Vec_u32(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_Vec_u32(b, out); c = make$conversions_Value_Vec_u32(data, 16); put$conversions_Value_Vec_u32(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2609,7 +2615,9 @@ export function converted$Vec_u8(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_Vec_u8(b, out); c = make$conversions_Value_Vec_u8(data, 16); put$conversions_Value_Vec_u8(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2720,7 +2728,9 @@ export function converted$array0_u8(data: rs.ByteSlice, out: rs.ByteVec): void { rs.byteSliceFrom(c); copy$array_u8(make$conversions_Value_array0_u8(data, 16)); put$conversions_Value_array0_u8(rs.byteSliceFrom(c), out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2839,7 +2849,9 @@ export function converted$array1_u8(data: rs.ByteSlice, out: rs.ByteVec): void { const _211: Uint8Array = copy$array_u8(make$conversions_Value_array1_u8(data, 16)); q.data.set(_211, q.start); put$conversions_Value_array1_u8(rs.byteSliceFrom(c), out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -2950,7 +2962,9 @@ export function converted$array2_u32(data: rs.ByteSlice, out: rs.ByteVec): void put$conversions_Value_array2_u32(rs.sliceFrom(b), out); rs.sliceSet(rs.sliceFrom(c), copy$array_u32(make$conversions_Value_array2_u32(data, 16))); put$conversions_Value_array2_u32(rs.sliceFrom(c), out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3069,7 +3083,9 @@ export function converted$array3_u8(data: rs.ByteSlice, out: rs.ByteVec): void { const _211: Uint8Array = copy$array_u8(make$conversions_Value_array3_u8(data, 16)); q.data.set(_211, q.start); put$conversions_Value_array3_u8(rs.byteSliceFrom(c), out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3188,7 +3204,9 @@ export function converted$array40_u8(data: rs.ByteSlice, out: rs.ByteVec): void const _211: Uint8Array = copy$array_u8(make$conversions_Value_array40_u8(data, 16)); q.data.set(_211, q.start); put$conversions_Value_array40_u8(rs.byteSliceFrom(c), out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3311,7 +3329,9 @@ export function converted$conversions_Logged(data: rs.ByteSlice, out: rs.ByteVec q[Symbol.dispose](); rs.ptrWrite(q, _211); Logged.put$conversions_Value(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); rs.dropVec(none); rs.dropVec(list); rs.dropAll(array); @@ -3418,7 +3438,9 @@ export function converted$conversions_Named(data: rs.ByteSlice, out: rs.ByteVec) Named.put$conversions_Value(b, out); rs.ptrWrite(c, Named.make$conversions_Value(data, 16)); Named.put$conversions_Value(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3506,7 +3528,9 @@ export function converted$u32(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u32(b, out); c = make$conversions_Value_u32(data, 16); put$conversions_Value_u32(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3594,7 +3618,9 @@ export function converted$u64(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u64(b, out); c = make$conversions_Value_u64(data, 16); put$conversions_Value_u64(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** Each conversion of a `T`, its copies put after a write to what they copied. */ @@ -3680,7 +3706,9 @@ export function converted$u8(data: rs.ByteSlice, out: rs.ByteVec): void { put$conversions_Value_u8(b, out); c = make$conversions_Value_u8(data, 16); put$conversions_Value_u8(c, out); - ({ v: byte(data, 17) }.v = 2); + const kept: rs.u8 = byte(data, 17); + ({ v: kept }.v = 2); + rs.byteVecPush(out, kept); } /** diff --git a/ts/fixtures/lang/conversions/src/conversions.rs b/ts/fixtures/lang/conversions/src/conversions.rs index 43ba3bf37..c7607bbc6 100644 --- a/ts/fixtures/lang/conversions/src/conversions.rs +++ b/ts/fixtures/lang/conversions/src/conversions.rs @@ -467,8 +467,11 @@ fn converted(data: &[u8], out: &mut Vec) { let q = std::ptr::from_mut(&mut c); unsafe { *q = T::make(data, 16) }; c.put(out); - // A box's pointer written through once, the only use of it, leaking the box. - unsafe { *Box::into_raw(Box::new(byte(data, 17))) = 2 }; + // A box's pointer written through once, the only use of it, leaking the box, which + // holds a copy of what it was made of. + let kept = byte(data, 17); + unsafe { *Box::into_raw(Box::new(kept)) = 2 }; + out.push(kept); } /// Boxes, which clone what they hold by its own `clone_from`: a box, one in a box, in an diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 2be49c939..eec980610 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -38,6 +38,8 @@ fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::st fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place +fixtures/reject/src/alloc.rs:joined_reference+6:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays +fixtures/reject/src/alloc.rs:joined_reference+6:66: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 70e19d0e8..9443763cc 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -42,6 +42,8 @@ fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::st fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place +fixtures/reject/src/alloc.rs:joined_reference+6:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays +fixtures/reject/src/alloc.rs:joined_reference+6:66: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 5b8161627..4987ab666 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -107,6 +107,18 @@ pub fn recast_reference(r: &u32) -> u32 { unsafe { (*p.add(1)).assume_init() + q.add(1).read() + *m.wrapping_add(1).read() } } +/// Pointers from a reference the body is given on one path of a branch, which JS keeps in +/// an array of one on that path: measured against another from it, which JS keeps in an +/// array of its own, and read one past it. +pub fn joined_reference(r: &u32, flag: bool) -> u32 { + let p = if flag { std::ptr::from_ref(r) } else { std::ptr::null() }; + let q = if flag { r as *const u32 } else { std::ptr::null() }; + if !flag { + return 0; + } + unsafe { p.offset_from(std::ptr::from_ref(r)) as u32 + q.add(1).read() } +} + /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 48b90ca21..d2da794bd 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -13,6 +13,7 @@ roots = [ "alloc::cell_raw_unknown", "alloc::collection_moved", "alloc::infallible_cell", + "alloc::joined_reference", "alloc::lazy_moved", "alloc::past_reference", "alloc::peeked_moved", diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index 8394c4c96..74b96b1c5 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -360,6 +360,9 @@ pub struct Fx<'m, 'b, 'a, 'tcx> { slots: IndexVec>, /// Where each local assigned once is assigned, by a statement. def_at: IndexVec>, + /// Whether `origin` is following the assignments of a pointer assigned more than once, + /// which it follows no deeper. + joining: std::cell::Cell, /// How many times each local is read, and assigned whole. uses: IndexVec, defs: IndexVec, @@ -522,6 +525,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { env, slots: IndexVec::from_elem_n(Slot::Unit, n), def_at: IndexVec::from_elem_n(None, n), + joining: std::cell::Cell::new(false), uses: IndexVec::from_elem_n(0, n), defs: IndexVec::from_elem_n(0, n), borrowed: IndexVec::from_elem_n(false, n), @@ -3943,7 +3947,8 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { } true } - // A member assigned to folds only to another slot, as `{ v: x }.v` does to `x`. + // A member assigned to folds only to the place an `rs.place` made here reads, as a + // value made here, such as `{ v: x }`'s, holds a copy of its own. Expr::Assign(l, r) => { let Expr::Member(base, key) = &mut **l else { return subst(l, name, value) || subst(r, name, value); @@ -3951,10 +3956,10 @@ fn subst(e: &mut Expr, name: &str, value: &Expr) -> bool { if !subst(base, name, value) { return subst(r, name, value); } - if let Some(folded) = folded_member(base, key) - && matches!(folded.unlocated(), Expr::Ident(_) | Expr::Member(..) | Expr::Index(..)) + if matches!(key, ts::Key::Name(key) if key == "v") + && let Some(place) = step::peel(base).place_read() { - **l = folded; + **l = place; } true } diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 564f29810..7462f0c9d 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -265,34 +265,74 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { /// Where the pointer `op` points, if the body shows it. fn origin(&self, op: &Operand<'tcx>) -> Option> { - let local = self.traced(op)?; - if let Some((bb, i)) = self.def_at[local] { - let rvalue = self.def_rvalue(local)?; - return match rvalue { - Rvalue::Use(op, _) - | Rvalue::Cast( - mir::CastKind::PtrToPtr - | mir::CastKind::PointerCoercion(ty::adjustment::PointerCoercion::MutToConstPointer, _), - op, - _, - ) if self.same_pointee(op, local) => self.origin(op), - Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_origin( - *place, - Location { - block: bb, - statement_index: i, - }, - ), - _ => None, - }; + let Some(local) = self.traced(op) else { + return self.joined_origin(op); + }; + match self.def_at[local] { + Some((block, statement_index)) => { + self.assigned_origin(local, self.def_rvalue(local)?, Location { block, statement_index }) + } + None => self.called_origin(self.def_block(local)?), } - let (def_id, gargs, args) = self.def_call(local)?; + } + + /// Where the pointer a body assigns more than once points, as far as a refusal needs: + /// in an array of one, if any assignment the body shows is to one, since JS keeps each + /// apart from every other array, at the index all of those give, if they agree. + fn joined_origin(&self, op: &Operand<'tcx>) -> Option> { + let local = op.place()?.as_local()?; + // A pointer moved in a loop is assigned from itself, which this follows no further. + if self.is_arg(local) || self.defs[local] < 2 || self.joining.replace(true) { + return None; + } + let mut lone = Vec::new(); + for (block, data) in self.mir.basic_blocks.iter_enumerated() { + for (statement_index, stmt) in data.statements.iter().enumerate() { + if let mir::StatementKind::Assign(assign) = &stmt.kind + && assign.0.as_local() == Some(local) + { + let at = Location { block, statement_index }; + lone.extend(self.assigned_origin(local, &assign.1, at)); + } + } + if let TerminatorKind::Call { destination, .. } = &data.terminator().kind + && destination.as_local() == Some(local) + { + lone.extend(self.called_origin(block)); + } + } + self.joining.set(false); + lone.retain(|o| matches!(o.array, Array::Lone(_))); + let first = *lone.first()?; + Some(Origin { + index: first.index.filter(|&i| lone.iter().all(|o| o.index == Some(i))), + ..first + }) + } + + /// Where the pointer `rvalue`, assigned to `local` at `at`, points, if the body shows it. + fn assigned_origin(&self, local: Local, rvalue: &Rvalue<'tcx>, at: Location) -> Option> { + match rvalue { + Rvalue::Use(op, _) + | Rvalue::Cast( + mir::CastKind::PtrToPtr + | mir::CastKind::PointerCoercion(ty::adjustment::PointerCoercion::MutToConstPointer, _), + op, + _, + ) if self.same_pointee(op, local) => self.origin(op), + Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_origin(*place, at), + _ => None, + } + } + + /// Where the pointer the call of std's ending `block` gives points, if the body shows it. + fn called_origin(&self, block: BasicBlock) -> Option> { + let (def_id, gargs, args) = self.call_at(block)?; let (owner, name) = library::key(self.tcx, def_id)?; match (owner.as_str(), name.as_str()) { ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") if crate::lower::pointer::from_reference(self.tcx, def_id, gargs) => { - let block = self.def_block(local)?; let at = Location { block, statement_index: self.mir.basic_blocks[block].statements.len(), @@ -318,7 +358,7 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { // The start of its own text's encoding, though a substring's bytes start past its // text's in Rust, so a move back before it is refused, as one off a copy is. ("str", "as_ptr") => Some(Origin { - array: Array::Text(self.def_block(local)?), + array: Array::Text(block), index: Some(0), }), _ => None, @@ -626,7 +666,19 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { GenericArgsRef<'tcx>, &[Spanned>], )> { - let bb = self.def_block(local)?; + self.call_at(self.def_block(local)?) + } + + /// The function, its generic arguments, and its arguments, of the call of std's that + /// ends `bb`. + fn call_at( + &self, + bb: BasicBlock, + ) -> Option<( + rustc_hir::def_id::DefId, + GenericArgsRef<'tcx>, + &[Spanned>], + )> { let TerminatorKind::Call { func, args, .. } = &self.mir.basic_blocks[bb].terminator().kind else { return None; }; From 51149824b0257a09130b3baf00c943134a1c2ba1 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:40:07 -0800 Subject: [PATCH 33/45] fix(ts): follow every assignment of a pointer to the arrays of one it may reach origin.rs walks each assignment of a pointer assigned more than once, through nested joins, with a stack of the pointers on the way for cycles. `origin` stays the one place a chain of single assignments shows; the refusals also check each array of one any joined path may make, so `offset_from` between a branch's pointers from one reference and another, and through a branch over a branch's, are refused with spans (reject's joined_twice). Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 2 + ts/fixtures/reject/expected.txt | 2 + ts/fixtures/reject/src/alloc.rs | 11 ++ ts/fixtures/reject/ts.toml | 1 + ts/src/lower/mir/mod.rs | 4 - ts/src/lower/mir/origin.rs | 272 ++++++++++++++++++++------------ 6 files changed, 185 insertions(+), 107 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index eec980610..01cbe8135 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -40,6 +40,8 @@ fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::v fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:joined_reference+6:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays fixtures/reject/src/alloc.rs:joined_reference+6:66: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:joined_twice+5:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays +fixtures/reject/src/alloc.rs:joined_twice+5:44: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 9443763cc..d9cb0871b 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -44,6 +44,8 @@ fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::v fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:joined_reference+6:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays fixtures/reject/src/alloc.rs:joined_reference+6:66: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust +fixtures/reject/src/alloc.rs:joined_twice+5:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays +fixtures/reject/src/alloc.rs:joined_twice+5:44: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 4987ab666..367b1ce41 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -119,6 +119,17 @@ pub fn joined_reference(r: &u32, flag: bool) -> u32 { unsafe { p.offset_from(std::ptr::from_ref(r)) as u32 + q.add(1).read() } } +/// Pointers from a reference the body is given, through a branch whose other path makes +/// another from it, and through a branch over such a branch: each `offset_from` measures +/// pointers JS keeps in different arrays on some path, where Rust's are one place. +pub fn joined_twice(r: &u32, a: bool, b: bool) -> isize { + let q = r as *const u32; + let p = if a { q } else { r as *const u32 }; + let s = if a { r as *const u32 } else { std::ptr::null() }; + let t = if b { s } else { std::ptr::null() }; + unsafe { p.offset_from(q) + if a && b { t.offset_from(r as *const u32) } else { 0 } } +} + /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index d2da794bd..4526e05f9 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -14,6 +14,7 @@ roots = [ "alloc::collection_moved", "alloc::infallible_cell", "alloc::joined_reference", + "alloc::joined_twice", "alloc::lazy_moved", "alloc::past_reference", "alloc::peeked_moved", diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index 74b96b1c5..67e1298b3 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -360,9 +360,6 @@ pub struct Fx<'m, 'b, 'a, 'tcx> { slots: IndexVec>, /// Where each local assigned once is assigned, by a statement. def_at: IndexVec>, - /// Whether `origin` is following the assignments of a pointer assigned more than once, - /// which it follows no deeper. - joining: std::cell::Cell, /// How many times each local is read, and assigned whole. uses: IndexVec, defs: IndexVec, @@ -525,7 +522,6 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { env, slots: IndexVec::from_elem_n(Slot::Unit, n), def_at: IndexVec::from_elem_n(None, n), - joining: std::cell::Cell::new(false), uses: IndexVec::from_elem_n(0, n), defs: IndexVec::from_elem_n(0, n), borrowed: IndexVec::from_elem_n(false, n), diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index 7462f0c9d..c1792e1b1 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -102,29 +102,34 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { )) } ("ptr" | "NonNull", "offset_from" | "offset_from_unsigned") => { - let (a, b) = (self.origin(arg(0)?)?, self.origin(arg(1)?)?); - if let (Array::Text(_), Array::Text(_)) = (a.array, b.array) { - return (a.array != b.array).then(|| { + let b = self.possible(arg(1)?); + let apart = |a: Origin<'tcx>, b: Origin<'tcx>| { + match (a.array, b.array) { + (Array::Text(_), Array::Text(_)) => (a.array != b.array).then(|| { "`offset_from` between pointers of two `str::as_ptr` calls, each into an encoding of the text of its own, which JS keeps in different arrays".into() - }); + }), + _ => self.apart(a.array, b.array).then(|| { + format!( + "`offset_from` between a pointer into {} and one into {}, which JS keeps in different arrays", + a.array.what(), + b.array.what() + ) + }), } - self.apart(a.array, b.array).then(|| { - format!( - "`offset_from` between a pointer into {} and one into {}, which JS keeps in different arrays", - a.array.what(), - b.array.what() - ) - }) - } - ("ptr" | "NonNull", "add" | "sub" | "offset") => { - let o = self.moved(self.origin(arg(0)?)?, name.as_str(), arg(1)?); - out(o, None, "a pointer") + }; + self.possible(arg(0)?) + .into_iter() + .find_map(|a| b.iter().find_map(|&b| apart(a, b))) } + ("ptr" | "NonNull", "add" | "sub" | "offset") => self + .possible(arg(0)?) + .into_iter() + .find_map(|o| out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer")), // A wrapping offset may leave any other array while nothing reads through it. - ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { - let o = self.lone_origin(arg(0)?)?; - out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer") - } + ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => self + .lone_origins(arg(0)?) + .into_iter() + .find_map(|o| out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer")), // An access through a pointer, which the body shows one element at: both of a // swap's, and else the first argument, as the second is the value written. ( @@ -134,20 +139,21 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ) => [Some(arg(0)?), arg(1).filter(|_| name == "swap")] .into_iter() .flatten() - .find_map(|p| self.access(self.lone_origin(p)?)), + .flat_map(|p| self.lone_origins(p)) + .find_map(|o| self.access(o)), ("raw", "from_raw_parts" | "from_raw_parts_mut") | ("ptr", "slice_from_raw_parts" | "slice_from_raw_parts_mut") | ("NonNull", "slice_from_raw_parts") => { - let o = self.origin(arg(0)?)?; + let len = self.const_int(arg(1)?); // A raw pointer to a slice of any other array reads nothing until a read // through it. - if owner != "raw" && !matches!(o.array, Array::Lone(_)) { - return None; - } - match self.const_int(arg(1)?) { - Some(n) => out(o, Some(n), "a slice"), - None => unfixed(o, "a slice of a length"), - } + self.possible(arg(0)?) + .into_iter() + .filter(|o| owner == "raw" || matches!(o.array, Array::Lone(_))) + .find_map(|o| match len { + Some(n) => out(o, Some(n), "a slice"), + None => unfixed(o, "a slice of a length"), + }) } ( "ptr", @@ -160,20 +166,20 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ) | ("NonNull", "copy_to" | "copy_to_nonoverlapping" | "copy_from" | "copy_from_nonoverlapping") => { let n = self.const_int(arg(2)?); - [arg(0)?, arg(1)?].into_iter().find_map(|p| { - let o = self.origin(p)?; - match n { + [arg(0)?, arg(1)?] + .into_iter() + .flat_map(|p| self.possible(p)) + .find_map(|o| match n { Some(_) => out(o, n, "a copy"), None => unfixed(o, "a copy of a count"), - } - }) + }) } ("ptr" | "NonNull", "write_bytes") => { - let o = self.origin(arg(0)?)?; - match self.const_int(arg(2)?) { + let n = self.const_int(arg(2)?); + self.possible(arg(0)?).into_iter().find_map(|o| match n { Some(n) => out(o, Some(n), "a write"), None => unfixed(o, "a write of a count"), - } + }) } _ => None, } @@ -192,7 +198,8 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { if self.b.t.zero_sized(pointee) || self.b.t.pointee(pointee) != Pointee::Place { return None; } - self.access(self.lone_origin(&Operand::Copy(mir::Place::from(place.local)))?) + let pointer = Operand::Copy(mir::Place::from(place.local)); + self.lone_origins(&pointer).into_iter().find_map(|o| self.access(o)) } /// Why an access of the one element a pointer at `o` reads or writes reaches out of its @@ -203,9 +210,11 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { (o.index? == len).then(|| Self::out(o, Some(1), "a read or write"))? } - /// Where the pointer `op` points, if the body shows it in an array of one. - fn lone_origin(&self, op: &Operand<'tcx>) -> Option> { - self.origin(op).filter(|o| matches!(o.array, Array::Lone(_))) + /// Each array of one the pointer `op` may point into, as [`Self::possible`] finds them. + fn lone_origins(&self, op: &Operand<'tcx>) -> Vec> { + let mut lone = self.possible(op); + lone.retain(|o| matches!(o.array, Array::Lone(_))); + lone } /// Why an offset or a count the body doesn't fix, from `o`, may reach out of it: only @@ -263,55 +272,85 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { }) } - /// Where the pointer `op` points, if the body shows it. + /// Where the pointer `op` points, if the body shows it: one place, through pointers each + /// assigned once. fn origin(&self, op: &Operand<'tcx>) -> Option> { - let Some(local) = self.traced(op) else { - return self.joined_origin(op); - }; - match self.def_at[local] { - Some((block, statement_index)) => { - self.assigned_origin(local, self.def_rvalue(local)?, Location { block, statement_index }) - } - None => self.called_origin(self.def_block(local)?), + match self.paths(op, &mut Vec::new()).as_slice() { + &[(o, false)] => Some(o), + _ => None, } } - /// Where the pointer a body assigns more than once points, as far as a refusal needs: - /// in an array of one, if any assignment the body shows is to one, since JS keeps each - /// apart from every other array, at the index all of those give, if they agree. - fn joined_origin(&self, op: &Operand<'tcx>) -> Option> { - let local = op.place()?.as_local()?; - // A pointer moved in a loop is assigned from itself, which this follows no further. - if self.is_arg(local) || self.defs[local] < 2 || self.joining.replace(true) { - return None; + /// Where the pointer `op` may point, as a refusal needs it: the one place + /// [`Self::origin`] finds, or each array of one an assignment the body shows may make + /// it point into, through pointers assigned more than once, as on one path of a branch. + /// JS keeps each such array apart from every other on that path, but another place + /// such an assignment may make is left to the runtime, as Rust may never reach it. + fn possible(&self, op: &Operand<'tcx>) -> Vec> { + let paths = self.paths(op, &mut Vec::new()); + let kept = paths + .into_iter() + .filter(|&(o, joined)| !joined || matches!(o.array, Array::Lone(_))); + kept.map(|(o, _)| o).collect() + } + + /// Each place the body shows the pointer `op` may point at, and whether a pointer + /// assigned more than once on the way there makes it one of several. `seen` holds the + /// pointers on the way, whose cycle another assignment already reaches past. + fn paths(&self, op: &Operand<'tcx>, seen: &mut Vec) -> Vec<(Origin<'tcx>, bool)> { + let Some(local) = op.place().and_then(|p| p.as_local()) else { + return Vec::new(); + }; + if self.is_arg(local) || seen.contains(&local) { + return Vec::new(); } - let mut lone = Vec::new(); + seen.push(local); + let paths = match self.defs[local] { + 1 => match self.def_at[local] { + Some((block, statement_index)) => self.def_rvalue(local).and_then(|rvalue| { + let at = Location { block, statement_index }; + self.assigned_paths(local, rvalue, at, seen) + }), + None => self.def_block(local).and_then(|block| self.called_paths(block, seen)), + }, + _ => Some(self.joined_paths(local, seen)), + }; + seen.pop(); + paths.unwrap_or_default() + } + + /// Each place the pointer `local`, assigned more than once, may point at, by any of its + /// assignments. + fn joined_paths(&self, local: Local, seen: &mut Vec) -> Vec<(Origin<'tcx>, bool)> { + let mut paths = Vec::new(); for (block, data) in self.mir.basic_blocks.iter_enumerated() { for (statement_index, stmt) in data.statements.iter().enumerate() { if let mir::StatementKind::Assign(assign) = &stmt.kind && assign.0.as_local() == Some(local) + && !self.noops.contains(&(block, statement_index)) + && !self.in_ignored(stmt.source_info.span) { let at = Location { block, statement_index }; - lone.extend(self.assigned_origin(local, &assign.1, at)); + paths.extend(self.assigned_paths(local, &assign.1, at, seen).unwrap_or_default()); } } if let TerminatorKind::Call { destination, .. } = &data.terminator().kind && destination.as_local() == Some(local) { - lone.extend(self.called_origin(block)); + paths.extend(self.called_paths(block, seen).unwrap_or_default()); } } - self.joining.set(false); - lone.retain(|o| matches!(o.array, Array::Lone(_))); - let first = *lone.first()?; - Some(Origin { - index: first.index.filter(|&i| lone.iter().all(|o| o.index == Some(i))), - ..first - }) + paths.into_iter().map(|(o, _)| (o, true)).collect() } - /// Where the pointer `rvalue`, assigned to `local` at `at`, points, if the body shows it. - fn assigned_origin(&self, local: Local, rvalue: &Rvalue<'tcx>, at: Location) -> Option> { + /// Where the pointer `rvalue`, assigned to `local` at `at`, may point. + fn assigned_paths( + &self, + local: Local, + rvalue: &Rvalue<'tcx>, + at: Location, + seen: &mut Vec, + ) -> Option, bool)>> { match rvalue { Rvalue::Use(op, _) | Rvalue::Cast( @@ -319,16 +358,22 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { | mir::CastKind::PointerCoercion(ty::adjustment::PointerCoercion::MutToConstPointer, _), op, _, - ) if self.same_pointee(op, local) => self.origin(op), - Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_origin(*place, at), + ) if self.same_pointee(op, local) => Some(self.paths(op, seen)), + Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_paths(*place, at, seen), _ => None, } } - /// Where the pointer the call of std's ending `block` gives points, if the body shows it. - fn called_origin(&self, block: BasicBlock) -> Option> { + /// Where the pointer the call of std's ending `block` gives may point. + fn called_paths(&self, block: BasicBlock, seen: &mut Vec) -> Option, bool)>> { let (def_id, gargs, args) = self.call_at(block)?; let (owner, name) = library::key(self.tcx, def_id)?; + let moved = |paths: Vec<(Origin<'tcx>, bool)>| { + let moved = paths + .into_iter() + .map(|(o, joined)| (self.moved(o, name.as_str(), &args[1].node), joined)); + Some(moved.collect()) + }; match (owner.as_str(), name.as_str()) { ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") if crate::lower::pointer::from_reference(self.tcx, def_id, gargs) => @@ -337,40 +382,49 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { block, statement_index: self.mir.basic_blocks[block].statements.len(), }; - self.reference_origin(args[0].node.place()?, at) - } - ("ptr" | "NonNull", "add" | "sub" | "offset") => { - Some(self.moved(self.origin(&args[0].node)?, name.as_str(), &args[1].node)) + self.reference_paths(args[0].node.place()?, at, seen) } + ("ptr" | "NonNull", "add" | "sub" | "offset") => moved(self.paths(&args[0].node, seen)), // Only an array of one's, which every wrapping offset must stay within. ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { - Some(self.moved(self.lone_origin(&args[0].node)?, name.as_str(), &args[1].node)) + let mut lone = self.paths(&args[0].node, seen); + lone.retain(|(o, _)| matches!(o.array, Array::Lone(_))); + moved(lone) } ("NonNull", "as_ptr" | "new_unchecked") | ("ptr" | "NonNull", "cast_const" | "cast_mut") => { - self.origin(&args[0].node) + Some(self.paths(&args[0].node, seen)) } ("ptr" | "NonNull", "cast") if self.b.t.uninit_inner(gargs.type_at(0)) == self.b.t.uninit_inner(gargs.type_at(1)) => { - self.origin(&args[0].node) + Some(self.paths(&args[0].node, seen)) } - ("slice" | "Vec", "as_ptr" | "as_mut_ptr") => self.start(&args[0].node), + ("slice" | "Vec", "as_ptr" | "as_mut_ptr") => Some(vec![(self.start(&args[0].node)?, false)]), // The start of its own text's encoding, though a substring's bytes start past its // text's in Rust, so a move back before it is refused, as one off a copy is. - ("str", "as_ptr") => Some(Origin { - array: Array::Text(block), - index: Some(0), - }), + ("str", "as_ptr") => Some(vec![( + Origin { + array: Array::Text(block), + index: Some(0), + }, + false, + )]), _ => None, } } - /// Where a raw pointer to `place`, made at `at`, points, if the body shows it. - fn raw_origin(&self, place: mir::Place<'tcx>, at: Location) -> Option> { + /// Where a raw pointer to `place`, made at `at`, may point. + fn raw_paths( + &self, + place: mir::Place<'tcx>, + at: Location, + seen: &mut Vec, + ) -> Option, bool)>> { + let one = |o: Origin<'tcx>| Some(vec![(o, false)]); if let Some(local) = self.raw_byte(place) && matches!(self.slots[local], Slot::Byte(_)) { - return Some(Origin { + return one(Origin { array: Array::Byte(local), index: Some(0), }); @@ -378,36 +432,48 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { match place.projection.as_slice() { // `&raw mut *p` is `p`. [ProjectionElem::Deref] if self.mir.local_decls[place.local].ty.is_raw_ptr() => { - self.origin(&Operand::Copy(mir::Place::from(place.local))) + Some(self.paths(&Operand::Copy(mir::Place::from(place.local)), seen)) } - [rest @ .., ProjectionElem::Deref] => self.reference_origin( + [rest @ .., ProjectionElem::Deref] => self.reference_paths( mir::Place { local: place.local, projection: self.tcx.mk_place_elems(rest), }, at, + seen, ), [ array @ .., ProjectionElem::Index(_) | ProjectionElem::ConstantIndex { .. }, - ] => (!place.projection.contains(&ProjectionElem::Deref)).then(|| Origin { - array: Array::Owned(mir::Place { - local: place.local, - projection: self.tcx.mk_place_elems(array), - }), - index: None, + ] => (!place.projection.contains(&ProjectionElem::Deref)).then(|| { + vec![( + Origin { + array: Array::Owned(mir::Place { + local: place.local, + projection: self.tcx.mk_place_elems(array), + }), + index: None, + }, + false, + )] }), - _ => Some(Origin { + _ => one(Origin { array: Array::Lone(at), index: Some(0), }), } } - /// Where a raw pointer to what the reference `r` points at, made at `at`, points, as + /// Where a raw pointer to what the reference `r` points at, made at `at`, may point, as /// [`Self::behind_reference`] makes it: where the body shows `r` borrowed, and else at /// a place of its own, or, where `r` is an `rs.Place`, where that points. - fn reference_origin(&self, r: mir::Place<'tcx>, at: Location) -> Option> { + fn reference_paths( + &self, + r: mir::Place<'tcx>, + at: Location, + seen: &mut Vec, + ) -> Option, bool)>> { + let one = |o: Origin<'tcx>| Some(vec![(o, false)]); let ty = r.ty(&self.mir.local_decls, self.tcx).ty; // A reference to an `Arc`'s value points at that value's one place. if !ty.is_ref() || self.arc_of(&Operand::Copy(r)).is_some() { @@ -416,7 +482,7 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let pointee = self.tcx.mk_place_deref(r); let borrowed = self.through_aliases(pointee); if borrowed != pointee { - return self.raw_origin(borrowed, at); + return self.raw_paths(borrowed, at, seen); } if self.indexed(r).is_some() { let (def_id, _, args) = self.def_call(r.as_local()?)?; @@ -428,18 +494,18 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { return None; } let o = self.start(&args[0].node)?; - return Some(Origin { + return one(Origin { index: o.index.zip(self.const_int(&args[1].node)).map(|(i, n)| i + n), ..o }); } if let Some(p) = self.reborrowed_pointer(r) { - return self.origin(&Operand::Copy(p)); + return Some(self.paths(&Operand::Copy(p), seen)); } if self.b.t.place_pointee(ty).is_some() { return None; } - Some(Origin { + one(Origin { array: Array::Lone(at), index: Some(0), }) From 3be3d337d86dc8bc83414c20b391100376ce41a5 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:38:05 -0800 Subject: [PATCH 34/45] revert(ts): drop the join tracking of pointers from a reference Reverts 51149824b and the joined_reference half of 3281e7d38, per the maintainer's design checkpoint: a pointer from a reference that passes through a branch or loop fails loud at run time, as main's ptr::from_ref and &raw const of a local already do. The cast bounds check, the `{ v: x }.v` target fold, the uninitialized-cell copy, and the fixture leak fixes stay. Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 4 - ts/fixtures/reject/expected.txt | 4 - ts/fixtures/reject/src/alloc.rs | 23 --- ts/fixtures/reject/ts.toml | 2 - ts/src/lower/mir/origin.rs | 306 ++++++++++---------------------- 5 files changed, 94 insertions(+), 245 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 01cbe8135..2be49c939 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -38,10 +38,6 @@ fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::st fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place -fixtures/reject/src/alloc.rs:joined_reference+6:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays -fixtures/reject/src/alloc.rs:joined_reference+6:66: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust -fixtures/reject/src/alloc.rs:joined_twice+5:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays -fixtures/reject/src/alloc.rs:joined_twice+5:44: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index d9cb0871b..70e19d0e8 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -42,10 +42,6 @@ fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::st fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place -fixtures/reject/src/alloc.rs:joined_reference+6:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays -fixtures/reject/src/alloc.rs:joined_reference+6:66: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust -fixtures/reject/src/alloc.rs:joined_twice+5:13: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays -fixtures/reject/src/alloc.rs:joined_twice+5:44: `offset_from` between a pointer into a value in no array and one into a value in no array, which JS keeps in different arrays fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 367b1ce41..5b8161627 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -107,29 +107,6 @@ pub fn recast_reference(r: &u32) -> u32 { unsafe { (*p.add(1)).assume_init() + q.add(1).read() + *m.wrapping_add(1).read() } } -/// Pointers from a reference the body is given on one path of a branch, which JS keeps in -/// an array of one on that path: measured against another from it, which JS keeps in an -/// array of its own, and read one past it. -pub fn joined_reference(r: &u32, flag: bool) -> u32 { - let p = if flag { std::ptr::from_ref(r) } else { std::ptr::null() }; - let q = if flag { r as *const u32 } else { std::ptr::null() }; - if !flag { - return 0; - } - unsafe { p.offset_from(std::ptr::from_ref(r)) as u32 + q.add(1).read() } -} - -/// Pointers from a reference the body is given, through a branch whose other path makes -/// another from it, and through a branch over such a branch: each `offset_from` measures -/// pointers JS keeps in different arrays on some path, where Rust's are one place. -pub fn joined_twice(r: &u32, a: bool, b: bool) -> isize { - let q = r as *const u32; - let p = if a { q } else { r as *const u32 }; - let s = if a { r as *const u32 } else { std::ptr::null() }; - let t = if b { s } else { std::ptr::null() }; - unsafe { p.offset_from(q) + if a && b { t.offset_from(r as *const u32) } else { 0 } } -} - /// A pointer from a reference the body is given, which JS keeps in no array, moved past /// it. pub fn past_reference(r: &u32) -> u32 { diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 4526e05f9..48b90ca21 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -13,8 +13,6 @@ roots = [ "alloc::cell_raw_unknown", "alloc::collection_moved", "alloc::infallible_cell", - "alloc::joined_reference", - "alloc::joined_twice", "alloc::lazy_moved", "alloc::past_reference", "alloc::peeked_moved", diff --git a/ts/src/lower/mir/origin.rs b/ts/src/lower/mir/origin.rs index c1792e1b1..564f29810 100644 --- a/ts/src/lower/mir/origin.rs +++ b/ts/src/lower/mir/origin.rs @@ -102,34 +102,29 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { )) } ("ptr" | "NonNull", "offset_from" | "offset_from_unsigned") => { - let b = self.possible(arg(1)?); - let apart = |a: Origin<'tcx>, b: Origin<'tcx>| { - match (a.array, b.array) { - (Array::Text(_), Array::Text(_)) => (a.array != b.array).then(|| { + let (a, b) = (self.origin(arg(0)?)?, self.origin(arg(1)?)?); + if let (Array::Text(_), Array::Text(_)) = (a.array, b.array) { + return (a.array != b.array).then(|| { "`offset_from` between pointers of two `str::as_ptr` calls, each into an encoding of the text of its own, which JS keeps in different arrays".into() - }), - _ => self.apart(a.array, b.array).then(|| { - format!( - "`offset_from` between a pointer into {} and one into {}, which JS keeps in different arrays", - a.array.what(), - b.array.what() - ) - }), + }); } - }; - self.possible(arg(0)?) - .into_iter() - .find_map(|a| b.iter().find_map(|&b| apart(a, b))) + self.apart(a.array, b.array).then(|| { + format!( + "`offset_from` between a pointer into {} and one into {}, which JS keeps in different arrays", + a.array.what(), + b.array.what() + ) + }) + } + ("ptr" | "NonNull", "add" | "sub" | "offset") => { + let o = self.moved(self.origin(arg(0)?)?, name.as_str(), arg(1)?); + out(o, None, "a pointer") } - ("ptr" | "NonNull", "add" | "sub" | "offset") => self - .possible(arg(0)?) - .into_iter() - .find_map(|o| out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer")), // A wrapping offset may leave any other array while nothing reads through it. - ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => self - .lone_origins(arg(0)?) - .into_iter() - .find_map(|o| out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer")), + ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { + let o = self.lone_origin(arg(0)?)?; + out(self.moved(o, name.as_str(), arg(1)?), None, "a pointer") + } // An access through a pointer, which the body shows one element at: both of a // swap's, and else the first argument, as the second is the value written. ( @@ -139,21 +134,20 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ) => [Some(arg(0)?), arg(1).filter(|_| name == "swap")] .into_iter() .flatten() - .flat_map(|p| self.lone_origins(p)) - .find_map(|o| self.access(o)), + .find_map(|p| self.access(self.lone_origin(p)?)), ("raw", "from_raw_parts" | "from_raw_parts_mut") | ("ptr", "slice_from_raw_parts" | "slice_from_raw_parts_mut") | ("NonNull", "slice_from_raw_parts") => { - let len = self.const_int(arg(1)?); + let o = self.origin(arg(0)?)?; // A raw pointer to a slice of any other array reads nothing until a read // through it. - self.possible(arg(0)?) - .into_iter() - .filter(|o| owner == "raw" || matches!(o.array, Array::Lone(_))) - .find_map(|o| match len { - Some(n) => out(o, Some(n), "a slice"), - None => unfixed(o, "a slice of a length"), - }) + if owner != "raw" && !matches!(o.array, Array::Lone(_)) { + return None; + } + match self.const_int(arg(1)?) { + Some(n) => out(o, Some(n), "a slice"), + None => unfixed(o, "a slice of a length"), + } } ( "ptr", @@ -166,20 +160,20 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { ) | ("NonNull", "copy_to" | "copy_to_nonoverlapping" | "copy_from" | "copy_from_nonoverlapping") => { let n = self.const_int(arg(2)?); - [arg(0)?, arg(1)?] - .into_iter() - .flat_map(|p| self.possible(p)) - .find_map(|o| match n { + [arg(0)?, arg(1)?].into_iter().find_map(|p| { + let o = self.origin(p)?; + match n { Some(_) => out(o, n, "a copy"), None => unfixed(o, "a copy of a count"), - }) + } + }) } ("ptr" | "NonNull", "write_bytes") => { - let n = self.const_int(arg(2)?); - self.possible(arg(0)?).into_iter().find_map(|o| match n { + let o = self.origin(arg(0)?)?; + match self.const_int(arg(2)?) { Some(n) => out(o, Some(n), "a write"), None => unfixed(o, "a write of a count"), - }) + } } _ => None, } @@ -198,8 +192,7 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { if self.b.t.zero_sized(pointee) || self.b.t.pointee(pointee) != Pointee::Place { return None; } - let pointer = Operand::Copy(mir::Place::from(place.local)); - self.lone_origins(&pointer).into_iter().find_map(|o| self.access(o)) + self.access(self.lone_origin(&Operand::Copy(mir::Place::from(place.local)))?) } /// Why an access of the one element a pointer at `o` reads or writes reaches out of its @@ -210,11 +203,9 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { (o.index? == len).then(|| Self::out(o, Some(1), "a read or write"))? } - /// Each array of one the pointer `op` may point into, as [`Self::possible`] finds them. - fn lone_origins(&self, op: &Operand<'tcx>) -> Vec> { - let mut lone = self.possible(op); - lone.retain(|o| matches!(o.array, Array::Lone(_))); - lone + /// Where the pointer `op` points, if the body shows it in an array of one. + fn lone_origin(&self, op: &Operand<'tcx>) -> Option> { + self.origin(op).filter(|o| matches!(o.array, Array::Lone(_))) } /// Why an offset or a count the body doesn't fix, from `o`, may reach out of it: only @@ -272,159 +263,74 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { }) } - /// Where the pointer `op` points, if the body shows it: one place, through pointers each - /// assigned once. + /// Where the pointer `op` points, if the body shows it. fn origin(&self, op: &Operand<'tcx>) -> Option> { - match self.paths(op, &mut Vec::new()).as_slice() { - &[(o, false)] => Some(o), - _ => None, - } - } - - /// Where the pointer `op` may point, as a refusal needs it: the one place - /// [`Self::origin`] finds, or each array of one an assignment the body shows may make - /// it point into, through pointers assigned more than once, as on one path of a branch. - /// JS keeps each such array apart from every other on that path, but another place - /// such an assignment may make is left to the runtime, as Rust may never reach it. - fn possible(&self, op: &Operand<'tcx>) -> Vec> { - let paths = self.paths(op, &mut Vec::new()); - let kept = paths - .into_iter() - .filter(|&(o, joined)| !joined || matches!(o.array, Array::Lone(_))); - kept.map(|(o, _)| o).collect() - } - - /// Each place the body shows the pointer `op` may point at, and whether a pointer - /// assigned more than once on the way there makes it one of several. `seen` holds the - /// pointers on the way, whose cycle another assignment already reaches past. - fn paths(&self, op: &Operand<'tcx>, seen: &mut Vec) -> Vec<(Origin<'tcx>, bool)> { - let Some(local) = op.place().and_then(|p| p.as_local()) else { - return Vec::new(); - }; - if self.is_arg(local) || seen.contains(&local) { - return Vec::new(); - } - seen.push(local); - let paths = match self.defs[local] { - 1 => match self.def_at[local] { - Some((block, statement_index)) => self.def_rvalue(local).and_then(|rvalue| { - let at = Location { block, statement_index }; - self.assigned_paths(local, rvalue, at, seen) - }), - None => self.def_block(local).and_then(|block| self.called_paths(block, seen)), - }, - _ => Some(self.joined_paths(local, seen)), - }; - seen.pop(); - paths.unwrap_or_default() - } - - /// Each place the pointer `local`, assigned more than once, may point at, by any of its - /// assignments. - fn joined_paths(&self, local: Local, seen: &mut Vec) -> Vec<(Origin<'tcx>, bool)> { - let mut paths = Vec::new(); - for (block, data) in self.mir.basic_blocks.iter_enumerated() { - for (statement_index, stmt) in data.statements.iter().enumerate() { - if let mir::StatementKind::Assign(assign) = &stmt.kind - && assign.0.as_local() == Some(local) - && !self.noops.contains(&(block, statement_index)) - && !self.in_ignored(stmt.source_info.span) - { - let at = Location { block, statement_index }; - paths.extend(self.assigned_paths(local, &assign.1, at, seen).unwrap_or_default()); - } - } - if let TerminatorKind::Call { destination, .. } = &data.terminator().kind - && destination.as_local() == Some(local) - { - paths.extend(self.called_paths(block, seen).unwrap_or_default()); - } - } - paths.into_iter().map(|(o, _)| (o, true)).collect() - } - - /// Where the pointer `rvalue`, assigned to `local` at `at`, may point. - fn assigned_paths( - &self, - local: Local, - rvalue: &Rvalue<'tcx>, - at: Location, - seen: &mut Vec, - ) -> Option, bool)>> { - match rvalue { - Rvalue::Use(op, _) - | Rvalue::Cast( - mir::CastKind::PtrToPtr - | mir::CastKind::PointerCoercion(ty::adjustment::PointerCoercion::MutToConstPointer, _), - op, - _, - ) if self.same_pointee(op, local) => Some(self.paths(op, seen)), - Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_paths(*place, at, seen), - _ => None, + let local = self.traced(op)?; + if let Some((bb, i)) = self.def_at[local] { + let rvalue = self.def_rvalue(local)?; + return match rvalue { + Rvalue::Use(op, _) + | Rvalue::Cast( + mir::CastKind::PtrToPtr + | mir::CastKind::PointerCoercion(ty::adjustment::PointerCoercion::MutToConstPointer, _), + op, + _, + ) if self.same_pointee(op, local) => self.origin(op), + Rvalue::RawPtr(mir::RawPtrKind::Const | mir::RawPtrKind::Mut, place) => self.raw_origin( + *place, + Location { + block: bb, + statement_index: i, + }, + ), + _ => None, + }; } - } - - /// Where the pointer the call of std's ending `block` gives may point. - fn called_paths(&self, block: BasicBlock, seen: &mut Vec) -> Option, bool)>> { - let (def_id, gargs, args) = self.call_at(block)?; + let (def_id, gargs, args) = self.def_call(local)?; let (owner, name) = library::key(self.tcx, def_id)?; - let moved = |paths: Vec<(Origin<'tcx>, bool)>| { - let moved = paths - .into_iter() - .map(|(o, joined)| (self.moved(o, name.as_str(), &args[1].node), joined)); - Some(moved.collect()) - }; match (owner.as_str(), name.as_str()) { ("ptr", "from_ref" | "from_mut") | ("From", "from") | ("Into", "into") if crate::lower::pointer::from_reference(self.tcx, def_id, gargs) => { + let block = self.def_block(local)?; let at = Location { block, statement_index: self.mir.basic_blocks[block].statements.len(), }; - self.reference_paths(args[0].node.place()?, at, seen) + self.reference_origin(args[0].node.place()?, at) + } + ("ptr" | "NonNull", "add" | "sub" | "offset") => { + Some(self.moved(self.origin(&args[0].node)?, name.as_str(), &args[1].node)) } - ("ptr" | "NonNull", "add" | "sub" | "offset") => moved(self.paths(&args[0].node, seen)), // Only an array of one's, which every wrapping offset must stay within. ("ptr" | "NonNull", "wrapping_add" | "wrapping_sub" | "wrapping_offset") => { - let mut lone = self.paths(&args[0].node, seen); - lone.retain(|(o, _)| matches!(o.array, Array::Lone(_))); - moved(lone) + Some(self.moved(self.lone_origin(&args[0].node)?, name.as_str(), &args[1].node)) } ("NonNull", "as_ptr" | "new_unchecked") | ("ptr" | "NonNull", "cast_const" | "cast_mut") => { - Some(self.paths(&args[0].node, seen)) + self.origin(&args[0].node) } ("ptr" | "NonNull", "cast") if self.b.t.uninit_inner(gargs.type_at(0)) == self.b.t.uninit_inner(gargs.type_at(1)) => { - Some(self.paths(&args[0].node, seen)) + self.origin(&args[0].node) } - ("slice" | "Vec", "as_ptr" | "as_mut_ptr") => Some(vec![(self.start(&args[0].node)?, false)]), + ("slice" | "Vec", "as_ptr" | "as_mut_ptr") => self.start(&args[0].node), // The start of its own text's encoding, though a substring's bytes start past its // text's in Rust, so a move back before it is refused, as one off a copy is. - ("str", "as_ptr") => Some(vec![( - Origin { - array: Array::Text(block), - index: Some(0), - }, - false, - )]), + ("str", "as_ptr") => Some(Origin { + array: Array::Text(self.def_block(local)?), + index: Some(0), + }), _ => None, } } - /// Where a raw pointer to `place`, made at `at`, may point. - fn raw_paths( - &self, - place: mir::Place<'tcx>, - at: Location, - seen: &mut Vec, - ) -> Option, bool)>> { - let one = |o: Origin<'tcx>| Some(vec![(o, false)]); + /// Where a raw pointer to `place`, made at `at`, points, if the body shows it. + fn raw_origin(&self, place: mir::Place<'tcx>, at: Location) -> Option> { if let Some(local) = self.raw_byte(place) && matches!(self.slots[local], Slot::Byte(_)) { - return one(Origin { + return Some(Origin { array: Array::Byte(local), index: Some(0), }); @@ -432,48 +338,36 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { match place.projection.as_slice() { // `&raw mut *p` is `p`. [ProjectionElem::Deref] if self.mir.local_decls[place.local].ty.is_raw_ptr() => { - Some(self.paths(&Operand::Copy(mir::Place::from(place.local)), seen)) + self.origin(&Operand::Copy(mir::Place::from(place.local))) } - [rest @ .., ProjectionElem::Deref] => self.reference_paths( + [rest @ .., ProjectionElem::Deref] => self.reference_origin( mir::Place { local: place.local, projection: self.tcx.mk_place_elems(rest), }, at, - seen, ), [ array @ .., ProjectionElem::Index(_) | ProjectionElem::ConstantIndex { .. }, - ] => (!place.projection.contains(&ProjectionElem::Deref)).then(|| { - vec![( - Origin { - array: Array::Owned(mir::Place { - local: place.local, - projection: self.tcx.mk_place_elems(array), - }), - index: None, - }, - false, - )] + ] => (!place.projection.contains(&ProjectionElem::Deref)).then(|| Origin { + array: Array::Owned(mir::Place { + local: place.local, + projection: self.tcx.mk_place_elems(array), + }), + index: None, }), - _ => one(Origin { + _ => Some(Origin { array: Array::Lone(at), index: Some(0), }), } } - /// Where a raw pointer to what the reference `r` points at, made at `at`, may point, as + /// Where a raw pointer to what the reference `r` points at, made at `at`, points, as /// [`Self::behind_reference`] makes it: where the body shows `r` borrowed, and else at /// a place of its own, or, where `r` is an `rs.Place`, where that points. - fn reference_paths( - &self, - r: mir::Place<'tcx>, - at: Location, - seen: &mut Vec, - ) -> Option, bool)>> { - let one = |o: Origin<'tcx>| Some(vec![(o, false)]); + fn reference_origin(&self, r: mir::Place<'tcx>, at: Location) -> Option> { let ty = r.ty(&self.mir.local_decls, self.tcx).ty; // A reference to an `Arc`'s value points at that value's one place. if !ty.is_ref() || self.arc_of(&Operand::Copy(r)).is_some() { @@ -482,7 +376,7 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { let pointee = self.tcx.mk_place_deref(r); let borrowed = self.through_aliases(pointee); if borrowed != pointee { - return self.raw_paths(borrowed, at, seen); + return self.raw_origin(borrowed, at); } if self.indexed(r).is_some() { let (def_id, _, args) = self.def_call(r.as_local()?)?; @@ -494,18 +388,18 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { return None; } let o = self.start(&args[0].node)?; - return one(Origin { + return Some(Origin { index: o.index.zip(self.const_int(&args[1].node)).map(|(i, n)| i + n), ..o }); } if let Some(p) = self.reborrowed_pointer(r) { - return Some(self.paths(&Operand::Copy(p), seen)); + return self.origin(&Operand::Copy(p)); } if self.b.t.place_pointee(ty).is_some() { return None; } - one(Origin { + Some(Origin { array: Array::Lone(at), index: Some(0), }) @@ -732,19 +626,7 @@ impl<'tcx> Fx<'_, '_, '_, 'tcx> { GenericArgsRef<'tcx>, &[Spanned>], )> { - self.call_at(self.def_block(local)?) - } - - /// The function, its generic arguments, and its arguments, of the call of std's that - /// ends `bb`. - fn call_at( - &self, - bb: BasicBlock, - ) -> Option<( - rustc_hir::def_id::DefId, - GenericArgsRef<'tcx>, - &[Spanned>], - )> { + let bb = self.def_block(local)?; let TerminatorKind::Call { func, args, .. } = &self.mir.basic_blocks[bb].terminator().kind else { return None; }; From 513169af9f9ba42d266180f9648a9938b6c9413c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:38:48 -0800 Subject: [PATCH 35/45] quest: name a joined reference pointer's offset_from among the run-time throws Co-Authored-By: Claude Opus 5.5 --- quest/m3/ts-runtime-throws.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/quest/m3/ts-runtime-throws.md b/quest/m3/ts-runtime-throws.md index 9a108b3e1..1cf8573e0 100644 --- a/quest/m3/ts-runtime-throws.md +++ b/quest/m3/ts-runtime-throws.md @@ -24,9 +24,12 @@ The rest are gaps. At `main` they include a panic its hook cannot locate ([#556](https://github.com/moq-dev/rust2/pull/556)), a pointer cast to `()` that is not an `Arc`'s or `Rc`'s (`rs.arcErase`, [#562](https://github.com/moq-dev/rust2/pull/562), [#565](https://github.com/moq-dev/rust2/pull/565)), `offset_from` of one Rust allocation -JS keeps in two arrays, a pointer moved past the array JS keeps it in (`AtIndex.reach`, such -as one kept in a field), `Bytes::slice_ref` of other storage, `alloc`'s bytes read as -values, ranges stepped without their type, fmt widths written as a placeholder, a read +JS keeps in two arrays (such as two pointers made from one reference, one of them through +a branch or loop, which JS keeps each in an array of one of its own; decided 2026-11-07 in +[#621](https://github.com/moq-dev/rust2/pull/621) to throw rather than trace joins), a +pointer moved past the array JS keeps it in (`AtIndex.reach`, such as one kept in a field), +`Bytes::slice_ref` of other storage, `alloc`'s bytes read as values, ranges stepped +without their type, fmt widths written as a placeholder, a read through a substring's `str::as_ptr` outside its own bytes but inside its text's (`s[..1].as_ptr().add(1)`, `s[1..].as_ptr().wrapping_sub(1)`), and a raw or mutable slice of a byte in a field (a struct's, a tuple's, a closure's capture, or one an async body keeps From b26afb639b196e058e9fe525bc8a17350f74a67e Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:43:14 -0800 Subject: [PATCH 36/45] fix(ts): copy what a Pin or transparent wrapper holds as uninitialized when it is copy_parts unwraps a Pin as shim does, and passes a MaybeUninit's flag through Reverse, AssertUnwindSafe, Wrapping, and Saturating, as ManuallyDrop does. Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/copy.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 0f3e3faef..26b61614c 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -418,12 +418,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { body.extend(self.copy_parts(args.type_at(0), true, span)); body } - Some(Std::ManuallyDrop) => self.copy_parts(args.type_at(0), uninit, span), + // What it holds, as [`Self::shim`] copies it. + Some(Std::ManuallyDrop | Std::Pin) => self.copy_parts(args.type_at(0), uninit, span), Some(Std::Option) => ret(self.copy_unless(self.none(args.type_at(0)), args.type_at(0), span)), Some(Std::Poll) => ret(self.copy_unless(self.pending(args.type_at(0)), args.type_at(0), span)), Some(Std::Result) => ret(self.copy_result(args.type_at(0), args.type_at(1), span)), Some(Std::Reverse | Std::AssertUnwindSafe | Std::Wrapping | Std::Saturating) => { - ret(self.snapshot(v(), args.type_at(0), span)) + ret(self.snapshot(v(), part(args.type_at(0)), span)) } Some(Std::Bound) => { let ts_ty = self.ty(ty, span); From 993b76cfcd59d56db8d015c97f4eea4df7bb541c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:06:12 -0800 Subject: [PATCH 37/45] quest: recount the model census after #644 Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index 02ab1154b..8e932ceb9 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -42,14 +42,14 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi - The end-to-end work stays here: the model's roots, its differential harness, and the `frames.ts` workload. moq writes the binding files `model.toml` names, in its own tree. -The census at the pin on 2026-11-07: 45 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-07: 44 refusals. `Timestamp` alone refuses nothing, and the [model fixture](/ts/README.md#tests)'s harness runs it against native moq-net at the pin; each case it adds as a type translates. Every other root reaches the rest of the model, all of it moq's, for its research quest: kio's `log.rs` reads bytes as its segments and retires them through crossbeam-epoch, whose tagged pointers and inline closures (36 -refusals) translation refuses for good, and smallvec's union over a type parameter, its -`Array::Item`, and raw parts (5) stay refused under [m3's unsafe policy](/quest/m3/README.md), so moq rewrites or drops -them, as it does the `unsafe` slice of `model/frame.rs`'s `Filled`. +refusals) translation refuses for good, and smallvec's union over a type parameter and +raw parts (4) stay refused under [m3's unsafe policy](/quest/m3/README.md), so moq +rewrites or drops them, as it does the `unsafe` slice of `model/frame.rs`'s `Filled`. Decided with the maintainer on 2026-11-04, from a census at the bumped pin (moq-dev/rust2#390): the refusals no quest owned became child quests, since finished. From f47c1177c890d114d713ffd5b6a6c4e94f9f4919 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:07:49 -0800 Subject: [PATCH 38/45] docs(ts): name interior-mutable shared references and nonempty byte slices in the pointer row Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/README.md b/ts/README.md index a34aae850..3dfb7eae1 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,7 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Vec` | `rs.Vec`: 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` | `rs.Cow`: the `Vec` it owns, or an `rs.Borrowed` holding the slice it borrows, `rs.Cow>` 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`, `Reverse`, `NonZero`, `AssertUnwindSafe` | 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`, which for what a box holds is the place holding the box | -| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, and view; one to a value in no array is element 0 of an array of one, as in Rust, but a byte variable whose `&raw` the body takes, or that of a reference to it made there, as `&mut b as *mut u8` does, or of whose `&mut` it makes a slice of one, is element 0 of a `Uint8Array` of its own, which its raw, mutable, and shared slices view as they do any other bytes; JS keeps an array of one, and a shared subslice of a byte in no array, which JS copies, apart from what holds it in Rust, so a pointer, slice, copy, or write of bytes the body shows reaching past either, a read or write it shows one past a value in no array, a pointer it shows moved from one by an offset, wrapping or not, or a copy, write, or slice of a count or length, it doesn't fix, and `offset_from` between pointers the body shows JS keeps in different arrays, are refused, and through a pointer from elsewhere, or one a branch or loop assigns, any of these, a raw or mutable slice of a byte in no array, such as a field's, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference, which nothing may write while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut` is in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `RefCell` or a `Vec` one object deep, with what a cell, lock, or range holds inline, and one with interior mutability, such as a `Cell`, always; a collection whose JS object keeps its length with its storage, such as a `Vec` of other than bytes, a `VecDeque`, a `String`, a map, or a set, which no copy splits from the original as Rust's read does, and any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, are refused where the read would copy them | +| `*const T`, `*mut T`, `NonNull` | 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` 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`, `write_bytes` of bytes, and `slice::from_raw_parts` move, measure, copy between, set, and view; one to a value in no array is element 0 of an array of one, as in Rust, but a byte variable whose `&raw` the body takes, or that of a reference to it made there, as `&mut b as *mut u8` does, or of whose `&mut` it makes a slice of one, is element 0 of a `Uint8Array` of its own, which its raw, mutable, and shared slices view as they do any other bytes; JS keeps an array of one, and a shared subslice of a byte in no array, which JS copies, apart from what holds it in Rust, so a pointer, slice, copy, or write of bytes the body shows reaching past either, a read or write it shows one past a value in no array, a pointer it shows moved from one by an offset, wrapping or not, or a copy, write, or slice of a count or length, it doesn't fix, and `offset_from` between pointers the body shows JS keeps in different arrays, are refused, and through a pointer from elsewhere, or one a branch or loop assigns, any of these, a nonempty raw or mutable slice of a byte in no array, such as a field's, 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. `str::as_ptr` points into the text's UTF-8, which JS encodes anew at each call, so a read through it reads what Rust's does and a write, or a mutable slice, through it, which Rust forbids, fails at run time, as do a read before its first byte, such as a substring's pointer moved back into its text's bytes, and `offset_from` between two calls' pointers, each refused where the body shows it. 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. One made from a reference, by `&raw const *r`, `as`, `ptr::from_ref` or `from_mut`, or `NonNull::from`, is where the body shows the reference borrowed, as the `rs.AtIndex` of the element `&v[i]` lends, and else a place of its own: holding the value of a shared reference to a value that is `Freeze` or keeps a tag or niche, which nothing may write whole while the pointer lives, or an `rs.placeIn` writing a whole value into the object a `&mut`, or a shared reference to any other value, is in place, refused where JS can't write into one in place. 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 that moves a value out, by `ptr::read`, `replace`, `swap`, or `copy`, copies each object it holds at any depth that the crate writes into in place, a library object such as a `RefCell` or a `Vec` one object deep, with what a cell, lock, or range holds inline, and one with interior mutability, such as a `Cell`, always; a collection whose JS object keeps its length with its storage, such as a `Vec` of other than bytes, a `VecDeque`, a `String`, a map, or a set, which no copy splits from the original as Rust's read does, and any other library object such as a `LazyCell` or a `Peekable`, whose inline state only its runtime class knows, are refused where the read would copy them | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined`, as `Box::new_uninit` and each of `Box::new_uninit_slice`'s is; a slice's `write_clone_of_slice` clones into its slots without dropping what they held. `Box::new_zeroed`'s holds the value of zero bytes in each part they are one of, such as a number, `false`, `None`, a null pointer, or an enum's variant of a tag or niche of zero, and `undefined` in each part they are none of, such as a reference, for a write to fill before a read | | `ManuallyDrop` | `T`, which nothing drops but its own `drop`; its `Debug`, comparisons, and constants are refused | | `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it | From 52a10e96906f355f8fc678f676aaf434231e921f Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:22:27 -0800 Subject: [PATCH 39/45] fix(ts): refuse a move-out read of a closure or coroutine whose copy Rust tells apart A pointer from a shared reference to a Freeze closure reads out the same JS function, whose captured state the original and the copy then share. ptr::read and the other unsafe moves now refuse a value holding, by value, a closure that writes its captures or captures a value with interior mutability, or a coroutine, with a span (reject's closure_moved). Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 5 +++- ts/fixtures/reject/expected.txt | 5 +++- ts/fixtures/reject/src/alloc.rs | 21 +++++++++++++++++ ts/fixtures/reject/ts.toml | 1 + ts/src/lower/copy.rs | 42 +++++++++++++++++++++++++-------- ts/src/lower/pointer.rs | 8 +++---- 6 files changed, 65 insertions(+), 17 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index f0bee9eae..d18d26f23 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -32,13 +32,16 @@ fixtures/reject/src/alloc.rs:cell_raw_unknown+5:15: `UnsafeCell::raw_get` of a p fixtures/reject/src/alloc.rs:cell_raw_unknown+6:3: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:cell_raw_unknown+8:15: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:cell_raw_unknown+9:13: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is +fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+5:18: +5:25}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:closure_moved+2:27: a raw pointer to `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+4:23: a read moving out a `std::collections::VecDeque`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place -fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart +fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, which holds a `{closure@fixtures/reject/src/alloc.rs:lazy_moved+1:38: +1:45}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:pinned_lent+1:32: `UnsafeCell::get_mut` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 5c0fb04f3..d09408561 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -36,13 +36,16 @@ fixtures/reject/src/alloc.rs:cell_raw_unknown+5:15: `UnsafeCell::raw_get` of a p fixtures/reject/src/alloc.rs:cell_raw_unknown+6:3: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:cell_raw_unknown+8:15: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:cell_raw_unknown+9:13: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is +fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+5:18: +5:25}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:closure_moved+2:27: a raw pointer to `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+4:23: a read moving out a `std::collections::VecDeque`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place -fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which JS can't copy apart +fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, which holds a `{closure@fixtures/reject/src/alloc.rs:lazy_moved+1:38: +1:45}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart fixtures/reject/src/alloc.rs:pinned_lent+1:32: `UnsafeCell::get_mut` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index 5b8161627..d7f3a68b9 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -90,6 +90,27 @@ pub fn collection_moved(v: Vec, mut q: std::collections::VecDeque, s: old_v.len() + old_q.len() + words[0].len() + old_s.0.len() + old_t.len() } +/// Closures read out through a pointer from a shared reference and both called: one that +/// counts in its own capture, and one capturing a cell it sets. Rust's copy keeps state of +/// its own, which JS keeps in the one function. +pub fn closure_moved(n: u32) -> u32 { + fn read(r: &F) -> F { + unsafe { std::ptr::read(r) } + } + let mut m = n; + let mut count = move || { + m += 1; + m + }; + let mut old = read(&count); + let cell = std::cell::Cell::new(n); + let set = move || cell.set(cell.get() + 1); + let old_set = read(&set); + set(); + old_set(); + count() + old() +} + /// A pointer from a reference the body is given, which JS keeps in no array, read through /// past it by a constant wrapping offset and one past it, and viewed as a slice of a /// length the body doesn't fix: each reaches the neighbour Tree Borrows allows it to. diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index fdf67523d..ff0b87957 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -11,6 +11,7 @@ roots = [ "alloc::cell_pointer_reassigned", "alloc::cell_raw_reassigned", "alloc::cell_raw_unknown", + "alloc::closure_moved", "alloc::collection_moved", "alloc::infallible_cell", "alloc::lazy_moved", diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 26b61614c..492f3b156 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -152,16 +152,21 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }) } - /// The collection held by value in a `ty` that a move of one out of where it is kept, as - /// `ptr::read` makes, would copy, if any: a `Vec` of other than bytes, a `VecDeque`, a - /// `String`, a map, a set, or a list, whose JS object keeps its length with its storage, - /// so no copy splits from the original as Rust's read does, sharing the storage while - /// each keeps a length of its own. A safe move leaves nothing pointing into the - /// original, which a copy one object deep is exact for. - pub fn unsplit(&self, ty: Ty<'tcx>) -> Option> { - by_value(self.tcx, self.typing_env(), ty).find(|&part| { + /// What a move of a `ty` out of where it is kept, as `ptr::read` makes, would copy that no + /// JS copy splits from the original as Rust's read does, if anything, and why: a + /// collection held by value, a `Vec` of other than bytes, a `VecDeque`, a `String`, a + /// map, a set, or a list, whose JS object keeps its length with its storage, sharing the + /// storage while each keeps a length of its own; or a closure that writes its captures or + /// captures a value with interior mutability, or a coroutine, whose state is the JS + /// function's or generator's own. A safe move leaves nothing pointing into the original, + /// which a copy one object deep is exact for. + pub fn unsplit(&self, ty: Ty<'tcx>) -> Option<(Ty<'tcx>, &'static str)> { + by_value(self.tcx, self.typing_env(), ty).find_map(|part| { + if self.stateful(part) { + return Some((part, "a closure or coroutine whose state JS keeps in the function itself")); + } let ty::Adt(adt, args) = part.kind() else { - return false; + return None; }; let joined = match Std::of(self.tcx, adt.did()) { Some(Std::Vec) => !super::byte(args.type_at(0)), @@ -178,10 +183,27 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { ) => true, _ => false, }; - joined && self.changes(part) + (joined && self.changes(part)) + .then_some((part, "a collection whose JS object keeps its length with its storage")) }) } + /// Whether `ty` is a closure whose copy Rust can tell apart from the original, as it + /// writes its captures or captures a value with interior mutability, at any depth, or a + /// coroutine. + fn stateful(&self, ty: Ty<'tcx>) -> bool { + let (tcx, env) = (self.tcx, self.typing_env()); + match ty.kind() { + ty::Coroutine(..) => true, + ty::Closure(..) | ty::CoroutineClosure(..) => { + super::mir::writes_captures(tcx, ty) + || (super::captured(ty).unwrap_or_default().iter()) + .any(|t| by_value(tcx, env, t).any(|part| !part.is_freeze(tcx, env) || self.stateful(part))) + } + _ => false, + } + } + /// Whether the runs before compared pointers to a `ty`, or to a type it holds by value, /// so each copy of one is at an address of its own, as in Rust, wherever it is read. pub fn held_addressed(&self, ty: Ty<'tcx>) -> bool { diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index d0fd2743a..ddd37d963 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -1335,18 +1335,16 @@ pub(super) fn cell_pointer<'tcx>(b: &Body<'_, '_, 'tcx>, cell: Expr, ty: Ty<'tcx } /// The refusal of a move of a `ty` out of where it is kept, as `ptr::read` makes, that -/// would copy a collection whose JS storage can't split from the original as Rust's does. +/// would copy what JS can't split from the original as Rust's does. fn unsplit<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Option { - let held = b.t.unsplit(ty)?; + let (held, why) = b.t.unsplit(ty)?; let what = match held == ty { true => String::new(), false => format!(", which holds a `{held}`"), }; Some(b.err( span, - format!( - "a read moving out a `{ty}`{what}, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does" - ), + format!("a read moving out a `{ty}`{what}, {why}, which no copy splits from the original as Rust's read does"), )) } From 8c53bc9a28a444ca67f9c7703907d5fc8411c8d1 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:27:16 -0800 Subject: [PATCH 40/45] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/copy.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 492f3b156..26d9581b0 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -163,7 +163,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { pub fn unsplit(&self, ty: Ty<'tcx>) -> Option<(Ty<'tcx>, &'static str)> { by_value(self.tcx, self.typing_env(), ty).find_map(|part| { if self.stateful(part) { - return Some((part, "a closure or coroutine whose state JS keeps in the function itself")); + return Some(( + part, + "a closure or coroutine whose state JS keeps in the function itself", + )); } let ty::Adt(adt, args) = part.kind() else { return None; From 56ee890dfc81bd559426ef8202980622b2ee044f Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:36:53 -0800 Subject: [PATCH 41/45] fix(ts): refuse a move-out read of a closure whose captures its copy would split A closure that moves out a captured Vec the crate writes into gives each copy the same JS vector, so the stateful check also counts captures a copy copies (reject's closure_moved). Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 1 + ts/fixtures/reject/expected.txt | 1 + ts/fixtures/reject/src/alloc.rs | 13 +++++++++---- ts/src/lower/copy.rs | 15 +++++++++------ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index d18d26f23..8d36e0b75 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -33,6 +33,7 @@ fixtures/reject/src/alloc.rs:cell_raw_unknown+6:3: `UnsafeCell::raw_get` of a po fixtures/reject/src/alloc.rs:cell_raw_unknown+8:15: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:cell_raw_unknown+9:13: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+15:13: +15:20}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+5:18: +5:25}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:closure_moved+2:27: a raw pointer to `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index d09408561..e40b678ac 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -37,6 +37,7 @@ fixtures/reject/src/alloc.rs:cell_raw_unknown+6:3: `UnsafeCell::raw_get` of a po fixtures/reject/src/alloc.rs:cell_raw_unknown+8:15: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:cell_raw_unknown+9:13: `UnsafeCell::raw_get` of a pointer not made of a place of `std::cell::UnsafeCell`, such as a parameter, a vector's or a slice's, or one moved by arithmetic, which may point where no cell is fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+15:13: +15:20}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:closure_moved+2:12: a read moving out a `{closure@fixtures/reject/src/alloc.rs:closure_moved+5:18: +5:25}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:closure_moved+2:27: a raw pointer to `{closure@fixtures/reject/src/alloc.rs:closure_moved+11:12: +11:19}` behind a reference, which JS can't write a whole value into in place fixtures/reject/src/alloc.rs:collection_moved+2:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does diff --git a/ts/fixtures/reject/src/alloc.rs b/ts/fixtures/reject/src/alloc.rs index d7f3a68b9..744b51629 100644 --- a/ts/fixtures/reject/src/alloc.rs +++ b/ts/fixtures/reject/src/alloc.rs @@ -91,9 +91,10 @@ pub fn collection_moved(v: Vec, mut q: std::collections::VecDeque, s: } /// Closures read out through a pointer from a shared reference and both called: one that -/// counts in its own capture, and one capturing a cell it sets. Rust's copy keeps state of -/// its own, which JS keeps in the one function. -pub fn closure_moved(n: u32) -> u32 { +/// counts in its own capture, one capturing a cell it sets, and one moving out a vector +/// the crate writes into. Rust's copy keeps state of its own, which JS keeps in the one +/// function. +pub fn closure_moved(n: u32, bytes: Vec) -> u32 { fn read(r: &F) -> F { unsafe { std::ptr::read(r) } } @@ -108,7 +109,11 @@ pub fn closure_moved(n: u32) -> u32 { let old_set = read(&set); set(); old_set(); - count() + old() + let give = move || bytes; + let old_give = read(&give); + let mut first = std::mem::ManuallyDrop::new(give()); + unsafe { first.set_len(0) }; + count() + old() + old_give().len() as u32 } /// A pointer from a reference the body is given, which JS keeps in no array, read through diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 26d9581b0..977c6121d 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -156,9 +156,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// JS copy splits from the original as Rust's read does, if anything, and why: a /// collection held by value, a `Vec` of other than bytes, a `VecDeque`, a `String`, a /// map, a set, or a list, whose JS object keeps its length with its storage, sharing the - /// storage while each keeps a length of its own; or a closure that writes its captures or - /// captures a value with interior mutability, or a coroutine, whose state is the JS - /// function's or generator's own. A safe move leaves nothing pointing into the original, + /// storage while each keeps a length of its own; or a closure whose captures its copy + /// would split, or a coroutine, whose state is the JS function's or generator's own. A safe move leaves nothing pointing into the original, /// which a copy one object deep is exact for. pub fn unsplit(&self, ty: Ty<'tcx>) -> Option<(Ty<'tcx>, &'static str)> { by_value(self.tcx, self.typing_env(), ty).find_map(|part| { @@ -192,8 +191,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } /// Whether `ty` is a closure whose copy Rust can tell apart from the original, as it - /// writes its captures or captures a value with interior mutability, at any depth, or a - /// coroutine. + /// writes its captures or captures, at any depth, a value with interior mutability or + /// one a copy copies, such as a `Vec` the crate writes into, which the closure may move + /// out, or a coroutine. fn stateful(&self, ty: Ty<'tcx>) -> bool { let (tcx, env) = (self.tcx, self.typing_env()); match ty.kind() { @@ -201,7 +201,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { ty::Closure(..) | ty::CoroutineClosure(..) => { super::mir::writes_captures(tcx, ty) || (super::captured(ty).unwrap_or_default().iter()) - .any(|t| by_value(tcx, env, t).any(|part| !part.is_freeze(tcx, env) || self.stateful(part))) + .any(|t| { + by_value(tcx, env, t) + .any(|part| !part.is_freeze(tcx, env) || self.changes(part) || self.stateful(part)) + }) } _ => false, } From 45f045b88ffccfe978d62fcd60a11ab20c1d0fd8 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:43:26 -0800 Subject: [PATCH 42/45] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/copy.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 977c6121d..86756444c 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -200,11 +200,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { ty::Coroutine(..) => true, ty::Closure(..) | ty::CoroutineClosure(..) => { super::mir::writes_captures(tcx, ty) - || (super::captured(ty).unwrap_or_default().iter()) - .any(|t| { - by_value(tcx, env, t) - .any(|part| !part.is_freeze(tcx, env) || self.changes(part) || self.stateful(part)) - }) + || (super::captured(ty).unwrap_or_default().iter()).any(|t| { + by_value(tcx, env, t) + .any(|part| !part.is_freeze(tcx, env) || self.changes(part) || self.stateful(part)) + }) } _ => false, } From 357cf8a91097b8f1f87035bcb1870861674e4c2a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:05:31 -0800 Subject: [PATCH 43/45] quest: recount the model census Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index 8e932ceb9..596e4c225 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -42,7 +42,7 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi - The end-to-end work stays here: the model's roots, its differential harness, and the `frames.ts` workload. moq writes the binding files `model.toml` names, in its own tree. -The census at the pin on 2026-11-07: 44 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-07: 45 refusals. `Timestamp` alone refuses nothing, and the [model fixture](/ts/README.md#tests)'s harness runs it against native moq-net at the pin; each case it adds as a type translates. Every other root reaches the rest of the model, all of it moq's, for its research quest: kio's `log.rs` reads bytes as its segments From aa033e2f1254568a0d122a9b81729ba3f2785bbf Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:10:50 -0800 Subject: [PATCH 44/45] fix(ts): refuse an opaque library object only where an unsafe move reads it out Safe moves, such as Cell::replace of an array of LazyCells through elem_copy, copy such an object one object deep again, as main did; unsplit refuses it in the unsafe arms with the collection and closure refusals. Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/pointers/expected/lib.ts | 4 ++ .../lang/pointers/expected/pointers.ts | 32 ++++++++--- ts/fixtures/lang/pointers/src/pointers.rs | 5 ++ ts/fixtures/reject/census.txt | 4 +- ts/fixtures/reject/expected.txt | 4 +- ts/src/lower/copy.rs | 55 ++++++++++++++++--- 6 files changed, 82 insertions(+), 22 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 73946c9b8..9e95e0b21 100644 --- a/ts/fixtures/lang/pointers/expected/lib.ts +++ b/ts/fixtures/lang/pointers/expected/lib.ts @@ -71,6 +71,10 @@ export function copy$array_u8(v: Uint8Array): Uint8Array { return v.slice(); } +export function copy$cell_LazyCell_u32(v: rs.LazyCell): rs.LazyCell { + return rs.objectCopy(v); +} + export function copy$cell_OnceCell_Cell_u32(v: rs.OnceCell>): rs.OnceCell> { const c = rs.objectCopy(v); c.v = copy$Option_Cell_u32(c.v); diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 20a0c29e5..fa33364d5 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_pointers_HeldCell, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Reentrant, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u16, copy$tuple2_u8_u8, drop$pointers_List_Vec_u8, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; +import { copy$Option_Cell_u32, copy$Option_Vec_u8, copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$Vec_u8, copy$array_array2_u32, copy$array_ptr_array4_u8, copy$array_tuple2_u8_u8, copy$array_u16, copy$array_u32, copy$array_u8, copy$cell_LazyCell_u32, copy$cell_OnceCell_Cell_u32, copy$cell_RefCell_Cell_u32, copy$mem_MaybeUninit_Vec_u8, copy$mem_MaybeUninit_array_u32, copy$mem_MaybeUninit_array_u8, copy$mem_MaybeUninit_pointers_HeldCell, copy$mem_MaybeUninit_tuple2_u8_u8, copy$ops_Range_Cell_u32, copy$pointers_Aimed_Cell_u8, copy$pointers_Aimed_array2_u16, copy$pointers_Aimed_array4_u8, copy$pointers_Aimed_pointers_Housed_u8, copy$pointers_Aimed_pointers_Leaning, copy$pointers_Aimed_pointers_Paired, copy$pointers_Aimed_u8, copy$pointers_ByCall, copy$pointers_ByClosure, copy$pointers_ByObject, copy$pointers_Duo, copy$pointers_Either_u32, copy$pointers_Either_u8, copy$pointers_Halves, copy$pointers_Outer, copy$pointers_Pair, copy$pointers_Reentrant, copy$pointers_Spot, copy$pointers_TaggedVec, copy$pointers_Twin, copy$sync_Weak_u32, copy$tuple2_Vec_u8_u8, copy$tuple2_array2_u32_u8, copy$tuple2_u16_u8, copy$tuple2_u32_u32, copy$tuple2_u8_u16, copy$tuple2_u8_u8, drop$pointers_List_Vec_u8, drop$pointers_List_sync_Arc_u8, drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin, drop$tuple3_sync_Arc_u32_Rc_pointers_Duo_sync_Arc_unit, drop$tuple4_sync_Arc_pointers_Twin_sync_Arc_pointers_Twin_Rc_pointers_Twin_Rc_pointers_Twin, of$pointers_Bin_pointers_ByObject } from "./lib.ts"; import { thenSome$1gvhopg, then$01qpu7s, then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$1nosv3e, isSomeAnd$07atiew, unwrapOr$0mkoact as option_unwrapOr$0mkoact, unwrapOr$179xo1w as option_unwrapOr$179xo1w, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1cfrse5 as option_map$1cfrse5, map$1px2rk4 as option_map$1px2rk4, mapOr$078vnqn as option_mapOr$078vnqn, mapOr$00zno7m as option_mapOr$00zno7m, mapOr$18adzvj as option_mapOr$18adzvj, mapOr$0m14xib as option_mapOr$0m14xib, mapOr$1wlsr3s as option_mapOr$1wlsr3s, mapOr$0emjeap as option_mapOr$0emjeap, mapOr$173j2gr as option_mapOr$173j2gr, mapOr$0smr0aj as option_mapOr$0smr0aj, filter$0wvwd70 } from "./core/option.ts"; import type * as Aimed from "./pointers.Aimed.ts"; @@ -4874,17 +4874,17 @@ export function arrayParts(data: rs.ByteSlice, out: rs.ByteVec): void { } const _25: rs.u32[] = [rs.u32Add(n, 1), rs.u32Add(n, 2)]; const pointer2 = p; - const old2 = rs.sliceCopy(pointer2); + const old3 = rs.sliceCopy(pointer2); rs.sliceSet(pointer2, _25); - rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(old2), (_24: rs.u32): rs.u8 => { + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(old3), (_24: rs.u32): rs.u8 => { return _24 & 0xff; }), undefined, true); const q: rs.Slice = rs.optionUnwrap(rs.sliceGetRange(rs.sliceRange(rs.sliceFrom(words.items), 3), 0, 2)); const x = rs.optionUnwrap(rs.sliceGetRange(rs.sliceRange(rs.sliceFrom(words.items), 1), 0, 2)); const y = q; - const old3 = rs.sliceCopy(x); + const old4 = rs.sliceCopy(x); rs.sliceSet(x, rs.sliceCopy(y)); - rs.sliceSet(y, old3); + rs.sliceSet(y, old4); const nn: rs.Slice = rs.optionUnwrap(rs.sliceGetRange(rs.sliceRange(rs.sliceFrom(words.items), 2), 0, 2)); const _54: rs.u32 = rs.u32Add(n, 3); const _55: rs.Slice = nn; @@ -4905,8 +4905,8 @@ export function arrayParts(data: rs.ByteSlice, out: rs.ByteVec): void { rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([rs.sliceFrom(fewer).length, rs.iterCount(rs.iterSlice(more)), rs.sliceFrom(none2).length], (n2: rs.usize): rs.u8 => { return n2 & 0xff; })), undefined, true); - rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(words.items), (_211: rs.u32): rs.u8 => { - return _211 & 0xff; + rs.iterExtendBytes(out, rs.iterMap(rs.iterSlice(words.items), (_212: rs.u32): rs.u8 => { + return _212 & 0xff; }), undefined, true); const rows: rs.Vec = rs.iterToVec(rs.iterMap(rs.sliceChunks(rs.sliceFrom(words.items), 2), (c: rs.Slice): rs.u32[] => { rs.idx(c, 0); @@ -4928,8 +4928,8 @@ export function arrayParts(data: rs.ByteSlice, out: rs.ByteVec): void { const _127: rs.u32 = rs.u32Add(n, 5); const slice8 = rs.arraySliceAt(nn2, 0); slice8.data[slice8.start + 1] = _127; - rs.iterExtendBytes(out, rs.iterMap(rs.iterFlatten(rs.arraySliceIter(rs.arraySliceFrom(rows.items, rs.sliceFrom)), (value: rs.Slice) => rs.iterSlice(rs.sliceItems(value)), true, 2), (_212: rs.u32): rs.u8 => { - return _212 & 0xff; + rs.iterExtendBytes(out, rs.iterMap(rs.iterFlatten(rs.arraySliceIter(rs.arraySliceFrom(rows.items, rs.sliceFrom)), (value: rs.Slice) => rs.iterSlice(rs.sliceItems(value)), true, 2), (_215: rs.u32): rs.u8 => { + return _215 & 0xff; }), undefined, true); const pointer5 = rs.byteSliceFrom(Uint8Array.of(n & 0xff, 5)); rs.iterExtendBytes(out, rs.iterSlice(option_mapOr$0m14xib(pointer5 === rs.NULL ? rs.None : rs.byteSliceView(pointer5), rs.bytes(2), (m: Uint8Array): Uint8Array => { @@ -4949,6 +4949,20 @@ export function arrayParts(data: rs.ByteSlice, out: rs.ByteVec): void { rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap(uc.v, (w5: rs.u32): rs.u8 => { return w5 & 0xff; })), undefined, true); + const _184: [() => rs.u32, () => rs.u32] = [(): rs.u32 => { + return 7; + }, (): rs.u32 => { + return 8; + }]; + const seven: () => rs.u32 = _184[0]; + const eight: () => rs.u32 = _184[1]; + const lazies: rs.LazyCell[][] = [[new rs.LazyCell(seven)]]; + const _191: rs.Cell[][]> = rs.cellOfArraySlice(rs.arraySliceFrom(lazies, rs.sliceFrom), copy$cell_LazyCell_u32); + const _193: rs.LazyCell[][] = [[new rs.LazyCell(eight)]]; + const pointer6 = rs.cellArray(_191, rs.arraySliceOf(rs.sliceFrom)); + const old5 = rs.arraySliceCopy(pointer6, copy$cell_LazyCell_u32); + rs.arraySliceSet(pointer6, _193); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(old5[0][0].force() & 0xff, lazies[0][0].force() & 0xff, n & 0xff)), undefined, true); } //# sourceMappingURL=pointers.ts.map diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index aeeb33647..ded29dc65 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -3589,4 +3589,9 @@ pub fn array_parts(data: &[u8], out: &mut Vec) { unsafe { uc.get_mut().assume_init_mut()[0] = n + 8 }; unsafe { uc.as_mut_unchecked().assume_init_mut()[1] = n + 9 }; out.extend(unsafe { uc.into_inner().assume_init() }.map(|w| w as u8)); + // Lazy cells in arrays a `Cell` replaces, moved out safely and forced. + let (seven, eight): (fn() -> u32, fn() -> u32) = (|| 7, || 8); + let mut lazies = [[std::cell::LazyCell::new(seven)]]; + let old = Cell::from_mut(&mut lazies).replace([[std::cell::LazyCell::new(eight)]]); + out.extend([*old[0][0] as u8, *lazies[0][0] as u8, n as u8]); } diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 237cb2966..8975ece2a 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -43,9 +43,9 @@ fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::st fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place -fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, which holds a `{closure@fixtures/reject/src/alloc.rs:lazy_moved+1:38: +1:45}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust -fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart +fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:pinned_lent+1:32: `UnsafeCell::get_mut` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address fixtures/reject/src/alloc.rs:pinned_lent+2:43: `UnsafeCell::as_mut_unchecked` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address fixtures/reject/src/alloc.rs:recast_reference+4:11: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 6e803423d..ba82aa0ed 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -47,9 +47,9 @@ fixtures/reject/src/alloc.rs:collection_moved+10:3: a read moving out a `std::st fixtures/reject/src/alloc.rs:collection_moved+14:28: a read moving out a `(std::string::String, u8)`, which holds a `std::string::String`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:collection_moved+16:23: a read moving out a `std::vec::Vec`, a collection whose JS object keeps its length with its storage, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:infallible_cell+1:11: a raw pointer to `std::result::Result, !>` behind a reference, which JS can't write a whole value into in place -fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, which holds a `{closure@fixtures/reject/src/alloc.rs:lazy_moved+1:38: +1:45}`, a closure or coroutine whose state JS keeps in the function itself, which no copy splits from the original as Rust's read does +fixtures/reject/src/alloc.rs:lazy_moved+2:22: a read moving out a `std::cell::LazyCell`, a library object whose inline state only its runtime class knows, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:past_reference+1:30: a pointer reaching past a value in no array, which JS keeps apart from what holds it in Rust -fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which JS can't copy apart +fixtures/reject/src/alloc.rs:peeked_moved+3:26: a read moving out a `std::iter::Peekable>>`, a library object whose inline state only its runtime class knows, which no copy splits from the original as Rust's read does fixtures/reject/src/alloc.rs:pinned_lent+1:32: `UnsafeCell::get_mut` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address fixtures/reject/src/alloc.rs:pinned_lent+2:43: `UnsafeCell::as_mut_unchecked` of `std::pin::Pin>`, an object a whole write through the reference changes in place, so a value read out before would be a copy at another address fixtures/reject/src/alloc.rs:recast_reference+4:11: a read or write reaching past a value in no array, which JS keeps apart from what holds it in Rust diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 86756444c..cec0f3df8 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -156,10 +156,15 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// JS copy splits from the original as Rust's read does, if anything, and why: a /// collection held by value, a `Vec` of other than bytes, a `VecDeque`, a `String`, a /// map, a set, or a list, whose JS object keeps its length with its storage, sharing the - /// storage while each keeps a length of its own; or a closure whose captures its copy - /// would split, or a coroutine, whose state is the JS function's or generator's own. A safe move leaves nothing pointing into the original, + /// storage while each keeps a length of its own; a closure whose captures its copy + /// would split, or a coroutine, whose state is the JS function's or generator's own; or + /// a library object whose inline state only its runtime class knows, which the copy + /// copies one object deep. A safe move leaves nothing pointing into the original, /// which a copy one object deep is exact for. pub fn unsplit(&self, ty: Ty<'tcx>) -> Option<(Ty<'tcx>, &'static str)> { + if let Some(opaque) = self.opaque(ty) { + return Some((opaque, "a library object whose inline state only its runtime class knows")); + } by_value(self.tcx, self.typing_env(), ty).find_map(|part| { if self.stateful(part) { return Some(( @@ -190,6 +195,42 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }) } + /// The library object a copy of `ty` reaches whose inline state only its runtime class + /// knows, such as a `LazyCell` or a `Peekable`, if any, as [`Self::copy_parts`] walks it: + /// copied one object deep, which an unsafe move can tell apart. + fn opaque(&self, ty: Ty<'tcx>) -> Option> { + if !self.changes(ty) { + return None; + } + match ty.kind() { + ty::Tuple(tys) => tys.iter().find_map(|t| self.opaque(t)), + ty::Array(elem, _) | ty::Slice(elem) => self.opaque(*elem), + ty::Adt(adt, args) => match Std::of(self.tcx, adt.did()) { + Some(Std::WakerVTable | Std::RawWaker) => None, + Some( + Std::MaybeUninit + | Std::ManuallyDrop + | Std::Pin + | Std::Option + | Std::Poll + | Std::Result + | Std::ControlFlow + | Std::Reverse + | Std::AssertUnwindSafe + | Std::Wrapping + | Std::Saturating + | Std::Bound, + ) => args.types().find_map(|t| self.opaque(t)), + Some(_) => match self.inline_held(ty) { + Some(inline) => inline.fields.into_iter().find_map(|(_, t)| self.opaque(t)), + None => Some(ty), + }, + None => adt.all_fields().find_map(|f| self.opaque(self.field_ty(f.did, args))), + }, + _ => None, + } + } + /// Whether `ty` is a closure whose copy Rust can tell apart from the original, as it /// writes its captures or captures, at any depth, a value with interior mutability or /// one a copy copies, such as a `Vec` the crate writes into, which the closure may move @@ -482,14 +523,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { // A library object, which [`Self::changes`] says a whole write lands in, and a // copy of what it holds inline. Some(std) => { + // One object deep, which a safe move is exact for; an unsafe one is refused + // ([`Self::unsplit`]). let Some(inline) = self.inline_held(ty) else { - self.err( - span, - format!( - "a read moving out a `{ty}`, a library object whose inline state only its runtime class knows, which JS can't copy apart" - ), - ); - return ret(v()); + return ret(Expr::rs("objectCopy", vec![v()])); }; // A cell of its own, as `Cell::from_mut`'s place reads and writes another's. if std == Std::Cell { From 942417529d9f20579db647ad50917f6c079d37ac Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:16:29 -0800 Subject: [PATCH 45/45] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/copy.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index cec0f3df8..4414d8ee5 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -163,7 +163,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// which a copy one object deep is exact for. pub fn unsplit(&self, ty: Ty<'tcx>) -> Option<(Ty<'tcx>, &'static str)> { if let Some(opaque) = self.opaque(ty) { - return Some((opaque, "a library object whose inline state only its runtime class knows")); + return Some(( + opaque, + "a library object whose inline state only its runtime class knows", + )); } by_value(self.tcx, self.typing_env(), ty).find_map(|part| { if self.stateful(part) {