From 0269fe8daeb18f7adea0759267fdf88b6c2cd0fe Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:54:02 -0800 Subject: [PATCH 01/13] chore(quest): claim quest/m2/ts-model/addresses.md Quest-Claim: bac16fc78697e71a From 3ab30b738805dcf58346bd4dc666ee0e92902a6f Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:04:15 -0800 Subject: [PATCH 02/13] feat(ts): translate a pointer's address as an id per live object Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 5 +- quest/m2/ts-model/addresses.md | 24 -- ts/README.md | 3 +- .../lang/pointers/expected/core/option.ts | 8 + ts/fixtures/lang/pointers/expected/lib.ts | 9 +- .../lang/pointers/expected/pointers.Keyed.ts | 42 +++ .../lang/pointers/expected/pointers.ts | 171 +++++++++++- ts/fixtures/lang/pointers/src/lib.rs | 1 + ts/fixtures/lang/pointers/src/pointers.rs | 102 +++++++ ts/fixtures/reject/census.txt | 24 +- ts/fixtures/reject/expected.txt | 24 +- ts/fixtures/reject/src/addresses.rs | 58 ++++ ts/fixtures/reject/src/lib.rs | 1 + ts/fixtures/reject/ts.toml | 6 + ts/runtime/rs.ts | 43 ++- ts/runtime/rs/collections.ts | 7 +- ts/runtime/rs/hash.ts | 7 +- ts/runtime/rs/task.ts | 7 +- ts/src/lower/address.rs | 258 ++++++++++++++++++ ts/src/lower/collections.rs | 16 +- ts/src/lower/copy.rs | 28 +- ts/src/lower/mir/rvalue.rs | 23 ++ ts/src/lower/mod.rs | 13 + ts/src/lower/pointer.rs | 45 +-- ts/src/lower/task.rs | 5 +- 25 files changed, 863 insertions(+), 67 deletions(-) delete mode 100644 quest/m2/ts-model/addresses.md create mode 100644 ts/fixtures/lang/pointers/expected/pointers.Keyed.ts create mode 100644 ts/fixtures/reject/src/addresses.rs create mode 100644 ts/src/lower/address.rs diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index c1381d7ba..d87b41754 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -21,6 +21,10 @@ The codec's differential harness is the pattern to repeat. The shared npm runtim (for checked integer ops and the rest of `rs.ts`) is published only once two generated packages need it. +✅ Decided with the maintainer on 2026-11-04, for any crate: a pointer's address read as an +integer is an id unique among live objects, a multiple of the type's alignment, and whatever +has no exact answer, such as a pointer made back from an address, is refused at translation. + Decided with the maintainer on 2026-10-23, for the subset: `u128` stays, on the [wide-integer](/ts/README.md#design) representation. @@ -61,6 +65,5 @@ there, as does the `unsafe` slice of `model/frame.rs`'s `Filled`. - [std's impls through a reference](/quest/m2/ts-model/reference-impls.md) - `<&[u8] as AsRef<[u8]>>::as_ref` - [Bytes from an owner that drops](/quest/m2/ts-model/bytes-owner.md) - `Bytes::from_owner` of a frame's segment - [rangemap](/quest/m2/ts-model/rangemap.md) - B-tree ranges by a borrowed key, and std's iterators in a crate's fields -- [Addresses as identity](/quest/m2/ts-model/addresses.md) - `Arc::as_ptr(a) as usize` as a key, which needs a decision Public API: the translator's output for moq-net's model. diff --git a/quest/m2/ts-model/addresses.md b/quest/m2/ts-model/addresses.md deleted file mode 100644 index f78036cfb..000000000 --- a/quest/m2/ts-model/addresses.md +++ /dev/null @@ -1,24 +0,0 @@ -# [M] Addresses as identity - -## Goal - -[rust2-ts](/ts/README.md) translates a pointer's address read as an integer where a crate -uses it only as an identity: compared, hashed, or ordered as a key, such as -`Arc::as_ptr(a) as usize` and `Weak::as_ptr(w) as usize`, proven by differential tests -against native Rust. Today each pointer-to-integer cast is refused. - -## Plan - -moq-net's model keys its cache and frame accounting by address six times at the pin -(`model/cache.rs`, `model/frame.rs`, `model/group/content.rs`). - -Open question for the maintainer, since the 2026-10-28 decision refuses pointer-to-integer -casts: - -1. An address is an id unique among live objects, assigned on first read and kept in a - `WeakMap`, a multiple of the type's alignment so alignment checks hold, while an - integer cast back to a pointer stays refused (recommended: it closes the common - identity idiom for every crate, and Rust promises nothing more of an address's value). -2. Keep the refusal, and moq gives these a `cfg(rust2_ts)` twin keyed by an id of its own. - -Either way, crossbeam-epoch's tagged pointers stay refused: they cast the integer back. diff --git a/ts/README.md b/ts/README.md index 648549913..7f41546b9 100644 --- a/ts/README.md +++ b/ts/README.md @@ -110,6 +110,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `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.ByteSlice` of a `[u8; N]`'s bytes, which compares by the buffer and offset they are at, 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 an array of other values than bytes, which a slice lends as a copy 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 | +| a raw pointer's address: `p as usize`, `addr`, `expose_provenance`, and a raw pointer's `Hash` and `Ord` | `rs.addr`: null's 0, a dangling pointer's alignment, `Weak::new`'s `usize::MAX`, and otherwise a number the object keeps while JS keeps it, as an allocator places an allocation: a multiple of its type's alignment, in a block of its size no other kept object's overlaps, reused once JS collects the object, and an abort once wasm32's 4 GiB run out, as a failed allocation aborts. Refused, as JS can't show what Rust fixes: one to anything a pointer to doesn't compare as itself, to bytes, or to a type an allocation the program makes holds many of, as a `Vec` or an array does, or beside another whose address is read, as a struct's two fields are; `Weak::as_ptr` of a value `Arc::new_cyclic` makes; and a pointer made of an address | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined` | | `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 | @@ -1061,7 +1062,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 +Byte reinterpretation, pointers made of 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.md)), and trait objects other than `dyn Debug`, `dyn Display`, `dyn Future`, `dyn Any`, `dyn Error`, `dyn Fn*`, `dyn Buf`, `dyn Iterator` and its subtraits, and the translated crates' traits' behind `&`, `&mut`, `Box`, `Arc`, `Rc`, `Weak`, or `Pin` ([trait objects](/quest/m3/ts-dyn.md)), fail translation. Until they translate, an item that needs one, such as a `Box` made `From` a string, binds to diff --git a/ts/fixtures/lang/pointers/expected/core/option.ts b/ts/fixtures/lang/pointers/expected/core/option.ts index 8f6edbacb..b3ed0c268 100644 --- a/ts/fixtures/lang/pointers/expected/core/option.ts +++ b/ts/fixtures/lang/pointers/expected/core/option.ts @@ -2,6 +2,7 @@ import * as rs from "../rs.ts"; import { copy$pointers_Pair, copy$pointers_Spot } from "../lib.ts"; +import type * as Keyed from "../pointers.Keyed.ts"; import type * as Loaded from "../pointers.Loaded.ts"; import type * as Pair from "../pointers.Pair.ts"; import type * as Spot from "../pointers.Spot.ts"; @@ -13,6 +14,13 @@ export function isSomeAnd$1s2u5lz(self: rs.u8 | undefined, f: (a0: rs.u8) => boo return f(self); } +export function isSomeAnd$0fp5rqa(self: rs.Arc | undefined, f: (a0: rs.Arc) => boolean): boolean { + if (self === undefined) { + return false; + } + return f(self); +} + export function isSomeAnd$1nosv3e(self: [rs.Arc<[rs.u16, rs.u8] | undefined>, rs.Arc] | undefined, f: (a0: [rs.Arc<[rs.u16, rs.u8] | undefined>, rs.Arc]) => boolean): boolean { if (self === undefined) { return false; diff --git a/ts/fixtures/lang/pointers/expected/lib.ts b/ts/fixtures/lang/pointers/expected/lib.ts index 7c169c6d1..c7241e15c 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 } 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, addresses } 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"; @@ -11,6 +11,7 @@ import type * as Duo from "./pointers.Duo.ts"; import type * as Halves from "./pointers.Halves.ts"; import type * as Housed from "./pointers.Housed.ts"; import type * as Inner from "./pointers.Inner.ts"; +import type * as Keyed from "./pointers.Keyed.ts"; import type * as Leaning from "./pointers.Leaning.ts"; import * as List from "./pointers.List.ts"; import type * as Outer from "./pointers.Outer.ts"; @@ -201,6 +202,10 @@ export function drop$tuple2_sync_Arc_pointers_Twin_Rc_pointers_Twin(_1: [rs.Arc< _1[1][Symbol.dispose](); } +export function drop$tuple2_sync_Weak_pointers_Keyed_usize(_1: [rs.Weak, rs.usize]): void { + _1[0][Symbol.dispose](); +} + export function drop$tuple2_u8_sync_Arc_u8(_1: [rs.u8, rs.Arc]): void { _1[1][Symbol.dispose](); } @@ -287,6 +292,8 @@ export function cases(): rs.Vec<[string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.By rawArcs(data25, out25); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::raw_places", (_235: rs.u8, data26: rs.ByteSlice, out26: rs.ByteVec): undefined => { rawPlaces(data26, out26); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["pointers::addresses", (_236: rs.u8, data27: rs.ByteSlice, out27: rs.ByteVec): undefined => { + addresses(data27, out27); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); } diff --git a/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts b/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts new file mode 100644 index 000000000..093768b7b --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts @@ -0,0 +1,42 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; + +/** + * A value keyed by its address in a registry it leaves as it drops, as a cache keys + * what it tracks: by `Arc::as_ptr` as it is made, and by `ptr::from_ref` and a cast of + * `self` as it drops, which must be the same address. + */ +export class Keyed { + id: rs.u8; + registry: rs.Arc>>; + left: rs.Arc>; + + constructor(id: rs.u8, registry: rs.Arc>>, left: rs.Arc>) { + this.id = id; + this.registry = registry; + this.left = left; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + this.registry[Symbol.dispose](); + this.left[Symbol.dispose](); + } +} + +export function drop$ops_Drop(self: Keyed): void { + const address: rs.usize = rs.addr(self, 12, 4); + const _8: rs.RefCell> = rs.refCellBorrowMut(self.registry.value); + const _12: [rs.u8, rs.usize] = [rs.u8Rem(self.id, 4), address]; + const removed: rs.u8 | undefined = rs.btreeRemove(_8.value, _12); + rs.refMutRelease(_8); + const same: boolean = rs.addr(self, 12, 4) === address; + if (removed === self.id) { + if (same) { + rs.cellSet(self.left.value, rs.u8WrappingAdd(self.left.value.v, 1)); + } + } +} + +//# sourceMappingURL=pointers.Keyed.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 2f6d348aa..b21dfd5d9 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_ptr_array4_u8, copy$array_u16, copy$array_u8, copy$array_unit, 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 { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_ptr_array4_u8, copy$array_u16, copy$array_u8, copy$array_unit, 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$tuple2_sync_Weak_pointers_Keyed_usize, 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 { isSomeAnd$1s2u5lz, isSomeAnd$0fp5rqa, 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 type * as Aimed from "./pointers.Aimed.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; @@ -17,6 +17,7 @@ import type * as Halves from "./pointers.Halves.ts"; import * as Held from "./pointers.Held.ts"; import * as Housed from "./pointers.Housed.ts"; import type * as Inner from "./pointers.Inner.ts"; +import * as Keyed from "./pointers.Keyed.ts"; import * as Leaning from "./pointers.Leaning.ts"; import * as Lent from "./pointers.Lent.ts"; import type * as Links from "./pointers.Links.ts"; @@ -2855,4 +2856,170 @@ export function rawPlaces(data: rs.ByteSlice, out: rs.ByteVec): void { num[Symbol.dispose](); } +/** + * Addresses read as integers, as `addr`, and as a raw pointer's `Hash` and `Ord` read + * them: made `Arc`s and dropped ones keyed by address in a B-tree, a hash map, and sets of + * pointers, which must stay unique and aligned among those alive, order pointers as their + * addresses, and stay a `Weak`'s once its value drops. Only what Rust fixes is written: + * equality, counts, alignment, and the order's agreement, never an address itself. + */ +export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { + let _52!: boolean; + let _85!: boolean; + let _124!: boolean; + let _165!: boolean; + let agree!: boolean; + let _224!: rs.u8; + const registry: rs.Arc>> = new rs.Arc(new rs.RefCell(rs.btreeNew(rs.cmpTuple<[rs.u8, rs.usize], rs.Ordering>([rs.cmp, rs.cmp])) as rs.BTreeMap<[rs.u8, rs.usize], rs.u8>)); + const left: rs.Arc> = new rs.Arc(new rs.Cell(0)); + const align: rs.usize = 4; + const byAddress: rs.HashMap = rs.hashNew() as rs.HashMap; + const live: rs.Vec> = new rs.Vec(); + const weaks: rs.Vec<[rs.Weak, rs.usize]> = new rs.Vec(); + const ordered: rs.BTreeSet = rs.btreeNew(rs.cmpAddr(12, 4)) as rs.BTreeSet; + const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4), rs.same)) as rs.HashSet; + const _16: rs.Weak = new rs.Weak(); + const dangling: Keyed.Keyed = rs.weakAsPtr(_16); + _16[Symbol.dispose](); + rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([rs.addr(dangling, 12, 4) === 4294967295, rs.addr(rs.NULL, 12, 4) === 0, rs.addr(rs.DANGLING, 12, 4) === align], (a0: boolean): rs.u8 => { + return a0 ? 1 : 0; + })), undefined, true); + const iter: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); + l1: while (true) { + const _33: [rs.usize, rs.u8] | rs.Done = iter.next(); + if (_33 === rs.DONE) { + break; + } + const i: rs.usize = _33[0]; + const byte: rs.u8 = _33[1]; + const _38: rs.u8 = rs.u8Rem(byte, 4); + if (_38 === 0) { + const keyed: rs.Arc = new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))); + const arc3 = keyed; + const p: Keyed.Keyed = rs.arcRaw(arc3, arc3.value, "Arc"); + const address: rs.usize = rs.addr(p, 12, 4); + b5: { + if (rs.usizeRem(address, align) === 0) { + if (address !== 0) { + _52 = rs.addr(p, 12, 4) === address; + break b5; + } + } + _52 = false; + } + rs.byteVecPush(out, _52 ? 1 : 0); + rs.byteVecPush(out, rs.hashInsert(byAddress, address, keyed.value.id) === undefined ? 1 : 0); + const _70: rs.RefCell> = rs.refCellBorrowMut(registry.value); + const _73: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; + rs.btreeInsert(_70.value, _73, keyed.value.id); + rs.refMutRelease(_70); + if (!rs.btreeAdd(ordered, p)) { + _85 = false; + } else { + _85 = rs.hashAdd(hashed, p); + } + rs.byteVecPush(out, _85 ? 1 : 0); + const _91: [rs.Weak, rs.usize] = [new rs.Weak(keyed), address]; + weaks.items.push(_91); + live.items.push(keyed); + continue; + } + if (_38 === 1) { + if (!(live.items.length === 0)) { + const keyed2: rs.Arc = rs.vecRemove(live, rs.usizeRem(rs.u8Div(byte, 4) as rs.usize, live.items.length)); + const arc2 = keyed2; + const p2: Keyed.Keyed = rs.arcRaw(arc2, arc2.value, "Arc"); + rs.byteVecPush(out, rs.hashRemove(byAddress, rs.addr(p2, 12, 4)) === keyed2.value.id ? 1 : 0); + if (!rs.btreeDelete(ordered, p2)) { + _124 = false; + } else { + _124 = rs.hashDelete(hashed, p2); + } + rs.byteVecPush(out, _124 ? 1 : 0); + keyed2[Symbol.dispose](); + continue; + } + } else if (_38 === 2) { + const iterS: [rs.Weak, rs.usize][] = weaks.items; + let iterI: number = 0; + while (true) { + const _133: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; + if (_133 === rs.DONE) { + continue l1; + } + const at: rs.usize = rs.addr(rs.weakAsPtr(_133[0]), 12, 4); + const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_133[0]), (keyed3: rs.Arc): boolean => { + const arc = keyed3; + const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4) === at; + keyed3[Symbol.dispose](); + return _02; + }); + rs.byteVecPush(out, rs.u8Add(at === _133[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); + rs.byteVecPush(out, rs.hashContains(byAddress, at) === alive ? 1 : 0); + } + } + const addresses: rs.Vec = rs.iterToVec(rs.iterMap(rs.btreeKeys(ordered), (_2: Keyed.Keyed): rs.usize => { + return rs.addr(_2, 12, 4); + }), undefined, 4, true); + if (!rs.sliceIsSorted(rs.sliceFrom(addresses.items), rs.le)) { + _165 = false; + } else { + _165 = addresses.items.length === live.items.length; + } + rs.byteVecPush(out, _165 ? 1 : 0); + const iter3: rs.Iter<[rs.Arc, rs.Arc]> = rs.iterZip(rs.iterSlice(live.items), rs.iterSkip(rs.iterSlice(live.items), 1), [false, false]); + while (true) { + const _183: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); + if (_183 === rs.DONE) { + break; + } + const arc4 = _183[0]; + const _191: Keyed.Keyed = rs.arcRaw(arc4, arc4.value, "Arc"); + const arc5 = _183[1]; + b11: { + const _190: [Keyed.Keyed, Keyed.Keyed] = [_191, rs.arcRaw(arc5, arc5.value, "Arc")]; + const p3: Keyed.Keyed = _191; + const q: Keyed.Keyed = _190[1]; + if (rs.addr(p3, 12, 4) < rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) < rs.addr(q, 12, 4)) { + if (rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4)) { + if (rs.cmpAddr(12, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4), rs.addr(q, 12, 4))) { + agree = p3 !== q; + break b11; + } + } + } + agree = false; + } + rs.byteVecPush(out, agree ? 1 : 0); + } + const registry2: rs.RefCell> = rs.refCellBorrow(registry.value); + let iterStart: rs.u8 = 0; + const iterEnd: rs.u8 = 4; + while ((_224 = iterStart++) < iterEnd) { + const group: rs.u8 = _224; + rs.byteVecPush(out, rs.iterCount(rs.btreeRange(registry2.value, new rs.Range([group, 0] as [rs.u8, rs.usize], [rs.u8Add(group, 1), 0] as [rs.u8, rs.usize]))) & 0xff); + } + rs.byteVecPush(out, rs.iterAll(rs.btreeValues(registry2.value), (id: rs.u8): boolean => { + return rs.iterAny(rs.hashValues(byAddress), (live2: rs.u8): boolean => { + return live2 === id; + }); + }) ? 1 : 0); + rs.refRelease(registry2); + } + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_211: [rs.Weak, rs.usize]): boolean => { + const _510: rs.Arc | undefined = rs.weakUpgrade(_211[0]); + const _06: boolean = _510 !== undefined; + rs.drop(_510); + return _06; + })) & 0xff)), undefined, true); + rs.dropVec(live); + const _263: rs.u8 = left.value.v; + const _270: rs.RefCell> = rs.refCellBorrow(registry.value); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_263, rs.btreeLen(_270.value) === 0 ? 1 : 0)), undefined, true); + rs.refRelease(_270); + rs.dropVec(weaks, drop$tuple2_sync_Weak_pointers_Keyed_usize); + left[Symbol.dispose](); + registry[Symbol.dispose](); +} + //# sourceMappingURL=pointers.ts.map diff --git a/ts/fixtures/lang/pointers/src/lib.rs b/ts/fixtures/lang/pointers/src/lib.rs index 6646adfc2..58c65c848 100644 --- a/ts/fixtures/lang/pointers/src/lib.rs +++ b/ts/fixtures/lang/pointers/src/lib.rs @@ -48,5 +48,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("pointers::distinct", |_, data, out| pointers::distinct(data, out)), ("pointers::raw_arcs", |_, data, out| pointers::raw_arcs(data, out)), ("pointers::raw_places", |_, data, out| pointers::raw_places(data, out)), + ("pointers::addresses", |_, data, out| pointers::addresses(data, out)), ] } diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 2065c8438..9f129f6bf 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -10,6 +10,7 @@ //! raw pointers of an `Arc` and an `Rc`, counted and taken back. use std::cell::{Cell, UnsafeCell}; +use std::collections::BTreeMap; use std::mem::{self, MaybeUninit}; use std::ptr::{self, NonNull}; use std::rc::Rc; @@ -2271,3 +2272,104 @@ pub fn raw_places(data: &[u8], out: &mut Vec) { /// What holds an `Arc` a reference to it borrows through. struct Shelf(Arc); + +/// A value keyed by its address in a registry it leaves as it drops, as a cache keys +/// what it tracks: by `Arc::as_ptr` as it is made, and by `ptr::from_ref` and a cast of +/// `self` as it drops, which must be the same address. +struct Keyed { + id: u8, + registry: Rc>>, + left: Rc>, +} + +impl Drop for Keyed { + fn drop(&mut self) { + let address = ptr::from_ref::(self) as usize; + let removed = self.registry.borrow_mut().remove(&(self.id % 4, address)); + let same = self as *const Self as usize == address; + if removed == Some(self.id) && same { + self.left.set(self.left.get().wrapping_add(1)); + } + } +} + +/// Addresses read as integers, as `addr`, and as a raw pointer's `Hash` and `Ord` read +/// them: made `Arc`s and dropped ones keyed by address in a B-tree, a hash map, and sets of +/// pointers, which must stay unique and aligned among those alive, order pointers as their +/// addresses, and stay a `Weak`'s once its value drops. Only what Rust fixes is written: +/// equality, counts, alignment, and the order's agreement, never an address itself. +pub fn addresses(data: &[u8], out: &mut Vec) { + use std::collections::{BTreeSet, HashMap, HashSet}; + use std::sync::Weak; + let registry = Rc::new(std::cell::RefCell::new(BTreeMap::new())); + let left = Rc::new(Cell::new(0u8)); + let align = align_of::(); + let mut by_address: HashMap = HashMap::new(); + let mut live: Vec> = Vec::new(); + let mut weaks: Vec<(Weak, usize)> = Vec::new(); + let mut ordered: BTreeSet<*const Keyed> = BTreeSet::new(); + let mut hashed: HashSet<*const Keyed> = HashSet::new(); + let dangling = Weak::::new().as_ptr(); + out.extend( + [ + dangling as usize == usize::MAX, + ptr::null::() as usize == 0, + NonNull::::dangling().as_ptr().addr() == align, + ] + .map(u8::from), + ); + for (i, &byte) in data.iter().enumerate() { + match byte % 4 { + 0 => { + let keyed = Arc::new(Keyed { + id: i as u8, + registry: Rc::clone(®istry), + left: Rc::clone(&left), + }); + let p = Arc::as_ptr(&keyed); + let address = p as usize; + out.push(u8::from(address % align == 0 && address != 0 && p.addr() == address)); + out.push(u8::from(by_address.insert(address, keyed.id).is_none())); + registry.borrow_mut().insert((keyed.id % 4, p.expose_provenance()), keyed.id); + out.push(u8::from(ordered.insert(p) && hashed.insert(p))); + weaks.push((Arc::downgrade(&keyed), address)); + live.push(keyed); + } + 1 if !live.is_empty() => { + let keyed = live.remove(usize::from(byte / 4) % live.len()); + let p = Arc::as_ptr(&keyed); + out.push(u8::from(by_address.remove(&(p as usize)) == Some(keyed.id))); + out.push(u8::from(ordered.remove(&p) && hashed.remove(&p))); + } + // A `Weak` keeps its address once its value drops, as it keeps the allocation. + 2 => { + for (weak, address) in &weaks { + let at = weak.as_ptr() as usize; + let alive = weak.upgrade().is_some_and(|keyed| Arc::as_ptr(&keyed) as usize == at); + out.push(u8::from(at == *address) + 2 * u8::from(alive)); + out.push(u8::from(by_address.contains_key(&at) == alive)); + } + } + _ => { + let addresses: Vec = ordered.iter().map(|&p| p as usize).collect(); + out.push(u8::from(addresses.is_sorted() && addresses.len() == live.len())); + for (a, b) in live.iter().zip(live.iter().skip(1)) { + let (p, q) = (Arc::as_ptr(a), Arc::as_ptr(b)); + let agree = (p < q) == ((p as usize) < (q as usize)) + && (p >= q) == ((p as usize) >= (q as usize)) + && p.cmp(&q) == (p as usize).cmp(&(q as usize)) + && p != q; + out.push(u8::from(agree)); + } + let registry = registry.borrow(); + for group in 0..4u8 { + out.push(registry.range((group, 0)..(group + 1, 0)).count() as u8); + } + out.push(u8::from(registry.values().all(|id| by_address.values().any(|live| live == id)))); + } + } + } + out.extend([left.get(), weaks.iter().filter(|(weak, _)| weak.upgrade().is_some()).count() as u8]); + drop(live); + out.extend([left.get(), u8::from(registry.borrow().is_empty())]); +} diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 6e4576b9a..b62e0c7af 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -1,4 +1,22 @@ -fixtures/reject/src/alloc.rs:addressed+1:17: a pointer's address, which JS has no counterpart of +fixtures/reject/src/addresses.rs:cyclic+3:10: `Weak::as_ptr` of a `Weak`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns +fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds many of in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:made+1:5: a pointer made of an address, which JS has no memory at +fixtures/reject/src/addresses.rs:made+1:20: a pointer made of an address, which JS has no memory at +fixtures/reject/src/addresses.rs:made+1:54: a cast from `usize` to `*const addresses::Element`, between a pointer and an integer +fixtures/reject/src/addresses.rs:parts+1:34: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:parts+1:76: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:place_keys+1:13: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+1:47: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:place_keys+2:15: comparing pointers to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+2:15: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+2:48: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:place_keys+3:32: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:place_keys+3:32: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+3:50: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:places+1:2: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:places+1:35: a raw pointer to a place of `()`, which has no address of its own +fixtures/reject/src/addresses.rs:places+1:35: the address of a pointer to `()`, which has no bytes and so no address of its own +fixtures/reject/src/alloc.rs:addressed+1:17: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/alloc.rs:alloc_retyped+4:7: memory from `alloc` cast to pointers to `u32` and to `i32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:alloc_retyped+5:11: 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:alloc_retyped+7:17: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another @@ -174,10 +192,10 @@ fixtures/reject/src/pointers.rs:assigned_through+2:10: a raw pointer to `(u32, u fixtures/reject/src/pointers.rs:byte_option_slice+1:4: a slice of one byte's place, which a byte slice can view only in an array of bytes fixtures/reject/src/pointers.rs:byte_pointer_ops+5:3: a copy between pointers to `[u8; 4]`, which JS keeps as an object that knows no array it is in fixtures/reject/src/pointers.rs:byte_pointer_ops+6:3: a copy between pointers to `[u8; 4]`, which JS keeps as an object that knows no array it is in -fixtures/reject/src/pointers.rs:byte_pointer_ops+7:14: comparing `*mut [u8; 4]` +fixtures/reject/src/pointers.rs:byte_pointer_ops+7:14: the address of a pointer to `[u8; 4]`, a view JS makes of its own of bytes that may be in a larger array fixtures/reject/src/pointers.rs:byte_pointer_ops+8:56: an offset of a pointer to `[u8; 4]`, which JS keeps as an object that knows no array it is in fixtures/reject/src/pointers.rs:byte_pointer_ops+8:73: an offset of a pointer to `[u8; 4]`, which JS keeps as an object that knows no array it is in -fixtures/reject/src/pointers.rs:byte_pointer_ops+11:5: hashing `*const [u8; 4]` by its address, which JS has no counterpart of +fixtures/reject/src/pointers.rs:byte_pointer_ops+11:5: the address of a pointer to `[u8; 4]`, a view JS makes of its own of bytes that may be in a larger array fixtures/reject/src/pointers.rs:byte_pointer_parts+3:15: a raw pointer to `[u8; 3]` in an element, a payload, or a union field, which is not supported yet fixtures/reject/src/pointers.rs:byte_pointer_parts+3:18: a raw pointer to `[u8; 3]` in an element, a payload, or a union field, which is not supported yet fixtures/reject/src/pointers.rs:byte_pointer_parts+3:37: a raw pointer to `[u8; 2]` in an element, a payload, or a union field, which is not supported yet diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 96445c56c..439f97d1c 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -2,7 +2,25 @@ no item `bound_ranges` in the crate no item `clones::slice_of_object` in the crate no item `lopsided` in the crate no item `missing` in the crate -fixtures/reject/src/alloc.rs:addressed+1:17: a pointer's address, which JS has no counterpart of +fixtures/reject/src/addresses.rs:cyclic+3:10: `Weak::as_ptr` of a `Weak`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns +fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds many of in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:made+1:5: a pointer made of an address, which JS has no memory at +fixtures/reject/src/addresses.rs:made+1:20: a pointer made of an address, which JS has no memory at +fixtures/reject/src/addresses.rs:made+1:54: a cast from `usize` to `*const addresses::Element`, between a pointer and an integer +fixtures/reject/src/addresses.rs:parts+1:34: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:parts+1:76: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:place_keys+1:13: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+1:47: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:place_keys+2:15: comparing pointers to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+2:15: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+2:48: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:place_keys+3:32: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:place_keys+3:32: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:place_keys+3:50: a raw pointer to `u32` behind a reference, which is the object and not where it is kept +fixtures/reject/src/addresses.rs:places+1:2: the address of a pointer to `u32`, each a place JS makes of its own +fixtures/reject/src/addresses.rs:places+1:35: a raw pointer to a place of `()`, which has no address of its own +fixtures/reject/src/addresses.rs:places+1:35: the address of a pointer to `()`, which has no bytes and so no address of its own +fixtures/reject/src/alloc.rs:addressed+1:17: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/alloc.rs:alloc_retyped+4:7: memory from `alloc` cast to pointers to `u32` and to `i32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:alloc_retyped+5:11: 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:alloc_retyped+7:17: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another @@ -176,10 +194,10 @@ fixtures/reject/src/pointers.rs:assigned_through+2:10: a raw pointer to `(u32, u fixtures/reject/src/pointers.rs:byte_option_slice+1:4: a slice of one byte's place, which a byte slice can view only in an array of bytes fixtures/reject/src/pointers.rs:byte_pointer_ops+5:3: a copy between pointers to `[u8; 4]`, which JS keeps as an object that knows no array it is in fixtures/reject/src/pointers.rs:byte_pointer_ops+6:3: a copy between pointers to `[u8; 4]`, which JS keeps as an object that knows no array it is in -fixtures/reject/src/pointers.rs:byte_pointer_ops+7:14: comparing `*mut [u8; 4]` +fixtures/reject/src/pointers.rs:byte_pointer_ops+7:14: the address of a pointer to `[u8; 4]`, a view JS makes of its own of bytes that may be in a larger array fixtures/reject/src/pointers.rs:byte_pointer_ops+8:56: an offset of a pointer to `[u8; 4]`, which JS keeps as an object that knows no array it is in fixtures/reject/src/pointers.rs:byte_pointer_ops+8:73: an offset of a pointer to `[u8; 4]`, which JS keeps as an object that knows no array it is in -fixtures/reject/src/pointers.rs:byte_pointer_ops+11:5: hashing `*const [u8; 4]` by its address, which JS has no counterpart of +fixtures/reject/src/pointers.rs:byte_pointer_ops+11:5: the address of a pointer to `[u8; 4]`, a view JS makes of its own of bytes that may be in a larger array fixtures/reject/src/pointers.rs:byte_pointer_parts+3:15: a raw pointer to `[u8; 3]` in an element, a payload, or a union field, which is not supported yet fixtures/reject/src/pointers.rs:byte_pointer_parts+3:18: a raw pointer to `[u8; 3]` in an element, a payload, or a union field, which is not supported yet fixtures/reject/src/pointers.rs:byte_pointer_parts+3:37: a raw pointer to `[u8; 2]` in an element, a payload, or a union field, which is not supported yet diff --git a/ts/fixtures/reject/src/addresses.rs b/ts/fixtures/reject/src/addresses.rs new file mode 100644 index 000000000..4664e6d4c --- /dev/null +++ b/ts/fixtures/reject/src/addresses.rs @@ -0,0 +1,58 @@ +//! Addresses read as integers where JS has no exact counterpart: of values one allocation +//! holds many of or beside another whose address is read, of places, of values of no +//! bytes, of a `Weak` whose value `Arc::new_cyclic` makes, and pointers made of addresses. + +use std::sync::{Arc, Weak}; + +/// What a `Vec` holds many of. +pub struct Element(pub u32); + +/// The addresses of a `Vec`'s elements, which Rust keeps a fixed distance apart. +pub fn elements(v: &Vec) -> Option { + v.iter().map(|e| std::ptr::from_ref(e) as usize).max() +} + +/// One of two parts of a `Pair`. +pub struct Part(pub u32); + +/// What holds two `Part`s. +pub struct Pair { + pub first: Part, + pub second: Part, +} + +/// The addresses of a struct's two parts, which Rust orders as its layout does. +pub fn parts(pair: &Pair) -> bool { + std::ptr::from_ref(&pair.first).addr() < std::ptr::from_ref(&pair.second).addr() +} + +/// The address of a place, which JS makes anew each borrow, and of a value of no bytes. +pub fn places(n: &u32, unit: &()) -> usize { + std::ptr::from_ref(n) as usize ^ std::ptr::from_ref(unit) as usize +} + +/// Raw pointers to places ordered and hashed by their addresses. +pub fn place_keys(a: &u32, b: &u32) -> bool { + let keys = std::collections::BTreeSet::from([&raw const *a]); + let hashed = std::collections::HashSet::from([&raw const *b]); + keys.len() == hashed.len() && (&raw const *a) < (&raw const *b) +} + +/// What `Arc::new_cyclic` makes, whose `Weak` points at no value until it returns. +pub struct Cyclic { + pub me: Weak, + pub at: usize, +} + +/// `Weak::as_ptr` before the value is made. +pub fn cyclic() -> Arc { + Arc::new_cyclic(|me| Cyclic { + me: me.clone(), + at: me.as_ptr() as usize, + }) +} + +/// Pointers made of addresses, which JS has no memory at. +pub fn made(p: *const Element, at: usize) -> [*const Element; 3] { + [p.with_addr(at), std::ptr::without_provenance(at), at as *const Element] +} diff --git a/ts/fixtures/reject/src/lib.rs b/ts/fixtures/reject/src/lib.rs index 83d03c654..31f86f10c 100644 --- a/ts/fixtures/reject/src/lib.rs +++ b/ts/fixtures/reject/src/lib.rs @@ -4,6 +4,7 @@ #![allow(dead_code, reason = "each item is a translation root, not called from Rust")] #![feature(cow_is_borrowed, extend_one, f16, is_ascii_octdigit, linked_list_retain)] +mod addresses; mod alloc; mod buffers; mod clones; diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 37e1692d3..63307af4e 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -1,5 +1,11 @@ package = "reject-fixture" roots = [ + "addresses::cyclic", + "addresses::elements", + "addresses::made", + "addresses::parts", + "addresses::place_keys", + "addresses::places", "alloc::addressed", "alloc::alloc_retyped", "alloc::allocated", diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index 9e0238929..e7a45fb99 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -298,7 +298,7 @@ const UNHELD = /* @__PURE__ */ new FinalizationRegistry((addr) => { }); /** The pointer at `addr`, which no memory is at: the same `Address` as every other one there. */ -function addressAt(addr: number): Address { +export function addressAt(addr: number): Address { let at = ADDRESSES.get(addr)?.deref(); if (at === undefined) { at = new Address(addr); @@ -312,6 +312,47 @@ function nowhere(): never { throw new Error("rust2-ts: a read or write through a pointer to no memory"); } +/** The address `addr` gave each object, while JS keeps it. */ +const ADDRESSED = /* @__PURE__ */ new WeakMap(); + +/** The addresses of objects JS no longer keeps, by the size and alignment of their blocks. */ +const FREED = /* @__PURE__ */ new Map(); + +/** Free an object's address once JS no longer keeps it, as an allocator reuses a freed block. */ +const UNADDRESSED = /* @__PURE__ */ new FinalizationRegistry<[string, number]>(([block, at]) => { + let free = FREED.get(block); + if (free === undefined) FREED.set(block, (free = [])); + free.push(at); +}); + +/** Where the next new block starts: past wasm32's first page, as a heap is. */ +let brk = 0x10000; + +/** + * `p as usize`, of a pointer to a value of `size` bytes aligned to `align`: null's 0, a + * dangling pointer's `align`, an address no memory is at, or else the object's own, + * as an allocator places each allocation: a multiple of `align`, in a block no other + * object JS keeps overlaps, with room before it for an `Arc`'s counts. Running out of + * wasm32's addresses aborts, as a failed allocation does. + */ +export function addr(p: object, size: number, align: number): number { + if (p === NULL) return 0; + if (p === DANGLING) return align; + if (p instanceof Address) return p.addr; + let at = ADDRESSED.get(p); + if (at !== undefined) return at; + const block = `${size}:${align}`; + at = FREED.get(block)?.pop(); + if (at === undefined) { + at = Math.ceil((brk + Math.max(16, align)) / align) * align; + if (at + size > 2 ** 32) throw new Abort(`memory allocation of ${size} bytes failed`); + brk = at + size; + } + ADDRESSED.set(p, at); + UNADDRESSED.register(p, [block, at]); + return at; +} + /** The array of one element that a pointer to a value in no array points into, by its place. */ const LONE = /* @__PURE__ */ new WeakMap, unknown[]>(); diff --git a/ts/runtime/rs/collections.ts b/ts/runtime/rs/collections.ts index 93b77cf1d..2b524489f 100644 --- a/ts/runtime/rs/collections.ts +++ b/ts/runtime/rs/collections.ts @@ -15,7 +15,7 @@ // A function that may drop a key, value, or item std's would, rather than return or keep // it, takes what drops each that does, as `k`, `v`, or `f`, and drops it where std's does. -import { ByteSlice, ByteVec, byteVecPush, byteVecReserve, capReserve, capReserveExact, capTryReserve, capTryReserveExact, type CloneFrom, type Debug, debugStructField, debugStructFinishNonExhaustive, Err, type Fmt, fmtDebugStruct, fmtDebugStructFields, fmtDebugTupleFields, fmtIter, fmtList, type FmtResult, fmtSet, fmtTuple, type Formatter, type None, OK, Ok, panic, type Place, type RangeBound, rangeCheck, type Result, searchBy, type Elements, Slice, sliceData, sliceFrom, sliceLength, sliceSort, sliceStart, Vec, vecAppend, vecCapacity, vecCloneFrom, vecFrom, vecHold, vecKeep, vecMax, vecReserve, vecWithCapacity } from "../rs.ts"; +import { addr, ByteSlice, ByteVec, byteVecPush, byteVecReserve, capReserve, capReserveExact, capTryReserve, capTryReserveExact, type CloneFrom, type Debug, debugStructField, debugStructFinishNonExhaustive, Err, type Fmt, fmtDebugStruct, fmtDebugStructFields, fmtDebugTupleFields, fmtIter, fmtList, type FmtResult, fmtSet, fmtTuple, type Formatter, type None, OK, Ok, panic, type Place, type RangeBound, rangeCheck, type Result, searchBy, type Elements, Slice, sliceData, sliceFrom, sliceLength, sliceSort, sliceStart, Vec, vecAppend, vecCapacity, vecCloneFrom, vecFrom, vecHold, vecKeep, vecMax, vecReserve, vecWithCapacity } from "../rs.ts"; import { allocError, CAPACITY_OVERFLOW, type TryReserveError } from "./alloc.ts"; import type { Hashing } from "./hash.ts"; import { type Limbs, lt, type u64, type usize } from "./int.ts"; @@ -64,6 +64,11 @@ export function cmp(a: T, b: T): Ordering { return lt(x, y) ? -1 : lt(y, x) ? 1 : 0; } +/** `Ord::cmp` of raw pointers to values of `size` bytes aligned to `align`, which compares their addresses. */ +export function cmpAddr(size: number, align: number): (a: object, b: object) => Ordering { + return (a, b) => cmp(addr(a, size, align), addr(b, size, align)); +} + /** * `Ord::cmp` of two strings, by code point as Rust's UTF-8 bytes sort. JS's `<` compares * UTF-16 units, which puts a character past U+FFFF, a surrogate pair, below U+E000. diff --git a/ts/runtime/rs/hash.ts b/ts/runtime/rs/hash.ts index 5762270f8..b80ca8fc1 100644 --- a/ts/runtime/rs/hash.ts +++ b/ts/runtime/rs/hash.ts @@ -5,7 +5,7 @@ // A hash collection hashes a key that is a JS object with a faster hasher of its own, // whose values nothing sees, as std keys a map's hasher at random. -import { ByteSlice, type ByteVec, type Elements, type None, Slice, sliceData, sliceItems, sliceLength, sliceStart, Vec } from "../rs.ts"; +import { addr, ByteSlice, type ByteVec, type Elements, type None, Slice, sliceData, sliceItems, sliceLength, sliceStart, Vec } from "../rs.ts"; import { type i8, type i16, @@ -639,6 +639,11 @@ export function hashUsize(value: number, s: Hasher): void { s.usize(value); } +/** `Hash::hash` of a raw pointer to a value of `size` bytes aligned to `align`, which hashes its address. */ +export function hashAddr(size: number, align: number): (p: object, s: Hasher) => void { + return (p, s) => s.usize(addr(p, size, align)); +} + /** `Hash::hash` of an `i8`, or of a discriminant of `Ordering`, whose `repr` it is. */ export function hashI8(value: number, s: Hasher): void { s.i8(value); diff --git a/ts/runtime/rs/task.ts b/ts/runtime/rs/task.ts index 26db5ac64..4b11ff5d5 100644 --- a/ts/runtime/rs/task.ts +++ b/ts/runtime/rs/task.ts @@ -4,7 +4,7 @@ // A value whose drop JS can observe has `[Symbol.dispose]`, which runs its `Drop` impl // and drops its fields. `None` is `undefined`, which drops as nothing. -import { Abort, arrayAssign, type ByteSlice, type CloneFrom, cloneOver, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtDebugTupleFields, fmtPad, fmtStr, type None, NULL, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrErase, ptrHome, ptrNowhere, ptrWrite, type Result, Slice, stringAsBytes, type Vec } from "../rs.ts"; +import { Abort, addressAt, arrayAssign, type ByteSlice, type CloneFrom, cloneOver, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtDebugTupleFields, fmtPad, fmtStr, type None, NULL, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrErase, ptrHome, ptrNowhere, ptrWrite, type Result, Slice, stringAsBytes, type Vec } from "../rs.ts"; import type { Hasher } from "./hash.ts"; import type { Location } from "./location.ts"; @@ -400,6 +400,11 @@ export class Weak implements Disposable { } } +/** `Weak::as_ptr`, to an object: the object, or `Weak::new`'s address, `usize::MAX`. */ +export function weakAsPtr(weak: Weak): T { + return (weak.arc === DANGLING ? addressAt(0xffffffff) : weak.arc.value) as T; +} + /** `weak`, a `Weak`, unsized to a `Weak` of what `view` makes of its `Arc`. */ export function weakDyn(weak: Weak, view: (arc: Arc) => Arc): Weak { const dyn = new Weak(weak.arc as Arc, view as (arc: Arc) => Arc); diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs new file mode 100644 index 000000000..25d4e72c0 --- /dev/null +++ b/ts/src/lower/address.rs @@ -0,0 +1,258 @@ +//! Addresses read as integers: `p as usize`, `addr`, and `expose_provenance`. +//! +//! Rust promises of an allocation's address only that it is aligned and that no other +//! live allocation overlaps it, so `rs.addr` numbers each object as an allocator would +//! place it, on its first read, while JS keeps the object. A pointer's address is then +//! exact wherever its identity is, as [`Translator::unidentified`] decides for comparing +//! pointers, with one more condition: Rust fixes how far apart two values in one +//! allocation are, as the fields of a struct or the elements of a `Vec` are, which the +//! objects JS keeps apart can't show. So an address is read only of a type no allocation +//! holds beside another whose address is read, nor holds many of. Nothing makes a pointer +//! of an address, which JS has no memory at. + +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; +use rustc_span::Span; + +use super::Translator; +use super::body::Body; +use super::library::Std; +use super::pointer::Pointee; +use crate::ts::Expr; + +/// The address of `p`, a pointer to `ty`, as a `usize`. +pub fn address<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> Expr { + match read(b, ty, span) { + Ok([size, align]) => Expr::rs("addr", vec![p, size, align]), + Err(refused) => refused, + } +} + +/// The size and alignment `rs.addr` reads the address of a pointer to `ty` by, or the +/// refusal of a type whose address JS has no counterpart of. +pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Result<[Expr; 2], Expr> { + let (tcx, env) = (b.t.tcx, b.t.typing_env_pub()); + // Only an instance decides what a parameter is, and is refused on its own. + if !(b.t.polymorphic() && ty.has_param()) { + let why = match b.t.pointee(ty) { + _ if b.t.zero_sized(ty) => Some("which has no bytes and so no address of its own"), + Pointee::Bytes => Some("a view JS makes of its own of bytes that may be in a larger array"), + _ if b.t.erased(ty) != ty => Some("which JS keeps as what it holds"), + _ => b.t.unidentified(ty), + }; + let what = format!("the address of a pointer to `{ty}`"); + if let Some(why) = why { + return Err(b.err(span, format!("{what}, {why}"))); + } + let read = b.t.sited(span, what); + b.t.addresses.push((ty, read.0, read.1)); + } + let layout = tcx.layout_of(env.as_query_input(ty)).ok(); + let size = layout.map_or(0, |l| l.size.bytes()); + let align = layout.map_or(1, |l| l.align.abi.bytes()); + Ok([Expr::lit(size.to_string()), Expr::lit(align.to_string())]) +} + +impl<'a, 'tcx> Translator<'a, 'tcx> { + /// Note `Arc::new_cyclic` of a `ty`, whose value JS has none of until its closure + /// returns, so a `Weak::as_ptr` of one has nothing to point at. + pub fn cyclic(&mut self, ty: Ty<'tcx>) { + self.cyclic.insert(self.tcx.erase_and_anonymize_regions(ty)); + } + + /// Note `Weak::as_ptr` of a `Weak` at `span`, refused once translation settles if + /// the crate makes a `ty` by `Arc::new_cyclic`. + pub fn weak_pointer(&mut self, ty: Ty<'tcx>, span: Span) { + let msg = format!( + "`Weak::as_ptr` of a `Weak<{ty}>`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns" + ); + let (span, msg) = self.sited(span, msg); + self.weak_pointers.push((self.tcx.erase_and_anonymize_regions(ty), span, msg)); + } + + /// Refuse each address read of a type an allocation holds beside another whose address + /// is read, or many of, and each `Weak::as_ptr` of a value `Arc::new_cyclic` makes. + pub(super) fn refuse_addresses(&mut self) { + for (ty, span, msg) in std::mem::take(&mut self.weak_pointers) { + if self.cyclic.contains(&ty) { + self.refuse(span, msg); + } + } + let reads = std::mem::take(&mut self.addresses); + if reads.is_empty() { + return; + } + let tcx = self.tcx; + let addressed: FxHashSet> = reads.iter().map(|&(ty, ..)| super::copy::storage(tcx, ty)).collect(); + let shared = shared(tcx, &addressed, self.writes.locals(tcx)); + for (ty, span, msg) in reads { + if let Some(&(holder, many)) = shared.get(&super::copy::storage(tcx, ty)) { + let held = match many { + true => "many of", + false => "beside another value whose address is read,", + }; + let why = format!( + "{msg}, which `{holder}` holds {held} in one allocation, as far apart as Rust fixes and JS can't show" + ); + self.refuse(span, why); + } + } + } +} + +/// Each of `addressed` that an allocation holds beside another, or many of, by the type +/// it holds them in and whether it holds many, of the allocations `locals` hold or point at. +fn shared<'tcx>( + tcx: TyCtxt<'tcx>, + addressed: &FxHashSet>, + locals: impl Iterator>, +) -> FxHashMap, (Ty<'tcx>, bool)> { + let mut held = Held::new(tcx, addressed); + held.roots.extend(locals.map(|ty| (ty, false))); + let mut seen = FxHashSet::default(); + let mut shared = FxHashMap::default(); + while let Some((ty, many)) = held.roots.pop() { + if !seen.insert((ty, many)) { + continue; + } + let n = held.count(ty); + if n < 2 && !(many && n == 1) { + continue; + } + for &each in addressed { + let one = FxHashSet::from_iter([each]); + if Held::new(tcx, &one).count(ty) > 0 { + shared.entry(each).or_insert((ty, many || matches!(ty.kind(), ty::Array(..) | ty::Slice(_)))); + } + } + } + shared +} + +/// How many values of the `addressed` types a value holds, and the allocations it points +/// at, each with whether it may hold many. +struct Held<'s, 'tcx> { + tcx: TyCtxt<'tcx>, + addressed: &'s FxHashSet>, + counts: FxHashMap, u8>, + roots: Vec<(Ty<'tcx>, bool)>, +} + +impl<'s, 'tcx> Held<'s, 'tcx> { + fn new(tcx: TyCtxt<'tcx>, addressed: &'s FxHashSet>) -> Self { + Held { + tcx, + addressed, + counts: FxHashMap::default(), + roots: Vec::new(), + } + } + + /// How many values of the addressed types a `ty` holds by value, up to 2, which is + /// many. A std type holds its type arguments as its kind says, and the crate's own + /// raw pointers each point at one value, as no arithmetic moves one to an object. + fn count(&mut self, ty: Ty<'tcx>) -> u8 { + let tcx = self.tcx; + let env = ty::TypingEnv::fully_monomorphized(); + let ty = tcx + .try_normalize_erasing_regions(env, ty::Unnormalized::new_wip(ty)) + .unwrap_or(ty); + if let Some(&n) = self.counts.get(&ty) { + return n; + } + self.counts.insert(ty, 0); + let own = u8::from(self.addressed.contains(&super::copy::storage(tcx, ty))); + let parts = match *ty.kind() { + ty::Array(elem, len) => match (len.try_to_target_usize(tcx), self.count(elem)) { + (Some(0), _) | (_, 0) => 0, + (Some(1), n) => n, + _ => 2, + }, + ty::Slice(elem) => 2 * self.count(elem).min(1), + ty::Pat(elem, _) => self.count(elem), + ty::Tuple(tys) => total(tys.iter().map(|t| self.count(t))), + ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) => self.point(pointee, false), + ty::Adt(adt, args) if super::library::is_std(tcx, adt.did()) => match Std::of(tcx, adt.did()) { + // An allocation of one value, or a pointer to one. + Some(Std::Box | Std::Arc | Std::Weak | Std::NonNull) => self.point(args.type_at(0), false), + _ if adt.is_phantom_data() => 0, + Some(Std::Range | Std::RangeInclusive) => 2 * self.count(args.type_at(0)).min(1), + // Each of its type arguments, at most one of each, by value. + Some( + Std::Option + | Std::Result + | Std::Cow + | Std::Cell + | Std::RefCell + | Std::Mutex + | Std::OnceCell + | Std::OnceLock + | Std::LazyCell + | Std::LazyLock + | Std::Poll + | Std::Ready + | Std::Pin + | Std::ControlFlow + | Std::Bound + | Std::RangeTo + | Std::RangeFrom + | Std::RangeToInclusive + | Std::Reverse + | Std::Wrapping + | Std::Saturating + | Std::AssertUnwindSafe + | Std::MaybeUninit + | Std::ManuallyDrop, + ) => total(args.types().map(|t| self.count(t))), + _ if adt.is_unsafe_cell() => total(args.types().map(|t| self.count(t))), + // A collection, or anything else of std's, may hold many of each in one + // allocation of its own. + _ => { + self.roots.extend(args.types().map(|t| (t, true))); + 0 + } + }, + ty::Adt(adt, args) => { + let mut counts = adt + .variants() + .iter() + .map(|v| v.fields.iter().map(|f| self.count(f.ty(tcx, args).skip_norm_wip())).collect::>()); + match adt.is_union() { + // Its fields overlap, as an enum's variants do. + true => counts.next().into_iter().flatten().max().unwrap_or(0), + false => counts.map(|c| total(c.into_iter())).max().unwrap_or(0), + } + } + ty::Closure(_, args) => total(args.as_closure().upvar_tys().iter().map(|t| self.count(t))), + ty::CoroutineClosure(_, args) => total(args.as_coroutine_closure().upvar_tys().iter().map(|t| self.count(t))), + // Its captures, and the locals it keeps across an `.await`. + ty::Coroutine(def_id, args) => { + let upvars = total(args.as_coroutine().upvar_tys().iter().map(|t| self.count(t))); + let saved = match tcx.coroutine_layout(def_id, args) { + Ok(layout) => total(layout.field_tys.iter().map(|f| { + let saved = ty::EarlyBinder::bind(tcx, f.ty); + self.count(tcx.instantiate_and_normalize_erasing_regions(args, env, saved)) + })), + Err(_) => 0, + }; + upvars.saturating_add(saved) + } + _ => 0, + }; + let n = own.saturating_add(parts).min(2); + self.counts.insert(ty, n); + n + } + + /// A pointer to `pointee`, an allocation of its own, which holds nothing beside the + /// pointer: of many where `many` says it may be. + fn point(&mut self, pointee: Ty<'tcx>, many: bool) -> u8 { + self.roots.push((pointee, many)); + 0 + } +} + +/// The sum of `counts`, up to many. +fn total(counts: impl Iterator) -> u8 { + counts.fold(0, u8::saturating_add) +} diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index fee306796..35374a165 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -3028,10 +3028,10 @@ pub fn cmp_fn<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, partial: bool, spa let cmp = trait_fn(tcx, "Ord", "cmp"); b.fn_value(cmp, tcx.mk_args(&[ty.into()]), span) } - _ if raw_pointee(tcx, ty).is_some() => b.err( - span, - format!("ordering `{ty}` by its address, which JS has no counterpart of"), - ), + _ if let Some(pointee) = raw_pointee(tcx, ty) => match super::address::read(b, pointee, span) { + Ok(layout) => Expr::rs("cmpAddr", layout.into()), + Err(refused) => refused, + }, // Each instance decides what its type parameters are. _ if b.t.deferred(ty) => Expr::undefined(), _ => b.err(span, format!("ordering `{ty}`, which is not supported yet")), @@ -3343,10 +3343,10 @@ fn hash_fn<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, hasher: Ty<'tcx>, spa false => f, } } - _ if raw_pointee(tcx, ty).is_some() => b.err( - span, - format!("hashing `{ty}` by its address, which JS has no counterpart of"), - ), + _ if let Some(pointee) = raw_pointee(tcx, ty) => match super::address::read(b, pointee, span) { + Ok(layout) => Expr::rs("hashAddr", layout.into()), + Err(refused) => refused, + }, _ if b.t.deferred(ty) => Expr::undefined(), _ => b.err(span, format!("hashing `{ty}` as a key, which is not supported yet")), } diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index b88d3d26f..092db490a 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -437,6 +437,9 @@ pub struct Writes<'tcx> { /// The arguments of each body's instances that translation reached, without type /// parameters. reached: FxHashMap>>, + /// The type of each local of each body read, by that body: every allocation the + /// crate's code makes is one, or what one points at. + locals: FxIndexSet<(DefId, Ty<'tcx>)>, } /// Types, each without type parameters or over those of the body naming it. @@ -481,6 +484,9 @@ impl<'tcx> Writes<'tcx> { // One without MIR, such as an import, is refused where it is reached. _ => continue, }; + for local in body.local_decls.iter() { + writes.locals.insert((def_id, tcx.erase_and_anonymize_regions(local.ty))); + } // Another crate lists none of its closures' bodies. for local in body.local_decls.iter() { queue.extend(local.ty.walk().filter_map(|arg| match *arg.as_type()?.kind() { @@ -523,6 +529,24 @@ impl<'tcx> Writes<'tcx> { .is_some_and(|args| args.contains(&instance.args)) } + /// The type of each local of the bodies read, one over type parameters for each + /// instance translation reached of its body. + pub fn locals(&self, tcx: TyCtxt<'tcx>) -> impl Iterator> { + let env = ty::TypingEnv::fully_monomorphized(); + self.locals.iter().flat_map(move |&(def_id, ty)| { + let instances: Vec> = match ty.has_param() { + false => vec![ty], + true => (self.reached.get(&def_id).into_iter().flatten()) + .filter_map(|&args| { + tcx.try_instantiate_and_normalize_erasing_regions(args, env, EarlyBinder::bind(tcx, ty)) + .ok() + }) + .collect(), + }; + instances + }) + } + /// Whether `ty` is one of `types`, or with `held`, a type one of them holds by value. /// Over type parameters, as a census lowers some functions, it may be. A write into a /// type parameter counts for the type each reached instance gives it: in a census, a @@ -568,7 +592,7 @@ impl<'tcx> Types<'tcx> { /// `ty` as [`Types`] keys it: without regions, a `MaybeUninit` or `ManuallyDrop` as what /// it holds, and an array as a slice of its element. -fn storage<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Ty<'tcx> { +pub(super) fn storage<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Ty<'tcx> { let mut ty = tcx.erase_and_anonymize_regions(ty); while let ty::Adt(adt, args) = ty.kind() && matches!(Std::of(tcx, adt.did()), Some(Std::MaybeUninit | Std::ManuallyDrop)) @@ -584,7 +608,7 @@ fn storage<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Ty<'tcx> { /// `ty` and each type it holds by value, at any depth: in a field, an element, or a /// payload. An array of no elements holds none, and a type parameter, or a type `env` /// can't normalize, stands for what it holds. -fn by_value<'tcx>(tcx: TyCtxt<'tcx>, env: ty::TypingEnv<'tcx>, ty: Ty<'tcx>) -> impl Iterator> { +pub(super) fn by_value<'tcx>(tcx: TyCtxt<'tcx>, env: ty::TypingEnv<'tcx>, ty: Ty<'tcx>) -> impl Iterator> { let mut held = FxIndexSet::default(); let mut stack = vec![ty]; while let Some(ty) = stack.pop() { diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index d416527c1..05c0b42e7 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -209,6 +209,25 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { _ => same.not(), }); } + // Ordered by their addresses. + if let ty::RawPtr(pointee, _) = lty.kind() + && let Some(js) = match op { + BinOp::Lt => Some("<"), + BinOp::Le => Some("<="), + BinOp::Gt => Some(">"), + BinOp::Ge => Some(">="), + BinOp::Cmp => Some("cmp"), + _ => None, + } { + let x = self.operand(l, span, out); + let x = crate::lower::address::address(self.b, x, *pointee, span); + let y = self.operand(r, span, out); + let y = crate::lower::address::address(self.b, y, *pointee, span); + return Some(match js { + "cmp" => Expr::rs("cmp", vec![x, y]), + _ => x.binary(js, y), + }); + } if matches!(op, BinOp::Cmp | BinOp::Offset) { return Some(self.err(span, format!("`{op:?}` is not supported"))); } @@ -477,6 +496,10 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { span, format!("a cast from `{from}` to `{to}`, which reads the bytes of one type as another"), ), + CastKind::PointerExposeProvenance if let ty::RawPtr(pointee, _) = from.kind() => { + let p = self.operand(op, span, out); + crate::lower::address::address(self.b, p, *pointee, span) + } CastKind::PointerExposeProvenance | CastKind::PointerWithExposedProvenance => self.err( span, format!("a cast from `{from}` to `{to}`, between a pointer and an integer"), diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index 51e76b99c..c03563a44 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -5,6 +5,7 @@ //! call is always a direct call. Types are translated once, generically, and a function of //! a type is a module-level one taking `self` first, so a bundler drops what goes unused. +pub mod address; mod alloc; mod bind; pub use bind::check_module; @@ -216,6 +217,14 @@ pub struct Translator<'a, 'tcx> { capacity_reads: Vec<(Span, String)>, /// Whether a crate makes a `Bytes` or a `Vec` of a `String`'s bytes, whose capacity JS keeps none of. string_bytes: bool, + /// Each read of an address, of a pointer to its type, refused once translation settles + /// if an allocation holds that type beside another read, as [`address`] says. + addresses: Vec<(Ty<'tcx>, Span, String)>, + /// The types `Arc::new_cyclic` makes. + cyclic: rustc_data_structures::fx::FxHashSet>, + /// Each `Weak::as_ptr`, by the type it points at, refused once translation settles if + /// `Arc::new_cyclic` makes one. + weak_pointers: Vec<(Ty<'tcx>, Span, String)>, /// Whether each type's drop does something JS can see, as [`Self::drops`] found. drops: rustc_data_structures::fx::FxHashMap, bool>, /// The types [`Self::drops`] is still deciding, and whether the query open now read one. @@ -444,6 +453,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { && (t.hooked || !t.hooking) { t.refuse_capacity_reads(); + t.refuse_addresses(); return Some((t, out)); } read = std::mem::take(&mut t.read); @@ -503,6 +513,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { refusals: Some(Vec::new()), capacity_reads: Vec::new(), string_bytes: false, + addresses: Vec::new(), + cyclic: Default::default(), + weak_pointers: Vec::new(), drops: Default::default(), drops_open: Default::default(), drops_cycle: false, diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index e7affe577..05fffc874 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -94,39 +94,35 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// no bytes is unspecified, but a pointer to `()` is an identity. Notes the comparison of /// those that do, for [`Self::taken`]. pub fn incomparable(&mut self, ty: Ty<'tcx>) -> Option { + self.unidentified(ty).map(|why| format!("comparing pointers to `{ty}`, {why}")) + } + + /// Why pointers to `ty` that are not null aren't identities JS keeps, as + /// [`Self::incomparable`] says, noting the comparison of those that are. + pub fn unidentified(&mut self, ty: Ty<'tcx>) -> Option<&'static str> { if self.identity_pointee(ty) { return None; } if self.zero_sized(ty) { - return Some(format!( - "comparing pointers to `{ty}`, which has no bytes and so no address of its own" - )); + return Some("which has no bytes and so no address of its own"); } // A slice lends a run of other values as an array of its own, by `first_chunk` or // `as_chunks`, so where one points says nothing of where the slice holds it. if let ty::Array(..) = self.uninit_inner(ty).kind() && self.pointee(ty) == Pointee::Object { - return Some(format!( - "comparing pointers to `{ty}`, which a slice lends as a copy of its own" - )); + return Some("which a slice lends as a copy of its own"); } if ty.is_trait() { - return Some(format!( - "comparing pointers to `{ty}`, each an object JS makes of its own as it unsizes" - )); + return Some("each an object JS makes of its own as it unsizes"); } match self.pointee(ty) { Pointee::Object | Pointee::Bytes => { self.compare(ty); None } - Pointee::Place => Some(format!( - "comparing pointers to `{ty}`, each a place JS makes of its own" - )), - Pointee::Slice => Some(format!( - "comparing pointers to `{ty}`, whose element pointers are places JS makes of their own" - )), + Pointee::Place => Some("each a place JS makes of its own"), + Pointee::Slice => Some("whose element pointers are places JS makes of their own"), } } @@ -540,8 +536,9 @@ pub fn call<'tcx>( let p = arg(); cast(b, p, pointee(), targ(gargs, 1), span) } - ("ptr" | "NonNull", "addr" | "expose_provenance" | "with_addr" | "map_addr" | "without_provenance") => { - b.err(span, "a pointer's address, which JS has no counterpart of") + ("ptr" | "NonNull", "addr" | "expose_provenance") => super::address::address(b, arg(), pointee(), span), + ("ptr" | "NonNull", "with_addr" | "map_addr" | "without_provenance") => { + b.err(span, "a pointer made of an address, which JS has no memory at") } // A raw pointer to a slice is null where its first element's is. ("ptr", "is_null") if kind(b, pointee()) == Some(Pointee::Slice) => { @@ -668,6 +665,20 @@ pub fn call<'tcx>( }; Expr::rs("arcRaw", vec![arc, p, Expr::Str(owner.into())]) } + // The object a `Weak` points at, there still once the last `Arc` drops it, as its + // allocation is. + ("Weak", "as_ptr") + if kind(b, pointee()) == Some(Pointee::Object) + && !pointee().is_trait() + && !super::dynamic::unsized_object(b.t.tcx, pointee()) => + { + b.t.weak_pointer(pointee(), span); + Expr::rs("weakAsPtr", vec![arg()]) + } + ("Weak", "as_ptr") => b.err( + span, + format!("a raw pointer to the `{}` a `Weak` holds, which JS keeps no object of", pointee()), + ), ("Arc" | "Rc", "from_raw") => Expr::rs("arcFromRaw", vec![arg(), Expr::Str(owner.into())]), ("Arc" | "Rc", "increment_strong_count") => { let arc = Expr::rs("arcFromRaw", vec![arg(), Expr::Str(owner.into())]); diff --git a/ts/src/lower/task.rs b/ts/src/lower/task.rs index 21b53a4fd..f59218c5a 100644 --- a/ts/src/lower/task.rs +++ b/ts/src/lower/task.rs @@ -138,7 +138,10 @@ pub fn call<'tcx>( let owned_std = self_std.filter(|_| self_ty.is_some_and(|t| !t.is_ref())); let e = match (owner, name) { ("Arc" | "Rc", "new") => Expr::New(Box::new(Expr::Rs("Arc".into())), args), - ("Arc" | "Rc", "new_cyclic") => Expr::rs("arcNewCyclic", args), + ("Arc" | "Rc", "new_cyclic") => { + b.t.cyclic(call.args.type_at(0)); + Expr::rs("arcNewCyclic", args) + } // JS erases the pin. ("Arc" | "Rc", "pin") => Expr::New(Box::new(Expr::Rs("Arc".into())), args), // The object of one of a trait object, which every pointer to it shares. A `dyn Any`'s From c372e5117d475db15418b73d112cbedc1dfa1879 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:12:19 -0800 Subject: [PATCH 03/13] docs(quest): record the census without moq-net's address reads Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index d87b41754..6517284ad 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -49,12 +49,12 @@ 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-04: 72 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-04: 65 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 +crossbeam-epoch, whose tagged pointers and inline closures (29 refusals) translation refuses for good, and smallvec's union over a type parameter and raw parts (10) wait on [unsafe](/quest/m3/ts-unsafe.md), so both need safe `cfg(rust2_ts)` twins or bindings there, as does the `unsafe` slice of `model/frame.rs`'s `Filled`. From 2f0a69224d71e8c8ae71de264e22df280a1f80a7 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:15:15 -0800 Subject: [PATCH 04/13] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/pointers/src/pointers.rs | 13 ++++++++++--- ts/src/lower/address.rs | 21 ++++++++++++++------- ts/src/lower/copy.rs | 10 ++++++++-- ts/src/lower/pointer.rs | 8 ++++++-- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 9f129f6bf..c4b1809f6 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2330,7 +2330,9 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let address = p as usize; out.push(u8::from(address % align == 0 && address != 0 && p.addr() == address)); out.push(u8::from(by_address.insert(address, keyed.id).is_none())); - registry.borrow_mut().insert((keyed.id % 4, p.expose_provenance()), keyed.id); + registry + .borrow_mut() + .insert((keyed.id % 4, p.expose_provenance()), keyed.id); out.push(u8::from(ordered.insert(p) && hashed.insert(p))); weaks.push((Arc::downgrade(&keyed), address)); live.push(keyed); @@ -2365,11 +2367,16 @@ pub fn addresses(data: &[u8], out: &mut Vec) { for group in 0..4u8 { out.push(registry.range((group, 0)..(group + 1, 0)).count() as u8); } - out.push(u8::from(registry.values().all(|id| by_address.values().any(|live| live == id)))); + out.push(u8::from( + registry.values().all(|id| by_address.values().any(|live| live == id)), + )); } } } - out.extend([left.get(), weaks.iter().filter(|(weak, _)| weak.upgrade().is_some()).count() as u8]); + out.extend([ + left.get(), + weaks.iter().filter(|(weak, _)| weak.upgrade().is_some()).count() as u8, + ]); drop(live); out.extend([left.get(), u8::from(registry.borrow().is_empty())]); } diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index 25d4e72c0..4a954d297 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -67,7 +67,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { "`Weak::as_ptr` of a `Weak<{ty}>`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns" ); let (span, msg) = self.sited(span, msg); - self.weak_pointers.push((self.tcx.erase_and_anonymize_regions(ty), span, msg)); + self.weak_pointers + .push((self.tcx.erase_and_anonymize_regions(ty), span, msg)); } /// Refuse each address read of a type an allocation holds beside another whose address @@ -122,7 +123,9 @@ fn shared<'tcx>( for &each in addressed { let one = FxHashSet::from_iter([each]); if Held::new(tcx, &one).count(ty) > 0 { - shared.entry(each).or_insert((ty, many || matches!(ty.kind(), ty::Array(..) | ty::Slice(_)))); + shared + .entry(each) + .or_insert((ty, many || matches!(ty.kind(), ty::Array(..) | ty::Slice(_)))); } } } @@ -213,10 +216,12 @@ impl<'s, 'tcx> Held<'s, 'tcx> { } }, ty::Adt(adt, args) => { - let mut counts = adt - .variants() - .iter() - .map(|v| v.fields.iter().map(|f| self.count(f.ty(tcx, args).skip_norm_wip())).collect::>()); + let mut counts = adt.variants().iter().map(|v| { + v.fields + .iter() + .map(|f| self.count(f.ty(tcx, args).skip_norm_wip())) + .collect::>() + }); match adt.is_union() { // Its fields overlap, as an enum's variants do. true => counts.next().into_iter().flatten().max().unwrap_or(0), @@ -224,7 +229,9 @@ impl<'s, 'tcx> Held<'s, 'tcx> { } } ty::Closure(_, args) => total(args.as_closure().upvar_tys().iter().map(|t| self.count(t))), - ty::CoroutineClosure(_, args) => total(args.as_coroutine_closure().upvar_tys().iter().map(|t| self.count(t))), + ty::CoroutineClosure(_, args) => { + total(args.as_coroutine_closure().upvar_tys().iter().map(|t| self.count(t))) + } // Its captures, and the locals it keeps across an `.await`. ty::Coroutine(def_id, args) => { let upvars = total(args.as_coroutine().upvar_tys().iter().map(|t| self.count(t))); diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 092db490a..a3d1737d5 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -485,7 +485,9 @@ impl<'tcx> Writes<'tcx> { _ => continue, }; for local in body.local_decls.iter() { - writes.locals.insert((def_id, tcx.erase_and_anonymize_regions(local.ty))); + writes + .locals + .insert((def_id, tcx.erase_and_anonymize_regions(local.ty))); } // Another crate lists none of its closures' bodies. for local in body.local_decls.iter() { @@ -608,7 +610,11 @@ pub(super) fn storage<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Ty<'tcx> { /// `ty` and each type it holds by value, at any depth: in a field, an element, or a /// payload. An array of no elements holds none, and a type parameter, or a type `env` /// can't normalize, stands for what it holds. -pub(super) fn by_value<'tcx>(tcx: TyCtxt<'tcx>, env: ty::TypingEnv<'tcx>, ty: Ty<'tcx>) -> impl Iterator> { +pub(super) fn by_value<'tcx>( + tcx: TyCtxt<'tcx>, + env: ty::TypingEnv<'tcx>, + ty: Ty<'tcx>, +) -> impl Iterator> { let mut held = FxIndexSet::default(); let mut stack = vec![ty]; while let Some(ty) = stack.pop() { diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index 05fffc874..068bba0d2 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -94,7 +94,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// no bytes is unspecified, but a pointer to `()` is an identity. Notes the comparison of /// those that do, for [`Self::taken`]. pub fn incomparable(&mut self, ty: Ty<'tcx>) -> Option { - self.unidentified(ty).map(|why| format!("comparing pointers to `{ty}`, {why}")) + self.unidentified(ty) + .map(|why| format!("comparing pointers to `{ty}`, {why}")) } /// Why pointers to `ty` that are not null aren't identities JS keeps, as @@ -677,7 +678,10 @@ pub fn call<'tcx>( } ("Weak", "as_ptr") => b.err( span, - format!("a raw pointer to the `{}` a `Weak` holds, which JS keeps no object of", pointee()), + format!( + "a raw pointer to the `{}` a `Weak` holds, which JS keeps no object of", + pointee() + ), ), ("Arc" | "Rc", "from_raw") => Expr::rs("arcFromRaw", vec![arg(), Expr::Str(owner.into())]), ("Arc" | "Rc", "increment_strong_count") => { From 951f2cc8706f4c56fb698a8baea87d3e77fb97cb Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:42:48 -0800 Subject: [PATCH 05/13] fix(ts): keep the address refusals' code words in the census Co-Authored-By: Claude Opus 5.5 --- ts/src/census.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ts/src/census.rs b/ts/src/census.rs index b71959f97..6497bf64a 100644 --- a/ts/src/census.rs +++ b/ts/src/census.rs @@ -142,6 +142,7 @@ const VERBATIM: &[&str] = &[ ".await", "?", "Arc", + "Arc::new_cyclic", "Binary", "Buf::copy_to_bytes", "BufMut::put", @@ -174,6 +175,8 @@ const VERBATIM: &[&str] = &[ "Vec::into_flattened", "Vec::spare_capacity_mut", "Vec::splice", + "Weak", + "Weak::as_ptr", "alloc", "async fn", "become", From 1679ec173512b169c6b59aefd73b8d324cbb9b32 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:49:17 -0800 Subject: [PATCH 06/13] test(ts): census a pointer made of an address over a type parameter Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/census/census.md | 30 ++++++++++++++++-------------- ts/fixtures/census/src/lib.rs | 8 ++++---- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ts/fixtures/census/census.md b/ts/fixtures/census/census.md index e7587fd63..2bdba0018 100644 --- a/ts/fixtures/census/census.md +++ b/ts/fixtures/census/census.md @@ -9,11 +9,12 @@ | 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 | | 1 | `Bytes::from_owner` of `_`, which would drop with the last clone of what it makes, which is not supported | fixtures/census/src/lib.rs:45:2 | | 1 | `Pointer` formatting, as in `{:p}`, is not supported | fixtures/census/src/lib.rs:24:2 | +| 1 | a pointer made of an address, which JS has no memory at | fixtures/census/src/lib.rs:14:2 | +| 1 | the address of a pointer to `_`, each a place JS makes of its own | fixtures/census/src/lib.rs:9:2 | | 1 | union `_`, whose fields overlap in `_` and `f64`, which JS keeps apart | fixtures/census/src/lib.rs:28:1 | ## By API @@ -22,12 +23,9 @@ The type or module each refused type or path belongs to. | Count | API | First | | ---: | --- | --- | -| 4 | `u32` | fixtures/census/src/lib.rs:9:2 | +| 5 | `u32` | fixtures/census/src/lib.rs:9:2 | | 2 | `u64` | fixtures/census/src/lib.rs:19:47 | | 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 | @@ -37,11 +35,10 @@ The type or module each refused type or path belongs to. | ---: | --- | --- | --- | | 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 | the address of a pointer to `_`, each a place JS makes of its own | `u32` | fixtures/census/src/lib.rs:9:2 | | 1 | union `_`, whose fields overlap in `_` and `f64`, which JS keeps apart | `Bits`, `u32` | fixtures/census/src/lib.rs:28:1 | ## Spans @@ -54,13 +51,6 @@ The type or module each refused type or path belongs to. -
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 -- fixtures/census/src/lib.rs:14:2: a cast from `*const T` to `usize`, between a pointer and an integer - -
-
a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another (2) - fixtures/census/src/lib.rs:19:12: a cast from a pointer to `u32` to one to `u8`, which reads the bytes of one type as another @@ -87,6 +77,18 @@ The type or module each refused type or path belongs to.
+
a pointer made of an address, which JS has no memory at (1) + +- fixtures/census/src/lib.rs:14:2: a pointer made of an address, which JS has no memory at + +
+ +
the address of a pointer to `_`, each a place JS makes of its own (1) + +- fixtures/census/src/lib.rs:9:2: the address of a pointer to `u32`, each a place JS makes of its own + +
+
union `_`, whose fields overlap in `_` and `f64`, which JS keeps apart (1) - fixtures/census/src/lib.rs:28:1: union `Bits`, whose fields overlap in `u32` and `f64`, which JS keeps apart diff --git a/ts/fixtures/census/src/lib.rs b/ts/fixtures/census/src/lib.rs index b4037ce73..29343aedd 100644 --- a/ts/fixtures/census/src/lib.rs +++ b/ts/fixtures/census/src/lib.rs @@ -4,14 +4,14 @@ #![allow(dead_code, reason = "each item is a census root, not called from Rust")] -/// A pointer's address, which JS has no counterpart of. +/// The address of a place, which JS makes anew each borrow. pub fn addressed(n: u32) -> usize { &n as *const u32 as usize } -/// A pointer's address in a body no instance reaches, lowered over `T`. -pub fn generic(t: T) -> usize { - &t as *const T as usize +/// A pointer made of an address in a body no instance reaches, lowered over `T`. +pub fn generic(at: usize) -> *const T { + std::ptr::without_provenance(at) } /// A pointer read as another type's bytes, at two types. From 5ee442b8bbfc030dbaaab65d6b25c5b5f5c4305a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:16:58 -0800 Subject: [PATCH 07/13] fix(ts): cast an address to its integer type, aligned as what holds it Co-Authored-By: Claude Opus 5.5 --- .../pointers/expected/pointers.Aligned.ts | 10 + .../lang/pointers/expected/pointers.Small.ts | 11 + .../lang/pointers/expected/pointers.ts | 127 ++++++----- ts/fixtures/lang/pointers/src/pointers.rs | 17 ++ ts/fixtures/reject/census.txt | 1 + ts/fixtures/reject/expected.txt | 1 + ts/fixtures/reject/src/addresses.rs | 12 + ts/fixtures/reject/ts.toml | 1 + ts/src/lower/address.rs | 215 +++++++++++++++--- ts/src/lower/mir/rvalue.rs | 7 +- ts/src/lower/mod.rs | 4 + 11 files changed, 322 insertions(+), 84 deletions(-) create mode 100644 ts/fixtures/lang/pointers/expected/pointers.Aligned.ts create mode 100644 ts/fixtures/lang/pointers/expected/pointers.Small.ts diff --git a/ts/fixtures/lang/pointers/expected/pointers.Aligned.ts b/ts/fixtures/lang/pointers/expected/pointers.Aligned.ts new file mode 100644 index 000000000..0c36ad184 --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.Aligned.ts @@ -0,0 +1,10 @@ +// Generated by rust2-ts. Do not edit. + +import type * as Small from "./pointers.Small.ts"; + +/** A struct more aligned than what it holds. */ +export interface Aligned { + _0: Small.Small; +} + +//# sourceMappingURL=pointers.Aligned.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.Small.ts b/ts/fixtures/lang/pointers/expected/pointers.Small.ts new file mode 100644 index 000000000..1bac6a900 --- /dev/null +++ b/ts/fixtures/lang/pointers/expected/pointers.Small.ts @@ -0,0 +1,11 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; + +/** A value of no alignment of its own, whose address an `Arc` or a struct aligns. */ +export interface Small { + _0: rs.u8; + _1: rs.u8; +} + +//# sourceMappingURL=pointers.Small.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index b21dfd5d9..1d1e1881e 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -5,6 +5,7 @@ import { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_ptr_ar import { then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$0fp5rqa, 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 type * as Aimed from "./pointers.Aimed.ts"; +import type * as Aligned from "./pointers.Aligned.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; import * as ByClosure from "./pointers.ByClosure.ts"; @@ -35,6 +36,7 @@ import type * as Refilled from "./pointers.Refilled.ts"; import type * as Run from "./pointers.Run.ts"; import * as Segment from "./pointers.Segment.ts"; import * as Shelf from "./pointers.Shelf.ts"; +import type * as Small from "./pointers.Small.ts"; import type * as Span from "./pointers.Span.ts"; import type * as Spot from "./pointers.Spot.ts"; import type * as Stable from "./pointers.Stable.ts"; @@ -2864,12 +2866,12 @@ export function rawPlaces(data: rs.ByteSlice, out: rs.ByteVec): void { * equality, counts, alignment, and the order's agreement, never an address itself. */ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { - let _52!: boolean; - let _85!: boolean; - let _124!: boolean; - let _165!: boolean; + let _59!: boolean; + let _92!: boolean; + let _131!: boolean; + let _172!: boolean; let agree!: boolean; - let _224!: rs.u8; + let _231!: rs.u8; const registry: rs.Arc>> = new rs.Arc(new rs.RefCell(rs.btreeNew(rs.cmpTuple<[rs.u8, rs.usize], rs.Ordering>([rs.cmp, rs.cmp])) as rs.BTreeMap<[rs.u8, rs.usize], rs.u8>)); const left: rs.Arc> = new rs.Arc(new rs.Cell(0)); const align: rs.usize = 4; @@ -2881,19 +2883,20 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const _16: rs.Weak = new rs.Weak(); const dangling: Keyed.Keyed = rs.weakAsPtr(_16); _16[Symbol.dispose](); - rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([rs.addr(dangling, 12, 4) === 4294967295, rs.addr(rs.NULL, 12, 4) === 0, rs.addr(rs.DANGLING, 12, 4) === align], (a0: boolean): rs.u8 => { + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.addr(dangling, 12, 4) & 0xff, ((rs.addr(dangling, 12, 4) << 16) >> 16) & 0xff)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([(rs.addr(dangling, 12, 4) | 0) === -1, rs.addr(dangling, 12, 4) === 4294967295, rs.addr(rs.NULL, 12, 4) === 0, rs.addr(rs.DANGLING, 12, 4) === align], (a0: boolean): rs.u8 => { return a0 ? 1 : 0; })), undefined, true); const iter: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); l1: while (true) { - const _33: [rs.usize, rs.u8] | rs.Done = iter.next(); - if (_33 === rs.DONE) { + const _40: [rs.usize, rs.u8] | rs.Done = iter.next(); + if (_40 === rs.DONE) { break; } - const i: rs.usize = _33[0]; - const byte: rs.u8 = _33[1]; - const _38: rs.u8 = rs.u8Rem(byte, 4); - if (_38 === 0) { + const i: rs.usize = _40[0]; + const byte: rs.u8 = _40[1]; + const _45: rs.u8 = rs.u8Rem(byte, 4); + if (_45 === 0) { const keyed: rs.Arc = new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))); const arc3 = keyed; const p: Keyed.Keyed = rs.arcRaw(arc3, arc3.value, "Arc"); @@ -2901,60 +2904,60 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { b5: { if (rs.usizeRem(address, align) === 0) { if (address !== 0) { - _52 = rs.addr(p, 12, 4) === address; + _59 = rs.addr(p, 12, 4) === address; break b5; } } - _52 = false; + _59 = false; } - rs.byteVecPush(out, _52 ? 1 : 0); + rs.byteVecPush(out, _59 ? 1 : 0); rs.byteVecPush(out, rs.hashInsert(byAddress, address, keyed.value.id) === undefined ? 1 : 0); - const _70: rs.RefCell> = rs.refCellBorrowMut(registry.value); - const _73: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; - rs.btreeInsert(_70.value, _73, keyed.value.id); - rs.refMutRelease(_70); + const _77: rs.RefCell> = rs.refCellBorrowMut(registry.value); + const _80: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; + rs.btreeInsert(_77.value, _80, keyed.value.id); + rs.refMutRelease(_77); if (!rs.btreeAdd(ordered, p)) { - _85 = false; + _92 = false; } else { - _85 = rs.hashAdd(hashed, p); + _92 = rs.hashAdd(hashed, p); } - rs.byteVecPush(out, _85 ? 1 : 0); - const _91: [rs.Weak, rs.usize] = [new rs.Weak(keyed), address]; - weaks.items.push(_91); + rs.byteVecPush(out, _92 ? 1 : 0); + const _98: [rs.Weak, rs.usize] = [new rs.Weak(keyed), address]; + weaks.items.push(_98); live.items.push(keyed); continue; } - if (_38 === 1) { + if (_45 === 1) { if (!(live.items.length === 0)) { const keyed2: rs.Arc = rs.vecRemove(live, rs.usizeRem(rs.u8Div(byte, 4) as rs.usize, live.items.length)); const arc2 = keyed2; const p2: Keyed.Keyed = rs.arcRaw(arc2, arc2.value, "Arc"); rs.byteVecPush(out, rs.hashRemove(byAddress, rs.addr(p2, 12, 4)) === keyed2.value.id ? 1 : 0); if (!rs.btreeDelete(ordered, p2)) { - _124 = false; + _131 = false; } else { - _124 = rs.hashDelete(hashed, p2); + _131 = rs.hashDelete(hashed, p2); } - rs.byteVecPush(out, _124 ? 1 : 0); + rs.byteVecPush(out, _131 ? 1 : 0); keyed2[Symbol.dispose](); continue; } - } else if (_38 === 2) { + } else if (_45 === 2) { const iterS: [rs.Weak, rs.usize][] = weaks.items; let iterI: number = 0; while (true) { - const _133: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; - if (_133 === rs.DONE) { + const _140: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; + if (_140 === rs.DONE) { continue l1; } - const at: rs.usize = rs.addr(rs.weakAsPtr(_133[0]), 12, 4); - const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_133[0]), (keyed3: rs.Arc): boolean => { + const at: rs.usize = rs.addr(rs.weakAsPtr(_140[0]), 12, 4); + const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_140[0]), (keyed3: rs.Arc): boolean => { const arc = keyed3; const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4) === at; keyed3[Symbol.dispose](); return _02; }); - rs.byteVecPush(out, rs.u8Add(at === _133[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); + rs.byteVecPush(out, rs.u8Add(at === _140[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); rs.byteVecPush(out, rs.hashContains(byAddress, at) === alive ? 1 : 0); } } @@ -2962,24 +2965,24 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { return rs.addr(_2, 12, 4); }), undefined, 4, true); if (!rs.sliceIsSorted(rs.sliceFrom(addresses.items), rs.le)) { - _165 = false; + _172 = false; } else { - _165 = addresses.items.length === live.items.length; + _172 = addresses.items.length === live.items.length; } - rs.byteVecPush(out, _165 ? 1 : 0); + rs.byteVecPush(out, _172 ? 1 : 0); const iter3: rs.Iter<[rs.Arc, rs.Arc]> = rs.iterZip(rs.iterSlice(live.items), rs.iterSkip(rs.iterSlice(live.items), 1), [false, false]); while (true) { - const _183: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); - if (_183 === rs.DONE) { + const _190: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); + if (_190 === rs.DONE) { break; } - const arc4 = _183[0]; - const _191: Keyed.Keyed = rs.arcRaw(arc4, arc4.value, "Arc"); - const arc5 = _183[1]; + const arc4 = _190[0]; + const _198: Keyed.Keyed = rs.arcRaw(arc4, arc4.value, "Arc"); + const arc5 = _190[1]; b11: { - const _190: [Keyed.Keyed, Keyed.Keyed] = [_191, rs.arcRaw(arc5, arc5.value, "Arc")]; - const p3: Keyed.Keyed = _191; - const q: Keyed.Keyed = _190[1]; + const _197: [Keyed.Keyed, Keyed.Keyed] = [_198, rs.arcRaw(arc5, arc5.value, "Arc")]; + const p3: Keyed.Keyed = _198; + const q: Keyed.Keyed = _197[1]; if (rs.addr(p3, 12, 4) < rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) < rs.addr(q, 12, 4)) { if (rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4)) { if (rs.cmpAddr(12, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4), rs.addr(q, 12, 4))) { @@ -2995,8 +2998,8 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const registry2: rs.RefCell> = rs.refCellBorrow(registry.value); let iterStart: rs.u8 = 0; const iterEnd: rs.u8 = 4; - while ((_224 = iterStart++) < iterEnd) { - const group: rs.u8 = _224; + while ((_231 = iterStart++) < iterEnd) { + const group: rs.u8 = _231; rs.byteVecPush(out, rs.iterCount(rs.btreeRange(registry2.value, new rs.Range([group, 0] as [rs.u8, rs.usize], [rs.u8Add(group, 1), 0] as [rs.u8, rs.usize]))) & 0xff); } rs.byteVecPush(out, rs.iterAll(rs.btreeValues(registry2.value), (id: rs.u8): boolean => { @@ -3006,17 +3009,31 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { }) ? 1 : 0); rs.refRelease(registry2); } - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_211: [rs.Weak, rs.usize]): boolean => { - const _510: rs.Arc | undefined = rs.weakUpgrade(_211[0]); - const _06: boolean = _510 !== undefined; - rs.drop(_510); + const small: rs.Arc[] = [new rs.Arc({ _0: 1, _1: 2 }), new rs.Arc({ _0: 3, _1: 4 })]; + const first: Aligned.Aligned = { _0: { _0: 5, _1: 6 } }; + const second: Aligned.Aligned = { _0: { _0: 7, _1: 8 } }; + const iter5: rs.Iter<[rs.Arc, Aligned.Aligned]> = rs.iterZip(rs.iterSlice(small), rs.iterSlice([first, second]), "index"); + while (true) { + const _273: [rs.Arc, Aligned.Aligned] | rs.Done = iter5.next(); + if (_273 === rs.DONE) { + break; + } + const arc6 = _273[0]; + const _280: [rs.usize, rs.usize] = [rs.addr(rs.arcRaw(arc6, arc6.value, "Arc"), 2, 64), rs.addr(_273[1]._0, 2, 64)]; + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(_280[0], 4) === 0 ? 1 : 0, rs.usizeRem(_280[1], 64) === 0 ? 1 : 0, _273[0].value._0 ^ _273[1]._0._1)), undefined, true); + } + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_22: [rs.Weak, rs.usize]): boolean => { + const _54: rs.Arc | undefined = rs.weakUpgrade(_22[0]); + const _06: boolean = _54 !== undefined; + rs.drop(_54); return _06; })) & 0xff)), undefined, true); rs.dropVec(live); - const _263: rs.u8 = left.value.v; - const _270: rs.RefCell> = rs.refCellBorrow(registry.value); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_263, rs.btreeLen(_270.value) === 0 ? 1 : 0)), undefined, true); - rs.refRelease(_270); + const _316: rs.u8 = left.value.v; + const _323: rs.RefCell> = rs.refCellBorrow(registry.value); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_316, rs.btreeLen(_323.value) === 0 ? 1 : 0)), undefined, true); + rs.refRelease(_323); + rs.dropAll(small); rs.dropVec(weaks, drop$tuple2_sync_Weak_pointers_Keyed_usize); left[Symbol.dispose](); registry[Symbol.dispose](); diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index c4b1809f6..d1e5648f2 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2310,8 +2310,10 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let mut ordered: BTreeSet<*const Keyed> = BTreeSet::new(); let mut hashed: HashSet<*const Keyed> = HashSet::new(); let dangling = Weak::::new().as_ptr(); + out.extend([dangling as u8, dangling as i16 as u8]); out.extend( [ + dangling as isize == -1, dangling as usize == usize::MAX, ptr::null::() as usize == 0, NonNull::::dangling().as_ptr().addr() == align, @@ -2373,6 +2375,14 @@ pub fn addresses(data: &[u8], out: &mut Vec) { } } } + // Aligned as what holds them: an `Arc`'s counts, and a struct more aligned than its parts. + let small = [Arc::new(Small(1, 2)), Arc::new(Small(3, 4))]; + let first = Aligned(Small(5, 6)); + let second = Aligned(Small(7, 8)); + for (arc, holder) in small.iter().zip([&first, &second]) { + let (shared, held) = (Arc::as_ptr(arc).addr(), ptr::from_ref(&holder.0).addr()); + out.extend([u8::from(shared % align_of::() == 0), u8::from(held % 64 == 0), arc.0 ^ holder.0.1]); + } out.extend([ left.get(), weaks.iter().filter(|(weak, _)| weak.upgrade().is_some()).count() as u8, @@ -2380,3 +2390,10 @@ pub fn addresses(data: &[u8], out: &mut Vec) { drop(live); out.extend([left.get(), u8::from(registry.borrow().is_empty())]); } + +/// A value of no alignment of its own, whose address an `Arc` or a struct aligns. +struct Small(u8, u8); + +/// A struct more aligned than what it holds. +#[repr(C, align(64))] +struct Aligned(Small); diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index b62e0c7af..f054c51d6 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -1,5 +1,6 @@ fixtures/reject/src/addresses.rs:cyclic+3:10: `Weak::as_ptr` of a `Weak`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds many of in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:late+1:2: the address of a pointer to `addresses::Late`, which `addresses::Early` holds at an offset its alignment fixes the address of, which JS can't show fixtures/reject/src/addresses.rs:made+1:5: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:20: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:54: a cast from `usize` to `*const addresses::Element`, between a pointer and an integer diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 439f97d1c..e4f1f6ef8 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -4,6 +4,7 @@ no item `lopsided` in the crate no item `missing` in the crate fixtures/reject/src/addresses.rs:cyclic+3:10: `Weak::as_ptr` of a `Weak`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds many of in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:late+1:2: the address of a pointer to `addresses::Late`, which `addresses::Early` holds at an offset its alignment fixes the address of, which JS can't show fixtures/reject/src/addresses.rs:made+1:5: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:20: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:54: a cast from `usize` to `*const addresses::Element`, between a pointer and an integer diff --git a/ts/fixtures/reject/src/addresses.rs b/ts/fixtures/reject/src/addresses.rs index 4664e6d4c..a0a2a2b58 100644 --- a/ts/fixtures/reject/src/addresses.rs +++ b/ts/fixtures/reject/src/addresses.rs @@ -56,3 +56,15 @@ pub fn cyclic() -> Arc { pub fn made(p: *const Element, at: usize) -> [*const Element; 3] { [p.with_addr(at), std::ptr::without_provenance(at), at as *const Element] } + +/// What a struct more aligned than it holds after another part. +pub struct Late(pub u8); + +/// A struct whose alignment fixes the address of its `Late` apart from its own. +#[repr(C, align(64))] +pub struct Early(pub u32, pub Late); + +/// The address of a part an alignment fixes. +pub fn late(early: &Early) -> usize { + std::ptr::from_ref(&early.1) as usize +} diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 63307af4e..b19ec75ee 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -2,6 +2,7 @@ package = "reject-fixture" roots = [ "addresses::cyclic", "addresses::elements", + "addresses::late", "addresses::made", "addresses::parts", "addresses::place_keys", diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index 4a954d297..c268425a1 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -11,6 +11,8 @@ //! of an address, which JS has no memory at. use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_abi::{FieldsShape, Variants}; +use rustc_middle::ty::layout::{LayoutCx, TyAndLayout}; use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; use rustc_span::Span; @@ -44,8 +46,19 @@ pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Resul if let Some(why) = why { return Err(b.err(span, format!("{what}, {why}"))); } - let read = b.t.sited(span, what); + let read = b.t.sited(span, what.clone()); b.t.addresses.push((ty, read.0, read.1)); + let layout = tcx.layout_of(env.as_query_input(ty)).ok(); + let size = layout.map_or(0, |l| l.size.bytes()); + return match b.t.alignment(ty) { + Ok(align) => Ok([Expr::lit(size.to_string()), Expr::lit(align.to_string())]), + Err(holder) => Err(b.err( + span, + format!( + "{what}, which `{holder}` holds at an offset its alignment fixes the address of, which JS can't show" + ), + )), + }; } let layout = tcx.layout_of(env.as_query_input(ty)).ok(); let size = layout.map_or(0, |l| l.size.bytes()); @@ -71,6 +84,32 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { .push((self.tcx.erase_and_anonymize_regions(ty), span, msg)); } + /// The alignment `rs.addr` numbers the addresses of a `ty` by: the largest of its own + /// and each allocation's that holds one at an offset that alignment keeps, as an + /// `Arc`'s counts before its value do, or else the type of an allocation that holds one + /// where its alignment fixes the address apart from it. + fn alignment(&mut self, ty: Ty<'tcx>) -> Result> { + let tcx = self.tcx; + let ty = super::copy::storage(tcx, ty); + if let Some(&found) = self.alignments.get(&ty) { + return found; + } + let env = ty::TypingEnv::fully_monomorphized(); + let own = tcx.layout_of(env.as_query_input(ty)).map_or(1, |l| l.align.abi.bytes()); + let mut placed = Placed { + tcx, + cx: LayoutCx::new(tcx, env), + target: ty, + own, + align: own, + roots: self.writes.locals(tcx).map(|t| (t, None)).collect(), + seen: FxHashSet::default(), + }; + let found = placed.run(); + self.alignments.insert(ty, found); + found + } + /// Refuse each address read of a type an allocation holds beside another whose address /// is read, or many of, and each `Weak::as_ptr` of a value `Arc::new_cyclic` makes. pub(super) fn refuse_addresses(&mut self) { @@ -181,33 +220,7 @@ impl<'s, 'tcx> Held<'s, 'tcx> { _ if adt.is_phantom_data() => 0, Some(Std::Range | Std::RangeInclusive) => 2 * self.count(args.type_at(0)).min(1), // Each of its type arguments, at most one of each, by value. - Some( - Std::Option - | Std::Result - | Std::Cow - | Std::Cell - | Std::RefCell - | Std::Mutex - | Std::OnceCell - | Std::OnceLock - | Std::LazyCell - | Std::LazyLock - | Std::Poll - | Std::Ready - | Std::Pin - | Std::ControlFlow - | Std::Bound - | Std::RangeTo - | Std::RangeFrom - | Std::RangeToInclusive - | Std::Reverse - | Std::Wrapping - | Std::Saturating - | Std::AssertUnwindSafe - | Std::MaybeUninit - | Std::ManuallyDrop, - ) => total(args.types().map(|t| self.count(t))), - _ if adt.is_unsafe_cell() => total(args.types().map(|t| self.count(t))), + _ if by_value(tcx, adt) => total(args.types().map(|t| self.count(t))), // A collection, or anything else of std's, may hold many of each in one // allocation of its own. _ => { @@ -259,6 +272,152 @@ impl<'s, 'tcx> Held<'s, 'tcx> { } } +/// Where the program's allocations hold values of the `target` type: at what offset +/// from where each starts, aligned as its type is, or as an `Arc`'s allocation is. +struct Placed<'tcx> { + tcx: TyCtxt<'tcx>, + cx: LayoutCx<'tcx>, + target: Ty<'tcx>, + /// The target's own alignment. + own: u64, + /// The largest alignment of an allocation holding one at an offset it keeps. + align: u64, + /// The allocations still to look in, each with the offset of its value and the + /// alignment of the allocation, if it holds more before it, as an `Arc`'s does. + roots: Vec<(Ty<'tcx>, Option<(u64, u64)>)>, + seen: FxHashSet<(Ty<'tcx>, Option<(u64, u64)>)>, +} + +impl<'tcx> Placed<'tcx> { + fn run(&mut self) -> Result> { + let env = ty::TypingEnv::fully_monomorphized(); + while let Some((ty, header)) = self.roots.pop() { + let ty = (self.tcx) + .try_normalize_erasing_regions(env, ty::Unnormalized::new_wip(ty)) + .unwrap_or(ty); + if !self.seen.insert((ty, header)) { + continue; + } + // A slice's elements are each at a multiple of their own alignment. + let ty = match ty.kind() { + ty::Slice(elem) => *elem, + ty::Str | ty::Dynamic(..) => continue, + _ => ty, + }; + let Ok(layout) = self.tcx.layout_of(env.as_query_input(ty)) else { + continue; + }; + let (offset, align) = header.unwrap_or((0, layout.align.abi.bytes())); + self.visit(layout, offset, align, ty)?; + } + Ok(self.align) + } + + /// Look for the target in `layout`, `offset` bytes into an allocation of `holder` + /// aligned to `align`. + fn visit(&mut self, layout: TyAndLayout<'tcx>, offset: u64, align: u64, holder: Ty<'tcx>) -> Result<(), Ty<'tcx>> { + let tcx = self.tcx; + if super::copy::storage(tcx, layout.ty) == self.target && align > self.own { + match offset % align { + 0 => self.align = self.align.max(align), + _ => return Err(holder), + } + } + match *layout.ty.kind() { + ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) => self.roots.push((pointee, None)), + ty::Adt(adt, args) if super::library::is_std(tcx, adt.did()) => match Std::of(tcx, adt.did()) { + Some(Std::Box | Std::NonNull) => self.roots.push((args.type_at(0), None)), + // The value after the counts, at an offset of its own alignment. + Some(Std::Arc | Std::Weak) => { + let value = args.type_at(0); + let env = ty::TypingEnv::fully_monomorphized(); + if let Ok(inner) = tcx.layout_of(env.as_query_input(value)) { + let pointer = tcx.data_layout.pointer_size().bytes(); + let align = inner.align.abi.bytes(); + let at = (2 * pointer).div_ceil(align) * align; + let whole = align.max(tcx.data_layout.pointer_align().abi.bytes()); + self.roots.push((value, Some((at, whole)))); + } + } + _ if adt.is_phantom_data() => {} + _ if by_value(tcx, adt) => self.fields(layout, offset, align, holder)?, + _ => self.roots.extend(args.types().map(|t| (t, None))), + }, + ty::Coroutine(..) => { + let held = super::copy::by_value(tcx, ty::TypingEnv::fully_monomorphized(), layout.ty) + .any(|part| super::copy::storage(tcx, part) == self.target); + if held && layout.align.abi.bytes() > self.own { + return Err(layout.ty); + } + self.fields(layout, offset, align, holder)?; + } + ty::Adt(..) | ty::Tuple(_) | ty::Array(..) | ty::Closure(..) | ty::CoroutineClosure(..) | ty::Pat(..) => { + self.fields(layout, offset, align, holder)? + } + _ => {} + } + Ok(()) + } + + /// Look in each field of each variant of `layout`, and in the first two elements of an + /// array, whose others are where those two say. + fn fields(&mut self, layout: TyAndLayout<'tcx>, offset: u64, align: u64, holder: Ty<'tcx>) -> Result<(), Ty<'tcx>> { + let variants = match &layout.variants { + Variants::Multiple { variants, .. } => variants.indices().map(|v| layout.for_variant(&self.cx, v)).collect(), + Variants::Single { .. } => vec![layout], + Variants::Empty => Vec::new(), + }; + for variant in variants { + let count = match variant.fields { + FieldsShape::Array { count, .. } => count.min(2) as usize, + _ => variant.fields.count(), + }; + for i in 0..count { + let field = variant.field(&self.cx, i); + self.visit(field, offset + variant.fields.offset(i).bytes(), align, holder)?; + } + } + Ok(()) + } +} + +/// Whether std's `adt` holds each of its type arguments by value, at most one of each, as +/// [`Held::count`] counts them. +fn by_value(tcx: TyCtxt<'_>, adt: ty::AdtDef<'_>) -> bool { + adt.is_unsafe_cell() + || matches!( + Std::of(tcx, adt.did()), + Some( + Std::Option + | Std::Result + | Std::Cow + | Std::Cell + | Std::RefCell + | Std::Mutex + | Std::OnceCell + | Std::OnceLock + | Std::LazyCell + | Std::LazyLock + | Std::Poll + | Std::Ready + | Std::Pin + | Std::ControlFlow + | Std::Bound + | Std::Range + | Std::RangeInclusive + | Std::RangeTo + | Std::RangeFrom + | Std::RangeToInclusive + | Std::Reverse + | Std::Wrapping + | Std::Saturating + | Std::AssertUnwindSafe + | Std::MaybeUninit + | Std::ManuallyDrop + ) + ) +} + /// The sum of `counts`, up to many. fn total(counts: impl Iterator) -> u8 { counts.fold(0, u8::saturating_add) diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index 05c0b42e7..74a1a4535 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -498,7 +498,12 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { ), CastKind::PointerExposeProvenance if let ty::RawPtr(pointee, _) = from.kind() => { let p = self.operand(op, span, out); - crate::lower::address::address(self.b, p, *pointee, span) + let address = crate::lower::address::address(self.b, p, *pointee, span); + let address = num::Operand { + expr: address, + value: None, + }; + num::cast(self.b, address, tcx.types.usize, to, span) } CastKind::PointerExposeProvenance | CastKind::PointerWithExposedProvenance => self.err( span, diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index c03563a44..2566d9770 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -220,6 +220,9 @@ pub struct Translator<'a, 'tcx> { /// Each read of an address, of a pointer to its type, refused once translation settles /// if an allocation holds that type beside another read, as [`address`] says. addresses: Vec<(Ty<'tcx>, Span, String)>, + /// The alignment `rs.addr` numbers each type's addresses by, as [`Self::alignment`] + /// found it. + alignments: rustc_data_structures::fx::FxHashMap, Result>>, /// The types `Arc::new_cyclic` makes. cyclic: rustc_data_structures::fx::FxHashSet>, /// Each `Weak::as_ptr`, by the type it points at, refused once translation settles if @@ -514,6 +517,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { capacity_reads: Vec::new(), string_bytes: false, addresses: Vec::new(), + alignments: Default::default(), cyclic: Default::default(), weak_pointers: Vec::new(), drops: Default::default(), From 2181649758f4749e4108233ef02ff0d79f1e40d5 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:18:07 -0800 Subject: [PATCH 08/13] style(ts): format Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/pointers/src/pointers.rs | 6 +++++- ts/src/lower/address.rs | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index d1e5648f2..08b98c155 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2381,7 +2381,11 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let second = Aligned(Small(7, 8)); for (arc, holder) in small.iter().zip([&first, &second]) { let (shared, held) = (Arc::as_ptr(arc).addr(), ptr::from_ref(&holder.0).addr()); - out.extend([u8::from(shared % align_of::() == 0), u8::from(held % 64 == 0), arc.0 ^ holder.0.1]); + out.extend([ + u8::from(shared % align_of::() == 0), + u8::from(held % 64 == 0), + arc.0 ^ holder.0.1, + ]); } out.extend([ left.get(), diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index c268425a1..2d3b56292 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -10,8 +10,8 @@ //! holds beside another whose address is read, nor holds many of. Nothing makes a pointer //! of an address, which JS has no memory at. -use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_abi::{FieldsShape, Variants}; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_middle::ty::layout::{LayoutCx, TyAndLayout}; use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; use rustc_span::Span; @@ -363,7 +363,9 @@ impl<'tcx> Placed<'tcx> { /// array, whose others are where those two say. fn fields(&mut self, layout: TyAndLayout<'tcx>, offset: u64, align: u64, holder: Ty<'tcx>) -> Result<(), Ty<'tcx>> { let variants = match &layout.variants { - Variants::Multiple { variants, .. } => variants.indices().map(|v| layout.for_variant(&self.cx, v)).collect(), + Variants::Multiple { variants, .. } => { + variants.indices().map(|v| layout.for_variant(&self.cx, v)).collect() + } Variants::Single { .. } => vec![layout], Variants::Empty => Vec::new(), }; From d3dff1cc51ab37bc7bc95656ab5a0eab1db159fc Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:37:58 -0800 Subject: [PATCH 09/13] fix(ts): keep a dangling pointer's address at its type's alignment Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/pointers.Keyed.ts | 4 +- .../lang/pointers/expected/pointers.ts | 43 ++++++++++--------- ts/fixtures/lang/pointers/src/pointers.rs | 3 ++ ts/runtime/rs.ts | 19 ++++---- ts/runtime/rs/collections.ts | 6 +-- ts/runtime/rs/hash.ts | 6 +-- ts/src/lower/address.rs | 12 +++--- 7 files changed, 51 insertions(+), 42 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts b/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts index 093768b7b..4e88a3326 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts @@ -26,12 +26,12 @@ export class Keyed { } export function drop$ops_Drop(self: Keyed): void { - const address: rs.usize = rs.addr(self, 12, 4); + const address: rs.usize = rs.addr(self, 12, 4, 4); const _8: rs.RefCell> = rs.refCellBorrowMut(self.registry.value); const _12: [rs.u8, rs.usize] = [rs.u8Rem(self.id, 4), address]; const removed: rs.u8 | undefined = rs.btreeRemove(_8.value, _12); rs.refMutRelease(_8); - const same: boolean = rs.addr(self, 12, 4) === address; + const same: boolean = rs.addr(self, 12, 4, 4) === address; if (removed === self.id) { if (same) { rs.cellSet(self.left.value, rs.u8WrappingAdd(self.left.value.v, 1)); diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 1d1e1881e..69f797f4a 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -2878,13 +2878,13 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const byAddress: rs.HashMap = rs.hashNew() as rs.HashMap; const live: rs.Vec> = new rs.Vec(); const weaks: rs.Vec<[rs.Weak, rs.usize]> = new rs.Vec(); - const ordered: rs.BTreeSet = rs.btreeNew(rs.cmpAddr(12, 4)) as rs.BTreeSet; - const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4), rs.same)) as rs.HashSet; + const ordered: rs.BTreeSet = rs.btreeNew(rs.cmpAddr(12, 4, 4)) as rs.BTreeSet; + const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4, 4), rs.same)) as rs.HashSet; const _16: rs.Weak = new rs.Weak(); const dangling: Keyed.Keyed = rs.weakAsPtr(_16); _16[Symbol.dispose](); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.addr(dangling, 12, 4) & 0xff, ((rs.addr(dangling, 12, 4) << 16) >> 16) & 0xff)), undefined, true); - rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([(rs.addr(dangling, 12, 4) | 0) === -1, rs.addr(dangling, 12, 4) === 4294967295, rs.addr(rs.NULL, 12, 4) === 0, rs.addr(rs.DANGLING, 12, 4) === align], (a0: boolean): rs.u8 => { + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.addr(dangling, 12, 4, 4) & 0xff, ((rs.addr(dangling, 12, 4, 4) << 16) >> 16) & 0xff)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([(rs.addr(dangling, 12, 4, 4) | 0) === -1, rs.addr(dangling, 12, 4, 4) === 4294967295, rs.addr(rs.NULL, 12, 4, 4) === 0, rs.addr(rs.DANGLING, 12, 4, 4) === align], (a0: boolean): rs.u8 => { return a0 ? 1 : 0; })), undefined, true); const iter: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); @@ -2900,11 +2900,11 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const keyed: rs.Arc = new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))); const arc3 = keyed; const p: Keyed.Keyed = rs.arcRaw(arc3, arc3.value, "Arc"); - const address: rs.usize = rs.addr(p, 12, 4); + const address: rs.usize = rs.addr(p, 12, 4, 4); b5: { if (rs.usizeRem(address, align) === 0) { if (address !== 0) { - _59 = rs.addr(p, 12, 4) === address; + _59 = rs.addr(p, 12, 4, 4) === address; break b5; } } @@ -2913,7 +2913,7 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { rs.byteVecPush(out, _59 ? 1 : 0); rs.byteVecPush(out, rs.hashInsert(byAddress, address, keyed.value.id) === undefined ? 1 : 0); const _77: rs.RefCell> = rs.refCellBorrowMut(registry.value); - const _80: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; + const _80: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4, 4)]; rs.btreeInsert(_77.value, _80, keyed.value.id); rs.refMutRelease(_77); if (!rs.btreeAdd(ordered, p)) { @@ -2932,7 +2932,7 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const keyed2: rs.Arc = rs.vecRemove(live, rs.usizeRem(rs.u8Div(byte, 4) as rs.usize, live.items.length)); const arc2 = keyed2; const p2: Keyed.Keyed = rs.arcRaw(arc2, arc2.value, "Arc"); - rs.byteVecPush(out, rs.hashRemove(byAddress, rs.addr(p2, 12, 4)) === keyed2.value.id ? 1 : 0); + rs.byteVecPush(out, rs.hashRemove(byAddress, rs.addr(p2, 12, 4, 4)) === keyed2.value.id ? 1 : 0); if (!rs.btreeDelete(ordered, p2)) { _131 = false; } else { @@ -2950,10 +2950,10 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { if (_140 === rs.DONE) { continue l1; } - const at: rs.usize = rs.addr(rs.weakAsPtr(_140[0]), 12, 4); + const at: rs.usize = rs.addr(rs.weakAsPtr(_140[0]), 12, 4, 4); const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_140[0]), (keyed3: rs.Arc): boolean => { const arc = keyed3; - const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4) === at; + const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4, 4) === at; keyed3[Symbol.dispose](); return _02; }); @@ -2962,7 +2962,7 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { } } const addresses: rs.Vec = rs.iterToVec(rs.iterMap(rs.btreeKeys(ordered), (_2: Keyed.Keyed): rs.usize => { - return rs.addr(_2, 12, 4); + return rs.addr(_2, 12, 4, 4); }), undefined, 4, true); if (!rs.sliceIsSorted(rs.sliceFrom(addresses.items), rs.le)) { _172 = false; @@ -2983,9 +2983,9 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const _197: [Keyed.Keyed, Keyed.Keyed] = [_198, rs.arcRaw(arc5, arc5.value, "Arc")]; const p3: Keyed.Keyed = _198; const q: Keyed.Keyed = _197[1]; - if (rs.addr(p3, 12, 4) < rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) < rs.addr(q, 12, 4)) { - if (rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4)) { - if (rs.cmpAddr(12, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4), rs.addr(q, 12, 4))) { + if (rs.addr(p3, 12, 4, 4) < rs.addr(q, 12, 4, 4) === rs.addr(p3, 12, 4, 4) < rs.addr(q, 12, 4, 4)) { + if (rs.addr(p3, 12, 4, 4) >= rs.addr(q, 12, 4, 4) === rs.addr(p3, 12, 4, 4) >= rs.addr(q, 12, 4, 4)) { + if (rs.cmpAddr(12, 4, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4, 4), rs.addr(q, 12, 4, 4))) { agree = p3 !== q; break b11; } @@ -3019,8 +3019,11 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { break; } const arc6 = _273[0]; - const _280: [rs.usize, rs.usize] = [rs.addr(rs.arcRaw(arc6, arc6.value, "Arc"), 2, 64), rs.addr(_273[1]._0, 2, 64)]; - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(_280[0], 4) === 0 ? 1 : 0, rs.usizeRem(_280[1], 64) === 0 ? 1 : 0, _273[0].value._0 ^ _273[1]._0._1)), undefined, true); + const _280: [rs.usize, rs.usize] = [rs.addr(rs.arcRaw(arc6, arc6.value, "Arc"), 2, 1, 64), rs.addr(_273[1]._0, 2, 1, 64)]; + const shared: rs.usize = _280[0]; + const held: rs.usize = _280[1]; + rs.byteVecPush(out, rs.addr(rs.DANGLING, 2, 1, 64) === 1 ? 1 : 0); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(shared, 4) === 0 ? 1 : 0, rs.usizeRem(held, 64) === 0 ? 1 : 0, _273[0].value._0 ^ _273[1]._0._1)), undefined, true); } rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_22: [rs.Weak, rs.usize]): boolean => { const _54: rs.Arc | undefined = rs.weakUpgrade(_22[0]); @@ -3029,10 +3032,10 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { return _06; })) & 0xff)), undefined, true); rs.dropVec(live); - const _316: rs.u8 = left.value.v; - const _323: rs.RefCell> = rs.refCellBorrow(registry.value); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_316, rs.btreeLen(_323.value) === 0 ? 1 : 0)), undefined, true); - rs.refRelease(_323); + const _323: rs.u8 = left.value.v; + const _330: rs.RefCell> = rs.refCellBorrow(registry.value); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_323, rs.btreeLen(_330.value) === 0 ? 1 : 0)), undefined, true); + rs.refRelease(_330); rs.dropAll(small); rs.dropVec(weaks, drop$tuple2_sync_Weak_pointers_Keyed_usize); left[Symbol.dispose](); diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 08b98c155..d6dd94e3f 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2381,6 +2381,9 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let second = Aligned(Small(7, 8)); for (arc, holder) in small.iter().zip([&first, &second]) { let (shared, held) = (Arc::as_ptr(arc).addr(), ptr::from_ref(&holder.0).addr()); + out.push(u8::from( + NonNull::::dangling().as_ptr().addr() == align_of::(), + )); out.extend([ u8::from(shared % align_of::() == 0), u8::from(held % 64 == 0), diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index e7a45fb99..2e6d92f71 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -329,22 +329,23 @@ const UNADDRESSED = /* @__PURE__ */ new FinalizationRegistry<[string, number]>(( let brk = 0x10000; /** - * `p as usize`, of a pointer to a value of `size` bytes aligned to `align`: null's 0, a - * dangling pointer's `align`, an address no memory is at, or else the object's own, - * as an allocator places each allocation: a multiple of `align`, in a block no other - * object JS keeps overlaps, with room before it for an `Arc`'s counts. Running out of - * wasm32's addresses aborts, as a failed allocation does. - */ -export function addr(p: object, size: number, align: number): number { + * `p as usize`, of a pointer to a value of `size` bytes aligned to `align`, which the + * program keeps where `kept` aligns it: null's 0, a dangling pointer's `align`, an + * address no memory is at, or else the object's own, as an allocator places each + * allocation: a multiple of `kept`, in a block no other object JS keeps overlaps, with + * room before it for an `Arc`'s counts. Running out of wasm32's addresses aborts, as a + * failed allocation does. + */ +export function addr(p: object, size: number, align: number, kept: number): number { if (p === NULL) return 0; if (p === DANGLING) return align; if (p instanceof Address) return p.addr; let at = ADDRESSED.get(p); if (at !== undefined) return at; - const block = `${size}:${align}`; + const block = `${size}:${kept}`; at = FREED.get(block)?.pop(); if (at === undefined) { - at = Math.ceil((brk + Math.max(16, align)) / align) * align; + at = Math.ceil((brk + Math.max(16, kept)) / kept) * kept; if (at + size > 2 ** 32) throw new Abort(`memory allocation of ${size} bytes failed`); brk = at + size; } diff --git a/ts/runtime/rs/collections.ts b/ts/runtime/rs/collections.ts index 2b524489f..1c21e4b3e 100644 --- a/ts/runtime/rs/collections.ts +++ b/ts/runtime/rs/collections.ts @@ -64,9 +64,9 @@ export function cmp(a: T, b: T): Ordering { return lt(x, y) ? -1 : lt(y, x) ? 1 : 0; } -/** `Ord::cmp` of raw pointers to values of `size` bytes aligned to `align`, which compares their addresses. */ -export function cmpAddr(size: number, align: number): (a: object, b: object) => Ordering { - return (a, b) => cmp(addr(a, size, align), addr(b, size, align)); +/** `Ord::cmp` of raw pointers to values of `size` bytes aligned to `align` and kept where `kept` aligns them, which compares their addresses. */ +export function cmpAddr(size: number, align: number, kept: number): (a: object, b: object) => Ordering { + return (a, b) => cmp(addr(a, size, align, kept), addr(b, size, align, kept)); } /** diff --git a/ts/runtime/rs/hash.ts b/ts/runtime/rs/hash.ts index b80ca8fc1..3605ccc88 100644 --- a/ts/runtime/rs/hash.ts +++ b/ts/runtime/rs/hash.ts @@ -639,9 +639,9 @@ export function hashUsize(value: number, s: Hasher): void { s.usize(value); } -/** `Hash::hash` of a raw pointer to a value of `size` bytes aligned to `align`, which hashes its address. */ -export function hashAddr(size: number, align: number): (p: object, s: Hasher) => void { - return (p, s) => s.usize(addr(p, size, align)); +/** `Hash::hash` of a raw pointer to a value of `size` bytes aligned to `align` and kept where `kept` aligns it, which hashes its address. */ +export function hashAddr(size: number, align: number, kept: number): (p: object, s: Hasher) => void { + return (p, s) => s.usize(addr(p, size, align, kept)); } /** `Hash::hash` of an `i8`, or of a discriminant of `Ordering`, whose `repr` it is. */ diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index 2d3b56292..ab4378341 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -25,14 +25,15 @@ use crate::ts::Expr; /// The address of `p`, a pointer to `ty`, as a `usize`. pub fn address<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Span) -> Expr { match read(b, ty, span) { - Ok([size, align]) => Expr::rs("addr", vec![p, size, align]), + Ok(layout) => Expr::rs("addr", [p].into_iter().chain(layout).collect()), Err(refused) => refused, } } -/// The size and alignment `rs.addr` reads the address of a pointer to `ty` by, or the +/// The size and alignment `rs.addr` reads the address of a pointer to `ty` by, with the +/// alignment of where the program keeps one, or the /// refusal of a type whose address JS has no counterpart of. -pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Result<[Expr; 2], Expr> { +pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Result<[Expr; 3], Expr> { let (tcx, env) = (b.t.tcx, b.t.typing_env_pub()); // Only an instance decides what a parameter is, and is refused on its own. if !(b.t.polymorphic() && ty.has_param()) { @@ -50,8 +51,9 @@ pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Resul b.t.addresses.push((ty, read.0, read.1)); let layout = tcx.layout_of(env.as_query_input(ty)).ok(); let size = layout.map_or(0, |l| l.size.bytes()); + let own = layout.map_or(1, |l| l.align.abi.bytes()); return match b.t.alignment(ty) { - Ok(align) => Ok([Expr::lit(size.to_string()), Expr::lit(align.to_string())]), + Ok(kept) => Ok([size, own, kept].map(|n| Expr::lit(n.to_string()))), Err(holder) => Err(b.err( span, format!( @@ -63,7 +65,7 @@ pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Resul let layout = tcx.layout_of(env.as_query_input(ty)).ok(); let size = layout.map_or(0, |l| l.size.bytes()); let align = layout.map_or(1, |l| l.align.abi.bytes()); - Ok([Expr::lit(size.to_string()), Expr::lit(align.to_string())]) + Ok([size, align, align].map(|n| Expr::lit(n.to_string()))) } impl<'a, 'tcx> Translator<'a, 'tcx> { From fe4b49dde633abd5a6ee527fb9442c8094b0ffbe Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:44:48 -0800 Subject: [PATCH 10/13] docs(ts): an address's alignment is where the program keeps it 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 7f41546b9..8ed9bb560 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 | `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.ByteSlice` of a `[u8; N]`'s bytes, which compares by the buffer and offset they are at, 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 an array of other values than bytes, which a slice lends as a copy 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 | -| a raw pointer's address: `p as usize`, `addr`, `expose_provenance`, and a raw pointer's `Hash` and `Ord` | `rs.addr`: null's 0, a dangling pointer's alignment, `Weak::new`'s `usize::MAX`, and otherwise a number the object keeps while JS keeps it, as an allocator places an allocation: a multiple of its type's alignment, in a block of its size no other kept object's overlaps, reused once JS collects the object, and an abort once wasm32's 4 GiB run out, as a failed allocation aborts. Refused, as JS can't show what Rust fixes: one to anything a pointer to doesn't compare as itself, to bytes, or to a type an allocation the program makes holds many of, as a `Vec` or an array does, or beside another whose address is read, as a struct's two fields are; `Weak::as_ptr` of a value `Arc::new_cyclic` makes; and a pointer made of an address | +| a raw pointer's address: `p as usize`, `addr`, `expose_provenance`, and a raw pointer's `Hash` and `Ord` | `rs.addr`: null's 0, a dangling pointer's alignment, `Weak::new`'s `usize::MAX`, and otherwise a number the object keeps while JS keeps it, as an allocator places an allocation: a multiple of the largest alignment of what the program keeps one in, in a block of its size no other kept object's overlaps, reused once JS collects the object, and an abort once wasm32's 4 GiB run out, as a failed allocation aborts. Refused, as JS can't show what Rust fixes: one to anything a pointer to doesn't compare as itself, to bytes, or to a type an allocation the program makes holds many of, as a `Vec` or an array does, or beside another whose address is read, as a struct's two fields are, or at an offset an alignment larger than its own fixes; `Weak::as_ptr` of a value `Arc::new_cyclic` makes; and a pointer made of an address | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined` | | `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 b2fdecd637afed2afee406438b0ea4c80da68202 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:38:36 -0800 Subject: [PATCH 11/13] feat(ts): give addresses only to an Arc's or Rc's value, freed with it An id is the address of the rs.Arc allocation whose value a pointer points at, freed for reuse when its strong and weak counts reach 0, as Rust frees it, so churning addressed allocations never exhausts wasm32's 4 GiB. Reading the address of a type the program keeps anywhere else (a local it borrows, drops, or gives std, a field, a Box, a collection, a static, or what JS is given whole), or writes whole in place, is refused, as is moving one out of its Arc by into_inner or make_mut. Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 16 +- quest/m3/ts-runtime-throws.md | 2 +- ts/README.md | 2 +- .../pointers/expected/pointers.Aligned.ts | 10 - .../lang/pointers/expected/pointers.Keyed.ts | 4 +- .../lang/pointers/expected/pointers.Small.ts | 2 +- .../lang/pointers/expected/pointers.ts | 137 ++--- ts/fixtures/lang/pointers/src/pointers.rs | 52 +- ts/fixtures/reject/census.txt | 17 +- ts/fixtures/reject/expected.txt | 18 +- ts/fixtures/reject/src/addresses.rs | 104 +++- ts/fixtures/reject/ts.toml | 11 +- ts/runtime/rs.ts | 76 ++- ts/runtime/rs/collections.ts | 6 +- ts/runtime/rs/hash.ts | 6 +- ts/runtime/rs/task.ts | 31 +- ts/src/census.rs | 5 + ts/src/lower/address.rs | 513 +++++++----------- ts/src/lower/copy.rs | 40 +- ts/src/lower/mod.rs | 10 +- ts/src/lower/task.rs | 2 + ts/tests/pointers.ts | 29 + 22 files changed, 600 insertions(+), 493 deletions(-) delete mode 100644 ts/fixtures/lang/pointers/expected/pointers.Aligned.ts diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index efc452d87..ed510ad2f 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -25,6 +25,18 @@ generated packages need it. integer is an id unique among live objects, a multiple of the type's alignment, and whatever has no exact answer, such as a pointer made back from an address, is refused at translation. +✅ Decided with the maintainer on 2026-11-06, refining that: only an `Arc`'s or `Rc`'s value +has an address. Its id belongs to the `rs.Arc` allocation and is freed for reuse when its +strong and weak counts reach 0, exactly as Rust frees it. Reading the address of a type the +program keeps anywhere else (a borrowed local, a field, a `Box`, a collection, a static), and +moving one out of its `Arc` (`into_inner`, `try_unwrap`, `unwrap_or_clone`, `make_mut`, a +whole write through `get_mut`), is refused with a span. Rejected: freeing an id when JS +collects its object, since GC never runs within one synchronous job, so churning +allocations could exhaust wasm32's 4 GiB where Rust reuses the freed memory; freeing at each +type's drop, which changes drop emission and still refuses values never dropped; and a +counter that never reuses, since `usize` is 32 bits on wasm32, so a long-running program +would exhaust the address space where Rust reuses freed blocks. + Decided with the maintainer on 2026-10-23, for the subset: `u128` stays, on the [wide-integer](/ts/README.md#design) representation. @@ -49,12 +61,12 @@ 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-04: 69 refusals. `Timestamp` alone refuses nothing, and +The census at the pin on 2026-11-06: 86 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 (29 refusals) translation +crossbeam-epoch, whose tagged pointers and inline closures (30 refusals) translation refuses for good, and smallvec's union over a type parameter and raw parts (10) wait on [unsafe](/quest/m3/ts-unsafe.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-runtime-throws.md b/quest/m3/ts-runtime-throws.md index 5991b9505..e65f32c6c 100644 --- a/quest/m3/ts-runtime-throws.md +++ b/quest/m3/ts-runtime-throws.md @@ -28,7 +28,7 @@ JS keeps in two arrays, `Bytes::slice_ref` of other storage, `alloc`'s bytes rea values, ranges stepped without their type, and fmt widths written as a placeholder. `git grep 'rust2-ts: ' ts/runtime` lists them; classify each before fixing. The pointer comparisons [#584](https://github.com/moq-dev/rust2/pull/584) refuses at translation are -in scope too, through [addresses](/quest/m2/ts-model/addresses.md)' identities. +in scope too. Public API: the translator's output, and which crates translate. diff --git a/ts/README.md b/ts/README.md index 1443c59f7..49f28e80d 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 | `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 | -| a raw pointer's address: `p as usize`, `addr`, `expose_provenance`, and a raw pointer's `Hash` and `Ord` | `rs.addr`: null's 0, a dangling pointer's alignment, `Weak::new`'s `usize::MAX`, and otherwise a number the object keeps while JS keeps it, as an allocator places an allocation: a multiple of the largest alignment of what the program keeps one in, in a block of its size no other kept object's overlaps, reused once JS collects the object, and an abort once wasm32's 4 GiB run out, as a failed allocation aborts. Refused, as JS can't show what Rust fixes: one to anything a pointer to doesn't compare as itself, to bytes, or to a type an allocation the program makes holds many of, as a `Vec` or an array does, or beside another whose address is read, as a struct's two fields are, or at an offset an alignment larger than its own fixes; `Weak::as_ptr` of a value `Arc::new_cyclic` makes; and a pointer made of an address | +| a raw pointer's address: `p as usize`, `addr`, `expose_provenance`, and a raw pointer's `Hash` and `Ord` | `rs.addr`: null's 0, a dangling pointer's alignment, `Weak::new`'s `usize::MAX`, and otherwise the address of the `Arc` or `Rc` value it points at, as an allocator places the allocation: after its counts, at a multiple of the value's alignment, in a block no other live allocation's overlaps, freed for reuse once its strong and weak counts reach 0, as Rust frees it. Only a value an `Arc` or `Rc` holds itself has one: an address of a type the program keeps anywhere else, such as in a local it borrows, drops, or gives std, a field, a `Box`, a collection, a static, or what JS is given or gives the crate whole, or writes whole in place, as through `get_mut`, is refused, as are moving one out of its `Arc` by `into_inner` or `make_mut` (`try_unwrap` and `unwrap_or_clone` translate for no type), `Weak::as_ptr` of one `Arc::new_cyclic` makes, and a pointer made of an address | | `MaybeUninit` | `T`: `undefined` until written, `0` for a byte, or an object whose parts kept as objects are there, holding `undefined` | | `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.Aligned.ts b/ts/fixtures/lang/pointers/expected/pointers.Aligned.ts deleted file mode 100644 index 0c36ad184..000000000 --- a/ts/fixtures/lang/pointers/expected/pointers.Aligned.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Generated by rust2-ts. Do not edit. - -import type * as Small from "./pointers.Small.ts"; - -/** A struct more aligned than what it holds. */ -export interface Aligned { - _0: Small.Small; -} - -//# sourceMappingURL=pointers.Aligned.ts.map diff --git a/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts b/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts index 4e88a3326..093768b7b 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Keyed.ts @@ -26,12 +26,12 @@ export class Keyed { } export function drop$ops_Drop(self: Keyed): void { - const address: rs.usize = rs.addr(self, 12, 4, 4); + const address: rs.usize = rs.addr(self, 12, 4); const _8: rs.RefCell> = rs.refCellBorrowMut(self.registry.value); const _12: [rs.u8, rs.usize] = [rs.u8Rem(self.id, 4), address]; const removed: rs.u8 | undefined = rs.btreeRemove(_8.value, _12); rs.refMutRelease(_8); - const same: boolean = rs.addr(self, 12, 4, 4) === address; + const same: boolean = rs.addr(self, 12, 4) === address; if (removed === self.id) { if (same) { rs.cellSet(self.left.value, rs.u8WrappingAdd(self.left.value.v, 1)); diff --git a/ts/fixtures/lang/pointers/expected/pointers.Small.ts b/ts/fixtures/lang/pointers/expected/pointers.Small.ts index 1bac6a900..b14056aa9 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.Small.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.Small.ts @@ -2,7 +2,7 @@ import * as rs from "./rs.ts"; -/** A value of no alignment of its own, whose address an `Arc` or a struct aligns. */ +/** A value of no alignment of its own, whose address its `Arc` aligns. */ export interface Small { _0: rs.u8; _1: rs.u8; diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index ade24f5de..482b0f63e 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -5,7 +5,6 @@ import { copy$Option_pointers_Pair, copy$Option_tuple2_u16_u8, copy$array_array2 import { then$0bgt67i, then$1a5xgoh, then$0jw3xip } from "./core/bool.ts"; import { isSomeAnd$1s2u5lz, isSomeAnd$0fp5rqa, 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 type * as Aimed from "./pointers.Aimed.ts"; -import type * as Aligned from "./pointers.Aligned.ts"; import * as Bin from "./pointers.Bin.ts"; import type * as ByCall from "./pointers.ByCall.ts"; import * as ByClosure from "./pointers.ByClosure.ts"; @@ -2857,31 +2856,32 @@ export function rawPlaces(data: rs.ByteSlice, out: rs.ByteVec): void { /** * Addresses read as integers, as `addr`, and as a raw pointer's `Hash` and `Ord` read - * them: made `Arc`s and dropped ones keyed by address in a B-tree, a hash map, and sets of - * pointers, which must stay unique and aligned among those alive, order pointers as their - * addresses, and stay a `Weak`'s once its value drops. Only what Rust fixes is written: - * equality, counts, alignment, and the order's agreement, never an address itself. + * them: made `Arc`s, dropped ones, and freed ones keyed by address in a B-tree, a hash + * map, and sets of pointers, which must stay unique and aligned among the allocations a + * strong `Arc` or a `Weak` holds, order pointers as their addresses, and stay a `Weak`'s + * once its value drops. Only what Rust fixes is written: equality, counts, alignment, and + * the order's agreement, never an address itself. */ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { let _59!: boolean; let _92!: boolean; let _131!: boolean; - let _172!: boolean; + let _181!: boolean; let agree!: boolean; - let _231!: rs.u8; + let _240!: rs.u8; const registry: rs.Arc>> = new rs.Arc(new rs.RefCell(rs.btreeNew(rs.cmpTuple<[rs.u8, rs.usize], rs.Ordering>([rs.cmp, rs.cmp])) as rs.BTreeMap<[rs.u8, rs.usize], rs.u8>)); const left: rs.Arc> = new rs.Arc(new rs.Cell(0)); const align: rs.usize = 4; - const byAddress: rs.HashMap = rs.hashNew() as rs.HashMap; + const held: rs.HashMap = rs.hashNew() as rs.HashMap; const live: rs.Vec> = new rs.Vec(); const weaks: rs.Vec<[rs.Weak, rs.usize]> = new rs.Vec(); - const ordered: rs.BTreeSet = rs.btreeNew(rs.cmpAddr(12, 4, 4)) as rs.BTreeSet; - const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4, 4), rs.same)) as rs.HashSet; + const ordered: rs.BTreeSet = rs.btreeNew(rs.cmpAddr(12, 4)) as rs.BTreeSet; + const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4), rs.same)) as rs.HashSet; const _16: rs.Weak = new rs.Weak(); const dangling: Keyed.Keyed = rs.weakAsPtr(_16); _16[Symbol.dispose](); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.addr(dangling, 12, 4, 4) & 0xff, ((rs.addr(dangling, 12, 4, 4) << 16) >> 16) & 0xff)), undefined, true); - rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([(rs.addr(dangling, 12, 4, 4) | 0) === -1, rs.addr(dangling, 12, 4, 4) === 4294967295, rs.addr(rs.NULL, 12, 4, 4) === 0, rs.addr(rs.DANGLING, 12, 4, 4) === align], (a0: boolean): rs.u8 => { + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.addr(dangling, 12, 4) & 0xff, ((rs.addr(dangling, 12, 4) << 16) >> 16) & 0xff)), undefined, true); + rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([(rs.addr(dangling, 12, 4) | 0) === -1, rs.addr(dangling, 12, 4) === 4294967295, rs.addr(rs.NULL, 12, 4) === 0, rs.addr(rs.DANGLING, 12, 4) === align], (a0: boolean): rs.u8 => { return a0 ? 1 : 0; })), undefined, true); const iter: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); @@ -2897,20 +2897,20 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const keyed: rs.Arc = new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))); const arc3 = keyed; const p: Keyed.Keyed = rs.arcRaw(arc3, arc3.value, "Arc"); - const address: rs.usize = rs.addr(p, 12, 4, 4); + const address: rs.usize = rs.addr(p, 12, 4); b5: { if (rs.usizeRem(address, align) === 0) { if (address !== 0) { - _59 = rs.addr(p, 12, 4, 4) === address; + _59 = rs.addr(p, 12, 4) === address; break b5; } } _59 = false; } rs.byteVecPush(out, _59 ? 1 : 0); - rs.byteVecPush(out, rs.hashInsert(byAddress, address, keyed.value.id) === undefined ? 1 : 0); + rs.byteVecPush(out, rs.hashInsert(held, address, keyed.value.id) === undefined ? 1 : 0); const _77: rs.RefCell> = rs.refCellBorrowMut(registry.value); - const _80: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4, 4)]; + const _80: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; rs.btreeInsert(_77.value, _80, keyed.value.id); rs.refMutRelease(_77); if (!rs.btreeAdd(ordered, p)) { @@ -2929,7 +2929,8 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const keyed2: rs.Arc = rs.vecRemove(live, rs.usizeRem(rs.u8Div(byte, 4) as rs.usize, live.items.length)); const arc2 = keyed2; const p2: Keyed.Keyed = rs.arcRaw(arc2, arc2.value, "Arc"); - rs.byteVecPush(out, rs.hashRemove(byAddress, rs.addr(p2, 12, 4, 4)) === keyed2.value.id ? 1 : 0); + const address2: rs.usize = rs.addr(p2, 12, 4); + rs.byteVecPush(out, rs.hashGet(held, address2) === keyed2.value.id ? 1 : 0); if (!rs.btreeDelete(ordered, p2)) { _131 = false; } else { @@ -2937,52 +2938,59 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { } rs.byteVecPush(out, _131 ? 1 : 0); keyed2[Symbol.dispose](); + if (!((byte & 4) !== 0)) { + continue; + } + rs.vecRetain(weaks, (_2: [rs.Weak, rs.usize]): boolean => { + return _2[1] !== address2; + }, drop$tuple2_sync_Weak_pointers_Keyed_usize); + rs.hashRemove(held, address2); continue; } } else if (_45 === 2) { const iterS: [rs.Weak, rs.usize][] = weaks.items; let iterI: number = 0; while (true) { - const _140: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; - if (_140 === rs.DONE) { + const _150: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; + if (_150 === rs.DONE) { continue l1; } - const at: rs.usize = rs.addr(rs.weakAsPtr(_140[0]), 12, 4, 4); - const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_140[0]), (keyed3: rs.Arc): boolean => { + const at: rs.usize = rs.addr(rs.weakAsPtr(_150[0]), 12, 4); + const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_150[0]), (keyed3: rs.Arc): boolean => { const arc = keyed3; - const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4, 4) === at; + const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4) === at; keyed3[Symbol.dispose](); return _02; }); - rs.byteVecPush(out, rs.u8Add(at === _140[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); - rs.byteVecPush(out, rs.hashContains(byAddress, at) === alive ? 1 : 0); + rs.byteVecPush(out, rs.u8Add(at === _150[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); + rs.byteVecPush(out, rs.hashContains(held, at) ? 1 : 0); } } - const addresses: rs.Vec = rs.iterToVec(rs.iterMap(rs.btreeKeys(ordered), (_2: Keyed.Keyed): rs.usize => { - return rs.addr(_2, 12, 4, 4); + const addresses: rs.Vec = rs.iterToVec(rs.iterMap(rs.btreeKeys(ordered), (_22: Keyed.Keyed): rs.usize => { + return rs.addr(_22, 12, 4); }), undefined, 4, true); if (!rs.sliceIsSorted(rs.sliceFrom(addresses.items), rs.le)) { - _172 = false; + _181 = false; } else { - _172 = addresses.items.length === live.items.length; + _181 = addresses.items.length === live.items.length; } - rs.byteVecPush(out, _172 ? 1 : 0); + rs.byteVecPush(out, _181 ? 1 : 0); const iter3: rs.Iter<[rs.Arc, rs.Arc]> = rs.iterZip(rs.iterSlice(live.items), rs.iterSkip(rs.iterSlice(live.items), 1), [false, false]); while (true) { - const _190: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); - if (_190 === rs.DONE) { + const _199: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); + if (_199 === rs.DONE) { break; } - const arc4 = _190[0]; - const _198: Keyed.Keyed = rs.arcRaw(arc4, arc4.value, "Arc"); - const arc5 = _190[1]; + const arc4 = _199[0]; + const _207: Keyed.Keyed = rs.arcRaw(arc4, arc4.value, "Arc"); + const arc5 = _199[1]; b11: { - const _197: [Keyed.Keyed, Keyed.Keyed] = [_198, rs.arcRaw(arc5, arc5.value, "Arc")]; - const p3: Keyed.Keyed = _198; - const q: Keyed.Keyed = _197[1]; - if (rs.addr(p3, 12, 4, 4) < rs.addr(q, 12, 4, 4) === rs.addr(p3, 12, 4, 4) < rs.addr(q, 12, 4, 4)) { - if (rs.addr(p3, 12, 4, 4) >= rs.addr(q, 12, 4, 4) === rs.addr(p3, 12, 4, 4) >= rs.addr(q, 12, 4, 4)) { - if (rs.cmpAddr(12, 4, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4, 4), rs.addr(q, 12, 4, 4))) { + const _206: [Keyed.Keyed, Keyed.Keyed] = [_207, rs.arcRaw(arc5, arc5.value, "Arc")]; + const p3: Keyed.Keyed = _207; + const q: Keyed.Keyed = _206[1]; + if (rs.addr(p3, 12, 4) < rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) < rs.addr(q, 12, 4)) { + if (rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4)) { + if (rs.cmpAddr(12, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4), rs.addr(q, 12, 4))) { agree = p3 !== q; break b11; } @@ -2995,44 +3003,37 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const registry2: rs.RefCell> = rs.refCellBorrow(registry.value); let iterStart: rs.u8 = 0; const iterEnd: rs.u8 = 4; - while ((_231 = iterStart++) < iterEnd) { - const group: rs.u8 = _231; + while ((_240 = iterStart++) < iterEnd) { + const group: rs.u8 = _240; rs.byteVecPush(out, rs.iterCount(rs.btreeRange(registry2.value, new rs.Range([group, 0] as [rs.u8, rs.usize], [rs.u8Add(group, 1), 0] as [rs.u8, rs.usize]))) & 0xff); } rs.byteVecPush(out, rs.iterAll(rs.btreeValues(registry2.value), (id: rs.u8): boolean => { - return rs.iterAny(rs.hashValues(byAddress), (live2: rs.u8): boolean => { - return live2 === id; + return rs.iterAny(rs.hashValues(held), (h: rs.u8): boolean => { + return h === id; }); }) ? 1 : 0); rs.refRelease(registry2); } const small: rs.Arc[] = [new rs.Arc({ _0: 1, _1: 2 }), new rs.Arc({ _0: 3, _1: 4 })]; - const first: Aligned.Aligned = { _0: { _0: 5, _1: 6 } }; - const second: Aligned.Aligned = { _0: { _0: 7, _1: 8 } }; - const iter5: rs.Iter<[rs.Arc, Aligned.Aligned]> = rs.iterZip(rs.iterSlice(small), rs.iterSlice([first, second]), "index"); - while (true) { - const _273: [rs.Arc, Aligned.Aligned] | rs.Done = iter5.next(); - if (_273 === rs.DONE) { - break; - } - const arc6 = _273[0]; - const _280: [rs.usize, rs.usize] = [rs.addr(rs.arcRaw(arc6, arc6.value, "Arc"), 2, 1, 64), rs.addr(_273[1]._0, 2, 1, 64)]; - const shared: rs.usize = _280[0]; - const held: rs.usize = _280[1]; - rs.byteVecPush(out, rs.addr(rs.DANGLING, 2, 1, 64) === 1 ? 1 : 0); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(shared, 4) === 0 ? 1 : 0, rs.usizeRem(held, 64) === 0 ? 1 : 0, _273[0].value._0 ^ _273[1]._0._1)), undefined, true); - } - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_22: [rs.Weak, rs.usize]): boolean => { - const _54: rs.Arc | undefined = rs.weakUpgrade(_22[0]); - const _06: boolean = _54 !== undefined; - rs.drop(_54); - return _06; + for (const _272 of small) { + const arc6 = _272; + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(rs.addr(rs.arcRaw(arc6, arc6.value, "Arc"), 2, 1), 4) === 0 ? 1 : 0, rs.addr(rs.DANGLING, 2, 1) === 1 ? 1 : 0, _272.value._0 ^ _272.value._1)), undefined, true); + } + const arc7 = small[0]; + const _299: Small.Small = rs.arcRaw(arc7, arc7.value, "Arc"); + const arc8 = small[1]; + rs.byteVecPush(out, _299 !== rs.arcRaw(arc8, arc8.value, "Arc") ? 1 : 0); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_221: [rs.Weak, rs.usize]): boolean => { + const _55: rs.Arc | undefined = rs.weakUpgrade(_221[0]); + const _07: boolean = _55 !== undefined; + rs.drop(_55); + return _07; })) & 0xff)), undefined, true); rs.dropVec(live); - const _323: rs.u8 = left.value.v; - const _330: rs.RefCell> = rs.refCellBorrow(registry.value); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_323, rs.btreeLen(_330.value) === 0 ? 1 : 0)), undefined, true); - rs.refRelease(_330); + const _322: rs.u8 = left.value.v; + const _329: rs.RefCell> = rs.refCellBorrow(registry.value); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_322, rs.btreeLen(_329.value) === 0 ? 1 : 0)), undefined, true); + rs.refRelease(_329); rs.dropAll(small); rs.dropVec(weaks, drop$tuple2_sync_Weak_pointers_Keyed_usize); left[Symbol.dispose](); diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index cbcee5787..e2cdac5ab 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2295,17 +2295,18 @@ impl Drop for Keyed { } /// Addresses read as integers, as `addr`, and as a raw pointer's `Hash` and `Ord` read -/// them: made `Arc`s and dropped ones keyed by address in a B-tree, a hash map, and sets of -/// pointers, which must stay unique and aligned among those alive, order pointers as their -/// addresses, and stay a `Weak`'s once its value drops. Only what Rust fixes is written: -/// equality, counts, alignment, and the order's agreement, never an address itself. +/// them: made `Arc`s, dropped ones, and freed ones keyed by address in a B-tree, a hash +/// map, and sets of pointers, which must stay unique and aligned among the allocations a +/// strong `Arc` or a `Weak` holds, order pointers as their addresses, and stay a `Weak`'s +/// once its value drops. Only what Rust fixes is written: equality, counts, alignment, and +/// the order's agreement, never an address itself. pub fn addresses(data: &[u8], out: &mut Vec) { use std::collections::{BTreeSet, HashMap, HashSet}; use std::sync::Weak; let registry = Rc::new(std::cell::RefCell::new(BTreeMap::new())); let left = Rc::new(Cell::new(0u8)); let align = align_of::(); - let mut by_address: HashMap = HashMap::new(); + let mut held: HashMap = HashMap::new(); let mut live: Vec> = Vec::new(); let mut weaks: Vec<(Weak, usize)> = Vec::new(); let mut ordered: BTreeSet<*const Keyed> = BTreeSet::new(); @@ -2332,7 +2333,8 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let p = Arc::as_ptr(&keyed); let address = p as usize; out.push(u8::from(address % align == 0 && address != 0 && p.addr() == address)); - out.push(u8::from(by_address.insert(address, keyed.id).is_none())); + // Apart from every allocation still held, a freed one's included. + out.push(u8::from(held.insert(address, keyed.id).is_none())); registry .borrow_mut() .insert((keyed.id % 4, p.expose_provenance()), keyed.id); @@ -2343,8 +2345,15 @@ pub fn addresses(data: &[u8], out: &mut Vec) { 1 if !live.is_empty() => { let keyed = live.remove(usize::from(byte / 4) % live.len()); let p = Arc::as_ptr(&keyed); - out.push(u8::from(by_address.remove(&(p as usize)) == Some(keyed.id))); + let address = p as usize; + out.push(u8::from(held.get(&address) == Some(&keyed.id))); out.push(u8::from(ordered.remove(&p) && hashed.remove(&p))); + drop(keyed); + // Its `Weak` too, which frees the allocation for another to reuse. + if byte & 4 != 0 { + weaks.retain(|(_, at)| *at != address); + held.remove(&address); + } } // A `Weak` keeps its address once its value drops, as it keeps the allocation. 2 => { @@ -2352,7 +2361,7 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let at = weak.as_ptr() as usize; let alive = weak.upgrade().is_some_and(|keyed| Arc::as_ptr(&keyed) as usize == at); out.push(u8::from(at == *address) + 2 * u8::from(alive)); - out.push(u8::from(by_address.contains_key(&at) == alive)); + out.push(u8::from(held.contains_key(&at))); } } _ => { @@ -2370,27 +2379,20 @@ pub fn addresses(data: &[u8], out: &mut Vec) { for group in 0..4u8 { out.push(registry.range((group, 0)..(group + 1, 0)).count() as u8); } - out.push(u8::from( - registry.values().all(|id| by_address.values().any(|live| live == id)), - )); + out.push(u8::from(registry.values().all(|id| held.values().any(|h| h == id)))); } } } - // Aligned as what holds them: an `Arc`'s counts, and a struct more aligned than its parts. + // A value of no alignment of its own, aligned as the counts before it in its `Arc`. let small = [Arc::new(Small(1, 2)), Arc::new(Small(3, 4))]; - let first = Aligned(Small(5, 6)); - let second = Aligned(Small(7, 8)); - for (arc, holder) in small.iter().zip([&first, &second]) { - let (shared, held) = (Arc::as_ptr(arc).addr(), ptr::from_ref(&holder.0).addr()); - out.push(u8::from( - NonNull::::dangling().as_ptr().addr() == align_of::(), - )); + for arc in &small { out.extend([ - u8::from(shared % align_of::() == 0), - u8::from(held % 64 == 0), - arc.0 ^ holder.0.1, + u8::from(Arc::as_ptr(arc).addr() % align_of::() == 0), + u8::from(NonNull::::dangling().as_ptr().addr() == align_of::()), + arc.0 ^ arc.1, ]); } + out.push(u8::from(Arc::as_ptr(&small[0]) != Arc::as_ptr(&small[1]))); out.extend([ left.get(), weaks.iter().filter(|(weak, _)| weak.upgrade().is_some()).count() as u8, @@ -2399,13 +2401,9 @@ pub fn addresses(data: &[u8], out: &mut Vec) { out.extend([left.get(), u8::from(registry.borrow().is_empty())]); } -/// A value of no alignment of its own, whose address an `Arc` or a struct aligns. +/// A value of no alignment of its own, whose address its `Arc` aligns. struct Small(u8, u8); -/// A struct more aligned than what it holds. -#[repr(C, align(64))] -struct Aligned(Small); - /// Pointers to arrays of other values than bytes, made of the array, of references to it /// and its parts, by `first_chunk`, `split_first_chunk`, a subslice pattern, and /// `array::from_mut`: compared where they point, and written through. diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 8a5422c07..5a2e4eba6 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -1,11 +1,16 @@ +fixtures/reject/src/addresses.rs:borrowed+2:2: the address of a pointer to `addresses::Borrowed`, which a local keeps that the program borrows, drops, or gives std, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:boxed+2:2: the address of a pointer to `addresses::Boxed`, which `std::boxed::Box` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does fixtures/reject/src/addresses.rs:cyclic+3:10: `Weak::as_ptr` of a `Weak`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns -fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds many of in one allocation, as far apart as Rust fixes and JS can't show -fixtures/reject/src/addresses.rs:late+1:2: the address of a pointer to `addresses::Late`, which `addresses::Early` holds at an offset its alignment fixes the address of, which JS can't show +fixtures/reject/src/addresses.rs:dropped+3:2: the address of a pointer to `addresses::Dropped`, which a local keeps that the program borrows, drops, or gives std, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:held+1:2: the address of a pointer to `addresses::Held`, which a static or constant holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:inner+1:11: the address of a pointer to `addresses::Inner`, which `std::option::Option` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:inner+2:7: `Arc::into_inner` of an `Arc` or `Rc` of `addresses::Inner`, which moves it out of the allocation whose address the program reads fixtures/reject/src/addresses.rs:made+1:5: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:20: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:54: a cast from `usize` to `*const addresses::Element`, between a pointer and an integer -fixtures/reject/src/addresses.rs:parts+1:34: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show -fixtures/reject/src/addresses.rs:parts+1:76: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:parts+1:34: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:parts+1:76: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does fixtures/reject/src/addresses.rs:place_keys+1:13: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/addresses.rs:place_keys+1:47: a raw pointer to `u32` behind a reference, which is the object and not where it is kept fixtures/reject/src/addresses.rs:place_keys+2:15: comparing pointers to `u32`, each a place JS makes of its own @@ -17,6 +22,10 @@ fixtures/reject/src/addresses.rs:place_keys+3:50: a raw pointer to `u32` behind fixtures/reject/src/addresses.rs:places+1:2: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/addresses.rs:places+1:35: a raw pointer to a place of `()`, which has no address of its own fixtures/reject/src/addresses.rs:places+1:35: the address of a pointer to `()`, which has no bytes and so no address of its own +fixtures/reject/src/addresses.rs:rewritten+4:2: the address of a pointer to `addresses::Rewritten`, which the crate writes whole in place, moving another into its `Arc`, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:unshared+1:2: `Arc::make_mut` of an `Arc` or `Rc` of `addresses::Unshared`, which moves it out of the allocation whose address the program reads +fixtures/reject/src/addresses.rs:unwrapped+1:2: unsupported call to `std::sync::Arc::::try_unwrap` +fixtures/reject/src/addresses.rs:unwrapped+1:55: unsupported call to `std::sync::Arc::::unwrap_or_clone` fixtures/reject/src/alloc.rs:addressed+1:17: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/alloc.rs:alloc_retyped+4:7: memory from `alloc` cast to pointers to `u32` and to `i32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:alloc_retyped+5:11: memory from `alloc` cast to pointers to `i32` and 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 4b0dc69ec..f2bfec8bc 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -2,14 +2,20 @@ no item `bound_ranges` in the crate no item `clones::slice_of_object` in the crate no item `lopsided` in the crate no item `missing` in the crate +fixtures/reject/src/addresses.rs:borrowed+2:2: the address of a pointer to `addresses::Borrowed`, which a local keeps that the program borrows, drops, or gives std, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:boxed+2:2: the address of a pointer to `addresses::Boxed`, which `std::boxed::Box` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does fixtures/reject/src/addresses.rs:cyclic+3:10: `Weak::as_ptr` of a `Weak`, which `Arc::new_cyclic` makes and JS has no value of until its closure returns -fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds many of in one allocation, as far apart as Rust fixes and JS can't show -fixtures/reject/src/addresses.rs:late+1:2: the address of a pointer to `addresses::Late`, which `addresses::Early` holds at an offset its alignment fixes the address of, which JS can't show +fixtures/reject/src/addresses.rs:dropped+3:2: the address of a pointer to `addresses::Dropped`, which a local keeps that the program borrows, drops, or gives std, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:elements+1:19: the address of a pointer to `addresses::Element`, which `[addresses::Element]` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:given_at+1:2: the address of a pointer to `addresses::Given`, which JS keeps, as `addresses::given` takes or returns one, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:held+1:2: the address of a pointer to `addresses::Held`, which a static or constant holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:inner+1:11: the address of a pointer to `addresses::Inner`, which `std::option::Option` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:inner+2:7: `Arc::into_inner` of an `Arc` or `Rc` of `addresses::Inner`, which moves it out of the allocation whose address the program reads fixtures/reject/src/addresses.rs:made+1:5: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:20: a pointer made of an address, which JS has no memory at fixtures/reject/src/addresses.rs:made+1:54: a cast from `usize` to `*const addresses::Element`, between a pointer and an integer -fixtures/reject/src/addresses.rs:parts+1:34: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show -fixtures/reject/src/addresses.rs:parts+1:76: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds beside another value whose address is read, in one allocation, as far apart as Rust fixes and JS can't show +fixtures/reject/src/addresses.rs:parts+1:34: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:parts+1:76: the address of a pointer to `addresses::Part`, which `addresses::Pair` holds, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does fixtures/reject/src/addresses.rs:place_keys+1:13: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/addresses.rs:place_keys+1:47: a raw pointer to `u32` behind a reference, which is the object and not where it is kept fixtures/reject/src/addresses.rs:place_keys+2:15: comparing pointers to `u32`, each a place JS makes of its own @@ -21,6 +27,10 @@ fixtures/reject/src/addresses.rs:place_keys+3:50: a raw pointer to `u32` behind fixtures/reject/src/addresses.rs:places+1:2: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/addresses.rs:places+1:35: a raw pointer to a place of `()`, which has no address of its own fixtures/reject/src/addresses.rs:places+1:35: the address of a pointer to `()`, which has no bytes and so no address of its own +fixtures/reject/src/addresses.rs:rewritten+4:2: the address of a pointer to `addresses::Rewritten`, which the crate writes whole in place, moving another into its `Arc`, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does +fixtures/reject/src/addresses.rs:unshared+1:2: `Arc::make_mut` of an `Arc` or `Rc` of `addresses::Unshared`, which moves it out of the allocation whose address the program reads +fixtures/reject/src/addresses.rs:unwrapped+1:2: unsupported call to `std::sync::Arc::::try_unwrap` +fixtures/reject/src/addresses.rs:unwrapped+1:55: unsupported call to `std::sync::Arc::::unwrap_or_clone` fixtures/reject/src/alloc.rs:addressed+1:17: the address of a pointer to `u32`, each a place JS makes of its own fixtures/reject/src/alloc.rs:alloc_retyped+4:7: memory from `alloc` cast to pointers to `u32` and to `i32`, which reads the bytes of one type as another fixtures/reject/src/alloc.rs:alloc_retyped+5:11: memory from `alloc` cast to pointers to `i32` and to `u32`, which reads the bytes of one type as another diff --git a/ts/fixtures/reject/src/addresses.rs b/ts/fixtures/reject/src/addresses.rs index a0a2a2b58..59580b324 100644 --- a/ts/fixtures/reject/src/addresses.rs +++ b/ts/fixtures/reject/src/addresses.rs @@ -1,6 +1,8 @@ -//! Addresses read as integers where JS has no exact counterpart: of values one allocation -//! holds many of or beside another whose address is read, of places, of values of no -//! bytes, of a `Weak` whose value `Arc::new_cyclic` makes, and pointers made of addresses. +//! Addresses read as integers where JS has no exact counterpart: of values the program +//! keeps anywhere but as an `Arc`'s value (a local it borrows or drops, a field, a `Box`, +//! a collection, a static, or where JS keeps it) or moves out of its `Arc`, of places, of +//! values of no bytes, of a `Weak` whose value `Arc::new_cyclic` makes, and pointers made +//! of addresses. use std::sync::{Arc, Weak}; @@ -57,14 +59,94 @@ pub fn made(p: *const Element, at: usize) -> [*const Element; 3] { [p.with_addr(at), std::ptr::without_provenance(at), at as *const Element] } -/// What a struct more aligned than it holds after another part. -pub struct Late(pub u8); +/// What a local holds, borrowed. +pub struct Borrowed(pub u8); -/// A struct whose alignment fixes the address of its `Late` apart from its own. -#[repr(C, align(64))] -pub struct Early(pub u32, pub Late); +/// The address of a local, which JS has no allocation of to free. +pub fn borrowed() -> usize { + let local = Borrowed(1); + std::ptr::from_ref(&local) as usize +} + +/// What a local holds until it drops, and an `Arc` holds elsewhere. +pub struct Dropped(pub u8); + +impl Drop for Dropped { + fn drop(&mut self) { + self.0 = 0; + } +} + +/// The address of an `Arc`'s `Dropped`, of a type a local drops too. +pub fn dropped() -> usize { + let shared = Arc::new(Dropped(1)); + let _local = Dropped(2); + Arc::as_ptr(&shared) as usize +} + +/// What a `Box` holds. +pub struct Boxed(pub u8); + +/// The address of a box's value. +pub fn boxed() -> usize { + let b = Box::new(Boxed(1)); + std::ptr::from_ref(&*b) as usize +} + +/// What a static holds. +pub struct Held(pub u8); + +static HELD: Held = Held(1); + +/// The address of a static. +pub fn held() -> usize { + std::ptr::from_ref(&HELD) as usize +} + +/// What JS is given whole. +pub struct Given(pub u8); + +/// A `Given` JS keeps where it likes. +pub fn given() -> Given { + Given(1) +} + +/// The address of a `Given`, which JS may keep anywhere. +pub fn given_at(given: &Given) -> usize { + std::ptr::from_ref(given) as usize +} + +/// What `Arc::into_inner` moves out. +pub struct Inner(pub u8); + +/// The address of an `Arc`'s value, and the value moved out of it. +pub fn inner(shared: Arc) -> usize { + let at = Arc::as_ptr(&shared) as usize; + at ^ Arc::into_inner(shared).map_or(0, |inner| usize::from(inner.0)) +} + +/// The values `Arc::try_unwrap` and `unwrap_or_clone` move out, which translate for no type. +pub fn unwrapped(shared: Arc, other: Arc) -> u8 { + Arc::try_unwrap(shared).map_or(0, |inner| inner.0) ^ Arc::unwrap_or_clone(other).0 +} + +/// What `Arc::make_mut` may move to a new `Arc`. +#[derive(Clone)] +pub struct Unshared(pub u8); + +/// The address of an `Arc`'s value made its own. +pub fn unshared(shared: &mut Arc) -> usize { + Arc::make_mut(shared).0 += 1; + Arc::as_ptr(shared) as usize +} + +/// What `Arc::get_mut` writes whole. +pub struct Rewritten(pub u8); -/// The address of a part an alignment fixes. -pub fn late(early: &Early) -> usize { - std::ptr::from_ref(&early.1) as usize +/// The address of an `Arc`'s value written whole through `get_mut`. +pub fn rewritten(shared: &mut Arc) -> usize { + if let Some(value) = Arc::get_mut(shared) { + *value = Rewritten(2); + } + Arc::as_ptr(shared) as usize } diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index e602bc8a2..18f32c003 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -1,12 +1,21 @@ package = "reject-fixture" roots = [ + "addresses::borrowed", + "addresses::boxed", "addresses::cyclic", + "addresses::dropped", "addresses::elements", - "addresses::late", + "addresses::given", + "addresses::given_at", + "addresses::held", + "addresses::inner", "addresses::made", "addresses::parts", "addresses::place_keys", "addresses::places", + "addresses::rewritten", + "addresses::unshared", + "addresses::unwrapped", "alloc::addressed", "alloc::alloc_retyped", "alloc::allocated", diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index b4e04dbfe..226f065c0 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -313,46 +313,66 @@ function nowhere(): never { throw new Error("rust2-ts: a read or write through a pointer to no memory"); } -/** The address `addr` gave each object, while JS keeps it. */ +/** The address `addr` gave each `Arc`'s or `Rc`'s value, or `UNREAD` once freed before any. */ const ADDRESSED = /* @__PURE__ */ new WeakMap(); -/** The addresses of objects JS no longer keeps, by the size and alignment of their blocks. */ -const FREED = /* @__PURE__ */ new Map(); +/** What `ADDRESSED` holds for a value whose allocation was freed before its address was read. */ +const UNREAD = -1; -/** Free an object's address once JS no longer keeps it, as an allocator reuses a freed block. */ -const UNADDRESSED = /* @__PURE__ */ new FinalizationRegistry<[string, number]>(([block, at]) => { - let free = FREED.get(block); - if (free === undefined) FREED.set(block, (free = [])); - free.push(at); -}); +/** The block each address `addr` gave is in, by its size and alignment, and where it starts, while its allocation lives. */ +const BLOCKS = /* @__PURE__ */ new Map(); + +/** The addresses of freed allocations, by the size and alignment of their blocks. */ +const FREED = /* @__PURE__ */ new Map(); /** Where the next new block starts: past wasm32's first page, as a heap is. */ let brk = 0x10000; +/** Whether `addr` has given an address, after which freeing an allocation frees its own. */ +let addressing = false; + /** - * `p as usize`, of a pointer to a value of `size` bytes aligned to `align`, which the - * program keeps where `kept` aligns it: null's 0, a dangling pointer's `align`, an - * address no memory is at, or else the object's own, as an allocator places each - * allocation: a multiple of `kept`, in a block no other object JS keeps overlaps, with - * room before it for an `Arc`'s counts. Running out of wasm32's addresses aborts, as a - * failed allocation does. + * `p as usize`, of a pointer to a value of `size` bytes aligned to `align`: null's 0, a + * dangling pointer's `align`, an address no memory is at, or else that of the value's + * `Arc` or `Rc` allocation, which `unaddress` frees, as an allocator places it: after its + * two counts, in a block no live allocation's overlaps, reusing a freed one. Running out + * of wasm32's addresses aborts, as a failed allocation does. */ -export function addr(p: object, size: number, align: number, kept: number): number { +export function addr(p: object, size: number, align: number): number { if (p === NULL) return 0; if (p === DANGLING) return align; if (p instanceof Address) return p.addr; - let at = ADDRESSED.get(p); - if (at !== undefined) return at; - const block = `${size}:${kept}`; - at = FREED.get(block)?.pop(); - if (at === undefined) { - at = Math.ceil((brk + Math.max(16, kept)) / kept) * kept; - if (at + size > 2 ** 32) throw new Abort(`memory allocation of ${size} bytes failed`); - brk = at + size; - } - ADDRESSED.set(p, at); - UNADDRESSED.register(p, [block, at]); - return at; + const at = ADDRESSED.get(p); + if (at !== undefined && at !== UNREAD) return at; + addressing = true; + // The two `usize` counts, then the value at a multiple of its alignment. + const head = Math.ceil(8 / align) * align; + const block = `${head + size}:${Math.max(align, 4)}`; + let start = FREED.get(block)?.pop(); + if (start === undefined) { + start = Math.ceil(brk / Math.max(align, 4)) * Math.max(align, 4); + if (start + head + size > 2 ** 32) throw new Abort(`memory allocation of ${size} bytes failed`); + brk = start + head + size; + } + ADDRESSED.set(p, start + head); + BLOCKS.set(start + head, [block, start]); + // A pointer to a freed allocation keeps the address it had, which another may reuse. + if (at === UNREAD) unaddress(p); + return start + head; +} + +/** Free the address of `value`, whose `Arc` or `Rc` allocation Rust frees now. */ +export function unaddress(value: unknown): void { + if (!addressing || (typeof value !== "object" && typeof value !== "function") || value === null) return; + const at = ADDRESSED.get(value); + if (at === undefined) return void ADDRESSED.set(value, UNREAD); + const held = BLOCKS.get(at); + if (held === undefined) return; + BLOCKS.delete(at); + const [block, start] = held; + let free = FREED.get(block); + if (free === undefined) FREED.set(block, (free = [])); + free.push(start); } /** The array of one element that a pointer to a value in no array points into, by its place. */ diff --git a/ts/runtime/rs/collections.ts b/ts/runtime/rs/collections.ts index 1c21e4b3e..2b524489f 100644 --- a/ts/runtime/rs/collections.ts +++ b/ts/runtime/rs/collections.ts @@ -64,9 +64,9 @@ export function cmp(a: T, b: T): Ordering { return lt(x, y) ? -1 : lt(y, x) ? 1 : 0; } -/** `Ord::cmp` of raw pointers to values of `size` bytes aligned to `align` and kept where `kept` aligns them, which compares their addresses. */ -export function cmpAddr(size: number, align: number, kept: number): (a: object, b: object) => Ordering { - return (a, b) => cmp(addr(a, size, align, kept), addr(b, size, align, kept)); +/** `Ord::cmp` of raw pointers to values of `size` bytes aligned to `align`, which compares their addresses. */ +export function cmpAddr(size: number, align: number): (a: object, b: object) => Ordering { + return (a, b) => cmp(addr(a, size, align), addr(b, size, align)); } /** diff --git a/ts/runtime/rs/hash.ts b/ts/runtime/rs/hash.ts index 3605ccc88..b80ca8fc1 100644 --- a/ts/runtime/rs/hash.ts +++ b/ts/runtime/rs/hash.ts @@ -639,9 +639,9 @@ export function hashUsize(value: number, s: Hasher): void { s.usize(value); } -/** `Hash::hash` of a raw pointer to a value of `size` bytes aligned to `align` and kept where `kept` aligns it, which hashes its address. */ -export function hashAddr(size: number, align: number, kept: number): (p: object, s: Hasher) => void { - return (p, s) => s.usize(addr(p, size, align, kept)); +/** `Hash::hash` of a raw pointer to a value of `size` bytes aligned to `align`, which hashes its address. */ +export function hashAddr(size: number, align: number): (p: object, s: Hasher) => void { + return (p, s) => s.usize(addr(p, size, align)); } /** `Hash::hash` of an `i8`, or of a discriminant of `Ordering`, whose `repr` it is. */ diff --git a/ts/runtime/rs/task.ts b/ts/runtime/rs/task.ts index 4b11ff5d5..a5cbd25ed 100644 --- a/ts/runtime/rs/task.ts +++ b/ts/runtime/rs/task.ts @@ -4,7 +4,7 @@ // A value whose drop JS can observe has `[Symbol.dispose]`, which runs its `Drop` impl // and drops its fields. `None` is `undefined`, which drops as nothing. -import { Abort, addressAt, arrayAssign, type ByteSlice, type CloneFrom, cloneOver, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtDebugTupleFields, fmtPad, fmtStr, type None, NULL, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrErase, ptrHome, ptrNowhere, ptrWrite, type Result, Slice, stringAsBytes, type Vec } from "../rs.ts"; +import { Abort, addressAt, arrayAssign, type ByteSlice, type CloneFrom, cloneOver, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtDebugTupleFields, fmtPad, fmtStr, type None, NULL, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrErase, ptrHome, ptrNowhere, ptrWrite, type Result, Slice, stringAsBytes, unaddress, type Vec } from "../rs.ts"; import type { Hasher } from "./hash.ts"; import type { Location } from "./location.ts"; @@ -125,10 +125,23 @@ export class Arc implements Disposable { } [Symbol.dispose](): void { - if (--this.strong === 0) drop(this.value); + if (--this.strong === 0) { + this.weak++; + drop(this.value); + release(this); + } } } +/** + * Release the weak count `arc` holds while its last strong one drops its value, as std's + * strong `Arc`s share one, freeing the allocation if no `Weak` is left. + */ +function release(arc: Arc): void { + const root = arcRoot(arc); + if (--root.weak === 0) unaddress(root.value); +} + /** * `Arc` unsized from `arc`, which it shares one count with, as Rust's one * allocation does: `value` is the trait object over `arc`'s value, which the last of @@ -160,7 +173,11 @@ export class ArcDyn implements Disposable { } [Symbol.dispose](): void { - if (--this.arc.strong === 0) drop(this.value); + if (--this.arc.strong === 0) { + this.arc.weak++; + drop(this.value); + release(this.arc); + } } } @@ -218,7 +235,11 @@ export function arcStrBytes(arc: Arc): Arc { * by `f`, or not at all without one, as a borrow. */ export function arcDrop(arc: Arc, f?: (value: T) => void): void { - if (--arc.strong === 0) f?.(arc.value); + if (--arc.strong === 0) { + arc.weak++; + f?.(arc.value); + release(arc); + } } /** `Arc::clone`: the same value, counted once more. */ @@ -396,7 +417,7 @@ export class Weak implements Disposable { } [Symbol.dispose](): void { - this.arc.weak--; + if (--this.arc.weak === 0 && this.arc.strong === 0) unaddress(arcRoot(this.arc).value); } } diff --git a/ts/src/census.rs b/ts/src/census.rs index 9d3e14d89..b71001310 100644 --- a/ts/src/census.rs +++ b/ts/src/census.rs @@ -142,6 +142,8 @@ const VERBATIM: &[&str] = &[ ".await", "?", "Arc", + "Arc::into_inner", + "Arc::make_mut", "Arc::new_cyclic", "Binary", "Buf::copy_to_bytes", @@ -167,6 +169,9 @@ const VERBATIM: &[&str] = &[ "Pending", "Pointer", "Poll", + "Rc", + "Rc::into_inner", + "Rc::make_mut", "Ready", "Reborrow", "Result", diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index cca56c628..ba517b764 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -1,18 +1,18 @@ //! Addresses read as integers: `p as usize`, `addr`, and `expose_provenance`. //! //! Rust promises of an allocation's address only that it is aligned and that no other -//! live allocation overlaps it, so `rs.addr` numbers each object as an allocator would -//! place it, on its first read, while JS keeps the object. A pointer's address is then -//! exact wherever its identity is, as [`Translator::unidentified`] decides for comparing -//! pointers, with one more condition: Rust fixes how far apart two values in one -//! allocation are, as the fields of a struct or the elements of a `Vec` are, which the -//! objects JS keeps apart can't show. So an address is read only of a type no allocation -//! holds beside another whose address is read, nor holds many of. Nothing makes a pointer -//! of an address, which JS has no memory at. +//! live allocation overlaps it. JS frees an object only when it collects it, which no +//! program can see or time, so the one allocation JS frees as Rust does is an `Arc`'s or +//! `Rc`'s, when its strong and weak counts reach 0. `rs.addr` numbers each value an `Arc` +//! or `Rc` holds as an allocator places that allocation, and frees the number with it. A +//! pointer's address is then exact wherever its identity is, as +//! [`Translator::unidentified`] decides for comparing pointers, of a type the program keeps +//! only as an `Arc`'s or `Rc`'s value, which it never moves out of one. Nothing makes a +//! pointer of an address, which JS has no memory at. -use rustc_abi::{FieldsShape, Variants}; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; -use rustc_middle::ty::layout::{LayoutCx, TyAndLayout}; +use rustc_hir::def_id::DefId; +use rustc_middle::mir::{self, Operand, Rvalue, StatementKind, TerminatorKind}; use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; use rustc_span::Span; @@ -30,11 +30,9 @@ pub fn address<'tcx>(b: &mut Body<'_, '_, 'tcx>, p: Expr, ty: Ty<'tcx>, span: Sp } } -/// The size and alignment `rs.addr` reads the address of a pointer to `ty` by, with the -/// alignment of where the program keeps one, or the +/// The size and alignment `rs.addr` reads the address of a pointer to `ty` by, or the /// refusal of a type whose address JS has no counterpart of. -pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Result<[Expr; 3], Expr> { - let (tcx, env) = (b.t.tcx, b.t.typing_env_pub()); +pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Result<[Expr; 2], Expr> { // Only an instance decides what a parameter is, and is refused on its own. if !(b.t.polymorphic() && ty.has_param()) { let why = match b.t.pointee(ty) { @@ -47,25 +45,14 @@ pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Resul if let Some(why) = why { return Err(b.err(span, format!("{what}, {why}"))); } - let read = b.t.sited(span, what.clone()); + let read = b.t.sited(span, what); b.t.addresses.push((ty, read.0, read.1)); - let layout = tcx.layout_of(env.as_query_input(ty)).ok(); - let size = layout.map_or(0, |l| l.size.bytes()); - let own = layout.map_or(1, |l| l.align.abi.bytes()); - return match b.t.alignment(ty) { - Ok(kept) => Ok([size, own, kept].map(|n| Expr::lit(n.to_string()))), - Err(holder) => Err(b.err( - span, - format!( - "{what}, which `{holder}` holds at an offset its alignment fixes the address of, which JS can't show" - ), - )), - }; } + let (tcx, env) = (b.t.tcx, b.t.typing_env_pub()); let layout = tcx.layout_of(env.as_query_input(ty)).ok(); let size = layout.map_or(0, |l| l.size.bytes()); let align = layout.map_or(1, |l| l.align.abi.bytes()); - Ok([size, align, align].map(|n| Expr::lit(n.to_string()))) + Ok([size, align].map(|n| Expr::lit(n.to_string()))) } impl<'a, 'tcx> Translator<'a, 'tcx> { @@ -86,34 +73,19 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { .push((self.tcx.erase_and_anonymize_regions(ty), span, msg)); } - /// The alignment `rs.addr` numbers the addresses of a `ty` by: the largest of its own - /// and each allocation's that holds one at an offset that alignment keeps, as an - /// `Arc`'s counts before its value do, or else the type of an allocation that holds one - /// where its alignment fixes the address apart from it. - fn alignment(&mut self, ty: Ty<'tcx>) -> Result> { - let tcx = self.tcx; - let ty = super::copy::storage(tcx, ty); - if let Some(&found) = self.alignments.get(&ty) { - return found; - } - let env = ty::TypingEnv::fully_monomorphized(); - let own = tcx.layout_of(env.as_query_input(ty)).map_or(1, |l| l.align.abi.bytes()); - let mut placed = Placed { - tcx, - cx: LayoutCx::new(tcx, env), - target: ty, - own, - align: own, - roots: self.writes.locals(tcx).map(|t| (t, None)).collect(), - seen: FxHashSet::default(), - }; - let found = placed.run(); - self.alignments.insert(ty, found); - found + /// Note `name`, such as `into_inner`, moving a `ty` out of its `Arc` or `Rc` at `span`, + /// refused once translation settles if the program reads the address of one. + pub fn arc_moved(&mut self, ty: Ty<'tcx>, name: &str, span: Span) { + let msg = format!( + "`{name}` of an `Arc` or `Rc` of `{ty}`, which moves it out of the allocation whose address the program reads" + ); + let (span, msg) = self.sited(span, msg); + self.arc_moves.push((super::copy::storage(self.tcx, ty), span, msg)); } - /// Refuse each address read of a type an allocation holds beside another whose address - /// is read, or many of, and each `Weak::as_ptr` of a value `Arc::new_cyclic` makes. + /// Refuse each address read of a type the program keeps anywhere but as an `Arc`'s or + /// `Rc`'s value, or writes whole in place, each move of one out of its `Arc`, and each + /// `Weak::as_ptr` of a value `Arc::new_cyclic` makes. pub(super) fn refuse_addresses(&mut self) { for (ty, span, msg) in std::mem::take(&mut self.weak_pointers) { if self.cyclic.contains(&ty) { @@ -121,308 +93,217 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } } let reads = std::mem::take(&mut self.addresses); + let moves = std::mem::take(&mut self.arc_moves); if reads.is_empty() { return; } let tcx = self.tcx; let addressed: FxHashSet> = reads.iter().map(|&(ty, ..)| super::copy::storage(tcx, ty)).collect(); - let shared = shared(tcx, &addressed, self.writes.locals(tcx)); + let mut walk = Kept::new(tcx, &addressed); + walk.roots + .extend(self.writes.constants(tcx).map(|ty| (ty, Some(Holder::Static)))); + walk.roots + .extend(self.writes.kept(tcx).map(|ty| (ty, Some(Holder::Local)))); + // What the outside calls takes and returns whole, which JS keeps where it likes. + for instance in &self.roots { + let sig = self.sig(instance.def_id(), instance.args); + let fn_name = tcx.def_path_str(instance.def_id()); + walk.roots.extend( + sig.inputs_and_output + .iter() + .map(|ty| (ty, Some(Holder::Js(fn_name.clone())))), + ); + } + // Last, so first: what holds one by value names it before the local it came from. + walk.roots.extend(self.writes.locals(tcx).map(|ty| (ty, None))); + let kept = walk.run(); for (ty, span, msg) in reads { - if let Some(&(holder, many)) = shared.get(&super::copy::storage(tcx, ty)) { - let held = match many { - true => "many of", - false => "beside another value whose address is read,", - }; - let why = format!( - "{msg}, which `{holder}` holds {held} in one allocation, as far apart as Rust fixes and JS can't show" - ); - self.refuse(span, why); + let ty = super::copy::storage(tcx, ty); + let why = match kept.get(&ty) { + Some(Holder::Type(holder)) => format!("which `{holder}` holds"), + Some(Holder::Local) => "which a local keeps that the program borrows, drops, or gives std".into(), + Some(Holder::Static) => "which a static or constant holds".into(), + Some(Holder::Js(name)) => format!("which JS keeps, as `{name}` takes or returns one"), + None if self.written_whole(ty) => { + "which the crate writes whole in place, moving another into its `Arc`".into() + } + None => continue, + }; + self.refuse( + span, + format!("{msg}, {why}, where only an `Arc`'s or `Rc`'s value has an address JS frees as Rust does"), + ); + } + for (ty, span, msg) in moves { + if addressed.contains(&ty) { + self.refuse(span, msg); } } } } -/// Each of `addressed` that an allocation holds beside another, or many of, by the type -/// it holds them in and whether it holds many, of the allocations `locals` hold or point at. -fn shared<'tcx>( +/// What `body` keeps where the program may read its address, other than as an `Arc`'s or +/// `Rc`'s value: the type of each local it borrows whole, drops, or gives a function that +/// `own` says doesn't translate, but `Arc::new`, `Rc::new`, or their `pin`, and apart, +/// the type each constant reference points at, as a static's is. +pub(super) fn kept<'tcx>( tcx: TyCtxt<'tcx>, - addressed: &FxHashSet>, - locals: impl Iterator>, -) -> FxHashMap, (Ty<'tcx>, bool)> { - let mut held = Held::new(tcx, addressed); - held.roots.extend(locals.map(|ty| (ty, false))); - let mut seen = FxHashSet::default(); - let mut shared = FxHashMap::default(); - while let Some((ty, many)) = held.roots.pop() { - if !seen.insert((ty, many)) { - continue; + body: &mir::Body<'tcx>, + own: &dyn Fn(DefId) -> bool, +) -> (Vec>, Vec>) { + let mut locals = FxHashSet::default(); + let mut constants = Vec::new(); + let constant = |operand: &Operand<'tcx>, constants: &mut Vec>| { + if let Operand::Constant(c) = operand + && let ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) = *c.ty().kind() + { + constants.push(pointee); } - let n = held.count(ty); - if n < 2 && !(many && n == 1) { - continue; + }; + for data in body.basic_blocks.iter() { + for stmt in &data.statements { + let StatementKind::Assign(assign) = &stmt.kind else { + continue; + }; + match &assign.1 { + Rvalue::Ref(_, _, place) | Rvalue::RawPtr(_, place) if let Some(local) = place.as_local() => { + locals.insert(local); + } + Rvalue::Use(op, _) | Rvalue::Cast(_, op, _) | Rvalue::UnaryOp(_, op) => constant(op, &mut constants), + Rvalue::Aggregate(_, ops) => ops.iter().for_each(|op| constant(op, &mut constants)), + _ => {} + } } - for &each in addressed { - let one = FxHashSet::from_iter([each]); - if Held::new(tcx, &one).count(ty) > 0 { - shared - .entry(each) - .or_insert((ty, many || matches!(ty.kind(), ty::Array(..) | ty::Slice(_)))); + match &data.terminator().kind { + TerminatorKind::Drop { place, .. } if let Some(local) = place.as_local() => { + locals.insert(local); + } + TerminatorKind::Call { func, args, .. } => { + args.iter().for_each(|arg| constant(&arg.node, &mut constants)); + let env = ty::TypingEnv::post_analysis(tcx, body.source.def_id()); + let given = match func.const_fn_def() { + Some((def_id, args)) + if let Ok(Some(instance)) = ty::Instance::try_resolve(tcx, env, def_id, args) + && let ty::InstanceKind::Item(did) = instance.def + && own(did) => + { + false + } + Some((def_id, _)) => !matches!( + super::library::key(tcx, def_id), + Some((owner, name)) if matches!(owner.as_str(), "Arc" | "Rc") && matches!(name.as_str(), "new" | "pin") + ), + None => true, + }; + if given { + locals.extend(args.iter().filter_map(|arg| arg.node.place()?.as_local())); + } } + _ => {} } } - shared + let locals = locals.into_iter().map(|local| body.local_decls[local].ty).collect(); + (locals, constants) +} + +/// Where the program keeps a value of an addressed type other than as an `Arc`'s or +/// `Rc`'s value. +#[derive(Clone)] +enum Holder<'tcx> { + /// By value in a value of this type: a field, an element, a payload, or a capture, or + /// in a `Box` or a collection. + Type(Ty<'tcx>), + /// In a local a body borrows, drops, or gives std. + Local, + /// In a static or constant. + Static, + /// Where JS keeps what the function named takes or returns. + Js(String), } -/// How many values of the `addressed` types a value holds, and the allocations it points -/// at, each with whether it may hold many. -struct Held<'s, 'tcx> { +/// The addressed types each value a program keeps holds where they have no address JS +/// frees as Rust does, walking what each holds by value and points at. +struct Kept<'s, 'tcx> { tcx: TyCtxt<'tcx>, addressed: &'s FxHashSet>, - counts: FxHashMap, u8>, - roots: Vec<(Ty<'tcx>, bool)>, + /// The values still to look in, each with what holds it, or `None` where it is a + /// local, an `Arc`'s or `Rc`'s value, or what a reference points at, which hold it + /// where an address may be its own. + roots: Vec<(Ty<'tcx>, Option>)>, + seen: FxHashSet>, + found: FxHashMap, Holder<'tcx>>, } -impl<'s, 'tcx> Held<'s, 'tcx> { +impl<'s, 'tcx> Kept<'s, 'tcx> { fn new(tcx: TyCtxt<'tcx>, addressed: &'s FxHashSet>) -> Self { - Held { + Kept { tcx, addressed, - counts: FxHashMap::default(), roots: Vec::new(), + seen: FxHashSet::default(), + found: FxHashMap::default(), } } - /// How many values of the addressed types a `ty` holds by value, up to 2, which is - /// many. A std type holds its type arguments as its kind says, and the crate's own - /// raw pointers each point at one value, as no arithmetic moves one to an object. - fn count(&mut self, ty: Ty<'tcx>) -> u8 { + fn run(mut self) -> FxHashMap, Holder<'tcx>> { let tcx = self.tcx; let env = ty::TypingEnv::fully_monomorphized(); - let ty = tcx - .try_normalize_erasing_regions(env, ty::Unnormalized::new_wip(ty)) - .unwrap_or(ty); - if let Some(&n) = self.counts.get(&ty) { - return n; - } - self.counts.insert(ty, 0); - let own = u8::from(self.addressed.contains(&super::copy::storage(tcx, ty))); - let parts = match *ty.kind() { - ty::Array(elem, len) => match (len.try_to_target_usize(tcx), self.count(elem)) { - (Some(0), _) | (_, 0) => 0, - (Some(1), n) => n, - _ => 2, - }, - ty::Slice(elem) => 2 * self.count(elem).min(1), - ty::Pat(elem, _) => self.count(elem), - ty::Tuple(tys) => total(tys.iter().map(|t| self.count(t))), - ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) => self.point(pointee, false), - ty::Adt(adt, args) if super::library::is_std(tcx, adt.did()) => match Std::of(tcx, adt.did()) { - // An allocation of one value, or a pointer to one. - Some(Std::Box | Std::Arc | Std::Weak | Std::NonNull) => self.point(args.type_at(0), false), - _ if adt.is_phantom_data() => 0, - Some(Std::Range | Std::RangeInclusive) => 2 * self.count(args.type_at(0)).min(1), - // Each of its type arguments, at most one of each, by value. - _ if by_value(tcx, adt) => total(args.types().map(|t| self.count(t))), - // A collection, or anything else of std's, may hold many of each in one - // allocation of its own. - _ => { - self.roots.extend(args.types().map(|t| (t, true))); - 0 - } - }, - ty::Adt(adt, args) => { - let mut counts = adt.variants().iter().map(|v| { - v.fields - .iter() - .map(|f| self.count(f.ty(tcx, args).skip_norm_wip())) - .collect::>() - }); - match adt.is_union() { - // Its fields overlap, as an enum's variants do. - true => counts.next().into_iter().flatten().max().unwrap_or(0), - false => counts.map(|c| total(c.into_iter())).max().unwrap_or(0), - } - } - ty::Closure(_, args) => total(args.as_closure().upvar_tys().iter().map(|t| self.count(t))), - ty::CoroutineClosure(_, args) => { - total(args.as_coroutine_closure().upvar_tys().iter().map(|t| self.count(t))) - } - // Its captures, and the locals it keeps across an `.await`. - ty::Coroutine(def_id, args) => { - let upvars = total(args.as_coroutine().upvar_tys().iter().map(|t| self.count(t))); - let saved = match tcx.coroutine_layout(def_id, args) { - Ok(layout) => total(layout.field_tys.iter().map(|f| { - let saved = ty::EarlyBinder::bind(tcx, f.ty); - self.count(tcx.instantiate_and_normalize_erasing_regions(args, env, saved)) - })), - Err(_) => 0, - }; - upvars.saturating_add(saved) - } - _ => 0, - }; - let n = own.saturating_add(parts).min(2); - self.counts.insert(ty, n); - n - } - - /// A pointer to `pointee`, an allocation of its own, which holds nothing beside the - /// pointer: of many where `many` says it may be. - fn point(&mut self, pointee: Ty<'tcx>, many: bool) -> u8 { - self.roots.push((pointee, many)); - 0 - } -} - -/// Where the program's allocations hold values of the `target` type: at what offset -/// from where each starts, aligned as its type is, or as an `Arc`'s allocation is. -struct Placed<'tcx> { - tcx: TyCtxt<'tcx>, - cx: LayoutCx<'tcx>, - target: Ty<'tcx>, - /// The target's own alignment. - own: u64, - /// The largest alignment of an allocation holding one at an offset it keeps. - align: u64, - /// The allocations still to look in, each with the offset of its value and the - /// alignment of the allocation, if it holds more before it, as an `Arc`'s does. - roots: Vec<(Ty<'tcx>, Option<(u64, u64)>)>, - seen: FxHashSet<(Ty<'tcx>, Option<(u64, u64)>)>, -} - -impl<'tcx> Placed<'tcx> { - fn run(&mut self) -> Result> { - let env = ty::TypingEnv::fully_monomorphized(); - while let Some((ty, header)) = self.roots.pop() { - let ty = (self.tcx) + while let Some((ty, holder)) = self.roots.pop() { + let ty = tcx .try_normalize_erasing_regions(env, ty::Unnormalized::new_wip(ty)) .unwrap_or(ty); - if !self.seen.insert((ty, header)) { - continue; + let ty = tcx.erase_and_anonymize_regions(ty); + if let Some(holder) = &holder + && self.addressed.contains(&super::copy::storage(tcx, ty)) + { + self.found + .entry(super::copy::storage(tcx, ty)) + .or_insert_with(|| holder.clone()); } - // A slice's elements are each at a multiple of their own alignment. - let ty = match ty.kind() { - ty::Slice(elem) => *elem, - ty::Str | ty::Dynamic(..) => continue, - _ => ty, - }; - let Ok(layout) = self.tcx.layout_of(env.as_query_input(ty)) else { + // What a type holds is held alike wherever the type is. + if !self.seen.insert(ty) { continue; - }; - let (offset, align) = header.unwrap_or((0, layout.align.abi.bytes())); - self.visit(layout, offset, align, ty)?; - } - Ok(self.align) - } - - /// Look for the target in `layout`, `offset` bytes into an allocation of `holder` - /// aligned to `align`. - fn visit(&mut self, layout: TyAndLayout<'tcx>, offset: u64, align: u64, holder: Ty<'tcx>) -> Result<(), Ty<'tcx>> { - let tcx = self.tcx; - if super::copy::storage(tcx, layout.ty) == self.target && align > self.own { - match offset % align { - 0 => self.align = self.align.max(align), - _ => return Err(holder), } - } - match *layout.ty.kind() { - ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) => self.roots.push((pointee, None)), - ty::Adt(adt, args) if super::library::is_std(tcx, adt.did()) => match Std::of(tcx, adt.did()) { - Some(Std::Box | Std::NonNull) => self.roots.push((args.type_at(0), None)), - // The value after the counts, at an offset of its own alignment. - Some(Std::Arc | Std::Weak) => { - let value = args.type_at(0); - let env = ty::TypingEnv::fully_monomorphized(); - if let Ok(inner) = tcx.layout_of(env.as_query_input(value)) { - let pointer = tcx.data_layout.pointer_size().bytes(); - let align = inner.align.abi.bytes(); - let at = (2 * pointer).div_ceil(align) * align; - let whole = align.max(tcx.data_layout.pointer_align().abi.bytes()); - self.roots.push((value, Some((at, whole)))); + let held = Some(Holder::Type(ty)); + match *ty.kind() { + ty::Array(elem, _) | ty::Slice(elem) | ty::Pat(elem, _) => self.roots.push((elem, held)), + ty::Tuple(tys) => self.roots.extend(tys.iter().map(|t| (t, held.clone()))), + ty::Ref(_, pointee, _) | ty::RawPtr(pointee, _) => self.roots.push((pointee, None)), + ty::Adt(adt, args) if super::library::is_std(tcx, adt.did()) => match Std::of(tcx, adt.did()) { + // The value of its own allocation, which JS frees as Rust does. + Some(Std::Arc | Std::Weak | Std::NonNull) => self.roots.push((args.type_at(0), None)), + _ if adt.is_phantom_data() => {} + // Each of its type arguments, by value or in a `Box` or a collection. + _ => self.roots.extend(args.types().map(|t| (t, held.clone()))), + }, + ty::Adt(adt, args) => self.roots.extend( + adt.all_fields() + .map(|f| (f.ty(tcx, args).skip_norm_wip(), held.clone())), + ), + ty::Closure(_, args) => self + .roots + .extend(args.as_closure().upvar_tys().iter().map(|t| (t, held.clone()))), + ty::CoroutineClosure(_, args) => self.roots.extend( + args.as_coroutine_closure() + .upvar_tys() + .iter() + .map(|t| (t, held.clone())), + ), + // Its captures, and the locals it keeps across an `.await`. + ty::Coroutine(def_id, args) => { + self.roots + .extend(args.as_coroutine().upvar_tys().iter().map(|t| (t, held.clone()))); + if let Ok(layout) = tcx.coroutine_layout(def_id, args) { + for f in layout.field_tys.iter() { + let saved = ty::EarlyBinder::bind(tcx, f.ty); + let saved = tcx.instantiate_and_normalize_erasing_regions(args, env, saved); + self.roots.push((saved, held.clone())); + } } } - _ if adt.is_phantom_data() => {} - _ if by_value(tcx, adt) => self.fields(layout, offset, align, holder)?, - _ => self.roots.extend(args.types().map(|t| (t, None))), - }, - ty::Coroutine(..) => { - let held = super::copy::by_value(tcx, ty::TypingEnv::fully_monomorphized(), layout.ty) - .any(|part| super::copy::storage(tcx, part) == self.target); - if held && layout.align.abi.bytes() > self.own { - return Err(layout.ty); - } - self.fields(layout, offset, align, holder)?; - } - ty::Adt(..) | ty::Tuple(_) | ty::Array(..) | ty::Closure(..) | ty::CoroutineClosure(..) | ty::Pat(..) => { - self.fields(layout, offset, align, holder)? + _ => {} } - _ => {} } - Ok(()) + self.found } - - /// Look in each field of each variant of `layout`, and in the first two elements of an - /// array, whose others are where those two say. - fn fields(&mut self, layout: TyAndLayout<'tcx>, offset: u64, align: u64, holder: Ty<'tcx>) -> Result<(), Ty<'tcx>> { - let variants = match &layout.variants { - Variants::Multiple { variants, .. } => { - variants.indices().map(|v| layout.for_variant(&self.cx, v)).collect() - } - Variants::Single { .. } => vec![layout], - Variants::Empty => Vec::new(), - }; - for variant in variants { - let count = match variant.fields { - FieldsShape::Array { count, .. } => count.min(2) as usize, - _ => variant.fields.count(), - }; - for i in 0..count { - let field = variant.field(&self.cx, i); - self.visit(field, offset + variant.fields.offset(i).bytes(), align, holder)?; - } - } - Ok(()) - } -} - -/// Whether std's `adt` holds each of its type arguments by value, at most one of each, as -/// [`Held::count`] counts them. -fn by_value(tcx: TyCtxt<'_>, adt: ty::AdtDef<'_>) -> bool { - adt.is_unsafe_cell() - || matches!( - Std::of(tcx, adt.did()), - Some( - Std::Option - | Std::Result - | Std::Cow - | Std::Cell - | Std::RefCell - | Std::Mutex - | Std::OnceCell - | Std::OnceLock - | Std::LazyCell - | Std::LazyLock - | Std::Poll - | Std::Ready - | Std::Pin - | Std::ControlFlow - | Std::Bound - | Std::Range - | Std::RangeInclusive - | Std::RangeTo - | Std::RangeFrom - | Std::RangeToInclusive - | Std::Reverse - | Std::Wrapping - | Std::Saturating - | Std::AssertUnwindSafe - | Std::MaybeUninit - | Std::ManuallyDrop - ) - ) -} - -/// The sum of `counts`, up to many. -fn total(counts: impl Iterator) -> u8 { - counts.fold(0, u8::saturating_add) } diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 84f3bc887..d2dd0207a 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -92,6 +92,11 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { .is_some_and(|holders| writes.contains(holders, false, self.tcx, ty)) } + /// Whether the crate may write a whole `ty`, or a value holding one, in place. + pub fn written_whole(&self, ty: Ty<'tcx>) -> bool { + self.writes.contains(&self.writes.replaced, true, self.tcx, ty) + } + /// Whether a `ty` is, or holds by value at any depth, a part that `object` says is a JS /// object, into which a raw pointer may be made through a reference that the crate's /// code can't follow back to where it was borrowed, by any body or by `body` alone. A @@ -470,6 +475,11 @@ pub struct Writes<'tcx> { /// The type of each local of each body read, by that body: every allocation the /// crate's code makes is one, or what one points at. locals: FxIndexSet<(DefId, Ty<'tcx>)>, + /// The locals each body read keeps where their address may be read, by that body, as + /// [`super::address::kept`] finds them. + kept: FxIndexSet<(DefId, Ty<'tcx>)>, + /// What the constant references of each body read point at, by that body. + constants: FxIndexSet<(DefId, Ty<'tcx>)>, } /// Types, each without type parameters or over those of the body naming it. @@ -519,6 +529,13 @@ impl<'tcx> Writes<'tcx> { .locals .insert((def_id, tcx.erase_and_anonymize_regions(local.ty))); } + let (kept, constants) = super::address::kept(tcx, body, own); + for ty in kept { + writes.kept.insert((def_id, tcx.erase_and_anonymize_regions(ty))); + } + for ty in constants { + writes.constants.insert((def_id, tcx.erase_and_anonymize_regions(ty))); + } // Another crate lists none of its closures' bodies. for local in body.local_decls.iter() { queue.extend(local.ty.walk().filter_map(|arg| match *arg.as_type()?.kind() { @@ -564,8 +581,29 @@ impl<'tcx> Writes<'tcx> { /// The type of each local of the bodies read, one over type parameters for each /// instance translation reached of its body. pub fn locals(&self, tcx: TyCtxt<'tcx>) -> impl Iterator> { + self.instances(tcx, &self.locals) + } + + /// The locals the bodies read keep where their address may be read, as + /// [`Self::locals`] are. + pub fn kept(&self, tcx: TyCtxt<'tcx>) -> impl Iterator> { + self.instances(tcx, &self.kept) + } + + /// What the constant references of the bodies read point at, as [`Self::locals`] are. + pub fn constants(&self, tcx: TyCtxt<'tcx>) -> impl Iterator> { + self.instances(tcx, &self.constants) + } + + /// Each of `types`, by the body naming it, one over type parameters for each instance + /// translation reached of its body. + fn instances<'s>( + &'s self, + tcx: TyCtxt<'tcx>, + types: &'s FxIndexSet<(DefId, Ty<'tcx>)>, + ) -> impl Iterator> + 's { let env = ty::TypingEnv::fully_monomorphized(); - self.locals.iter().flat_map(move |&(def_id, ty)| { + types.iter().flat_map(move |&(def_id, ty)| { let instances: Vec> = match ty.has_param() { false => vec![ty], true => (self.reached.get(&def_id).into_iter().flatten()) diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index 0f13d3541..c0bb11ef2 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -218,11 +218,11 @@ pub struct Translator<'a, 'tcx> { /// Whether a crate makes a `Bytes` or a `Vec` of a `String`'s bytes, whose capacity JS keeps none of. string_bytes: bool, /// Each read of an address, of a pointer to its type, refused once translation settles - /// if an allocation holds that type beside another read, as [`address`] says. + /// if the program keeps that type anywhere but as an `Arc`'s value, as [`address`] says. addresses: Vec<(Ty<'tcx>, Span, String)>, - /// The alignment `rs.addr` numbers each type's addresses by, as [`Self::alignment`] - /// found it. - alignments: rustc_data_structures::fx::FxHashMap, Result>>, + /// Each move of a value out of its `Arc` or `Rc`, by its type, refused once translation + /// settles if the program reads the address of one. + arc_moves: Vec<(Ty<'tcx>, Span, String)>, /// The types `Arc::new_cyclic` makes. cyclic: rustc_data_structures::fx::FxHashSet>, /// Each `Weak::as_ptr`, by the type it points at, refused once translation settles if @@ -517,7 +517,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { capacity_reads: Vec::new(), string_bytes: false, addresses: Vec::new(), - alignments: Default::default(), + arc_moves: Vec::new(), cyclic: Default::default(), weak_pointers: Vec::new(), drops: Default::default(), diff --git a/ts/src/lower/task.rs b/ts/src/lower/task.rs index 993c5abfc..2ee75e14f 100644 --- a/ts/src/lower/task.rs +++ b/ts/src/lower/task.rs @@ -163,6 +163,7 @@ pub fn call<'tcx>( match name { "get_mut" => Expr::rs(format!("arcGetMut{place}"), args), _ => { + b.t.arc_moved(value, &format!("{owner}::make_mut"), call.span); let cloned = shared_box(tcx, value).unwrap_or(value); let clone = super::collections::clone_fn(b, cloned, call.span); let arc = b.t.sig(call.def_id, call.args).inputs()[0].peel_refs(); @@ -221,6 +222,7 @@ pub fn call<'tcx>( ("Arc" | "Rc", "downgrade") | ("Weak", "new") => Expr::New(Box::new(Expr::Rs("Weak".into())), args), // The value of the last strong one, which its count says, and else its `None`. ("Arc" | "Rc", "into_inner") => { + b.t.arc_moved(call.args.type_at(0), &format!("{owner}::into_inner"), call.span); let none = b.t.none_arg(call.args.type_at(0)); Expr::rs("arcIntoInner", args.into_iter().chain(none).collect()) } diff --git a/ts/tests/pointers.ts b/ts/tests/pointers.ts index 77476adaa..1a0d137d0 100644 --- a/ts/tests/pointers.ts +++ b/ts/tests/pointers.ts @@ -131,3 +131,32 @@ const flagAt = rs.arcRaw(flag, rs.arcPlace(flag), "Arc"); assert.equal(rs.arcErase(rs.ptrAdd(rs.ptrAdd(flagAt, 1), -1), "bool", 1), flagAt); assert.equal(rs.arcErase(rs.NULL, "bool", 1), rs.NULL); assert.throws(() => rs.arcErase({ v: false }, "bool", 1), /only an `Arc`'s or `Rc`'s raw pointer keeps the place of/); + +// An `Arc`'s value's address, freed with the allocation once its strong and weak counts +// reach 0, which the next of its size reuses, as an allocator does, so churning far past +// wasm32's 4 GiB never runs out. One a `Weak` holds stays apart from every other. +const MIB = 2 ** 20; +const churned = new rs.Arc({ n: 0 }); +const churnedAt = rs.addr(churned.value, MIB, 8); +churned[Symbol.dispose](); +for (let n = 1; n <= 10_000; n++) { + const arc = new rs.Arc({ n }); + assert.equal(rs.addr(arc.value, MIB, 8), churnedAt); + arc[Symbol.dispose](); +} +const weakly = new rs.Arc({ n: 0 }); +const weak = new rs.Weak(weakly); +const weaklyAt = rs.addr(weakly.value, MIB, 8); +weakly[Symbol.dispose](); +const beside = new rs.Arc({ n: 1 }); +assert.notEqual(rs.addr(beside.value, MIB, 8), weaklyAt); +assert.equal(rs.addr(rs.weakAsPtr(weak), MIB, 8), weaklyAt); +weak[Symbol.dispose](); +assert.equal(rs.addr(new rs.Arc({ n: 2 }).value, MIB, 8), weaklyAt); +// A value whose allocation was freed before its address was read: a pointer to it has +// one, which the next allocation may reuse. +const unread = new rs.Arc({ n: 3 }); +unread[Symbol.dispose](); +const unreadAt = rs.addr(unread.value, MIB, 8); +assert.equal(rs.addr(new rs.Arc({ n: 4 }).value, MIB, 8), unreadAt); +assert.equal(rs.addr(unread.value, MIB, 8), unreadAt); From 3986bf1ed95e91e30edd70894683750b2b138749 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:27:58 -0800 Subject: [PATCH 12/13] fix(ts): number an addressed Arc as it is made A pointer read only after its allocation was freed took a block then, so two freed together could share an address, and frees before the first read reserved blocks for good. Now each Arc or Rc of a type whose address the program reads gets its address as it is made, by rs.arcAt, which a run settles like the comparisons it makes copies for, and keeps it once freed, as Rust's pointer does. Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/pointers.ts | 122 +++++++++++------- ts/fixtures/lang/pointers/src/pointers.rs | 16 +++ ts/runtime/rs.ts | 45 +++---- ts/runtime/rs/task.ts | 8 +- ts/src/lower/address.rs | 20 ++- ts/src/lower/library.rs | 8 +- ts/src/lower/mod.rs | 73 +++++++---- ts/src/lower/task.rs | 10 +- ts/tests/pointers.ts | 38 +++--- 9 files changed, 226 insertions(+), 114 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index 49da0320a..a7cf26d34 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -2865,10 +2865,11 @@ export function rawPlaces(data: rs.ByteSlice, out: rs.ByteVec): void { export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { let _59!: boolean; let _92!: boolean; - let _131!: boolean; - let _181!: boolean; + let _126!: boolean; + let _162!: boolean; + let _212!: boolean; let agree!: boolean; - let _240!: rs.u8; + let _271!: rs.u8; const registry: rs.Arc>> = new rs.Arc(new rs.RefCell(rs.btreeNew(rs.cmpTuple<[rs.u8, rs.usize], rs.Ordering>([rs.cmp, rs.cmp])) as rs.BTreeMap<[rs.u8, rs.usize], rs.u8>)); const left: rs.Arc> = new rs.Arc(new rs.Cell(0)); const align: rs.usize = 4; @@ -2894,7 +2895,7 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const byte: rs.u8 = _40[1]; const _45: rs.u8 = rs.u8Rem(byte, 4); if (_45 === 0) { - const keyed: rs.Arc = new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))); + const keyed: rs.Arc = rs.arcAt(new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))), 12, 4); const arc3 = keyed; const p: Keyed.Keyed = rs.arcRaw(arc3, arc3.value, "Arc"); const address: rs.usize = rs.addr(p, 12, 4); @@ -2922,6 +2923,33 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const _98: [rs.Weak, rs.usize] = [new rs.Weak(keyed), address]; weaks.items.push(_98); live.items.push(keyed); + if (!((byte & 4) !== 0)) { + continue; + } + const make: (a0: rs.u8) => rs.Arc = (id: rs.u8): rs.Arc => { + return rs.arcAt(new rs.Arc(new Keyed.Keyed(id, rs.arcClone(registry), rs.arcClone(left))), 12, 4); + }; + const pair: rs.Arc[] = [make(i & 0xff), make(i & 0xff)]; + const pointers: Keyed.Keyed[] = rs.arrayMap(rs.arrayFrom(pair), (a02: rs.Arc): Keyed.Keyed => { + const arc4 = a02; + return rs.arcRaw(arc4, arc4.value, "Arc"); + }); + rs.dropAll(pair); + const _123: rs.usize[] = rs.arrayMap(pointers, (p4: Keyed.Keyed): rs.usize => { + return rs.addr(p4, 12, 4); + }); + b8: { + const a: rs.usize = _123[0]; + const b: rs.usize = _123[1]; + if (a !== b) { + if (!rs.hashContains(held, a)) { + _126 = !rs.hashContains(held, b); + break b8; + } + } + _126 = false; + } + rs.byteVecPush(out, _126 ? 1 : 0); continue; } if (_45 === 1) { @@ -2932,11 +2960,11 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const address2: rs.usize = rs.addr(p2, 12, 4); rs.byteVecPush(out, rs.hashGet(held, address2) === keyed2.value.id ? 1 : 0); if (!rs.btreeDelete(ordered, p2)) { - _131 = false; + _162 = false; } else { - _131 = rs.hashDelete(hashed, p2); + _162 = rs.hashDelete(hashed, p2); } - rs.byteVecPush(out, _131 ? 1 : 0); + rs.byteVecPush(out, _162 ? 1 : 0); keyed2[Symbol.dispose](); if (!((byte & 4) !== 0)) { continue; @@ -2951,18 +2979,18 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const iterS: [rs.Weak, rs.usize][] = weaks.items; let iterI: number = 0; while (true) { - const _150: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; - if (_150 === rs.DONE) { + const _181: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; + if (_181 === rs.DONE) { continue l1; } - const at: rs.usize = rs.addr(rs.weakAsPtr(_150[0]), 12, 4); - const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_150[0]), (keyed3: rs.Arc): boolean => { + const at: rs.usize = rs.addr(rs.weakAsPtr(_181[0]), 12, 4); + const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_181[0]), (keyed3: rs.Arc): boolean => { const arc = keyed3; const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4) === at; keyed3[Symbol.dispose](); return _02; }); - rs.byteVecPush(out, rs.u8Add(at === _150[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); + rs.byteVecPush(out, rs.u8Add(at === _181[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); rs.byteVecPush(out, rs.hashContains(held, at) ? 1 : 0); } } @@ -2970,29 +2998,29 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { return rs.addr(_22, 12, 4); }), undefined, 4, true); if (!rs.sliceIsSorted(rs.sliceFrom(addresses.items), rs.le)) { - _181 = false; + _212 = false; } else { - _181 = addresses.items.length === live.items.length; + _212 = addresses.items.length === live.items.length; } - rs.byteVecPush(out, _181 ? 1 : 0); + rs.byteVecPush(out, _212 ? 1 : 0); const iter3: rs.Iter<[rs.Arc, rs.Arc]> = rs.iterZip(rs.iterSlice(live.items), rs.iterSkip(rs.iterSlice(live.items), 1), [false, false]); while (true) { - const _199: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); - if (_199 === rs.DONE) { + const _230: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); + if (_230 === rs.DONE) { break; } - const arc4 = _199[0]; - const _207: Keyed.Keyed = rs.arcRaw(arc4, arc4.value, "Arc"); - const arc5 = _199[1]; - b11: { - const _206: [Keyed.Keyed, Keyed.Keyed] = [_207, rs.arcRaw(arc5, arc5.value, "Arc")]; - const p3: Keyed.Keyed = _207; - const q: Keyed.Keyed = _206[1]; + const arc5 = _230[0]; + const _238: Keyed.Keyed = rs.arcRaw(arc5, arc5.value, "Arc"); + const arc6 = _230[1]; + b13: { + const _237: [Keyed.Keyed, Keyed.Keyed] = [_238, rs.arcRaw(arc6, arc6.value, "Arc")]; + const p3: Keyed.Keyed = _238; + const q: Keyed.Keyed = _237[1]; if (rs.addr(p3, 12, 4) < rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) < rs.addr(q, 12, 4)) { if (rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4)) { if (rs.cmpAddr(12, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4), rs.addr(q, 12, 4))) { agree = p3 !== q; - break b11; + break b13; } } } @@ -3003,37 +3031,37 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const registry2: rs.RefCell> = rs.refCellBorrow(registry.value); let iterStart: rs.u8 = 0; const iterEnd: rs.u8 = 4; - while ((_240 = iterStart++) < iterEnd) { - const group: rs.u8 = _240; + while ((_271 = iterStart++) < iterEnd) { + const group: rs.u8 = _271; rs.byteVecPush(out, rs.iterCount(rs.btreeRange(registry2.value, new rs.Range([group, 0] as [rs.u8, rs.usize], [rs.u8Add(group, 1), 0] as [rs.u8, rs.usize]))) & 0xff); } - rs.byteVecPush(out, rs.iterAll(rs.btreeValues(registry2.value), (id: rs.u8): boolean => { + rs.byteVecPush(out, rs.iterAll(rs.btreeValues(registry2.value), (id2: rs.u8): boolean => { return rs.iterAny(rs.hashValues(held), (h: rs.u8): boolean => { - return h === id; + return h === id2; }); }) ? 1 : 0); rs.refRelease(registry2); } - const small: rs.Arc[] = [new rs.Arc({ _0: 1, _1: 2 }), new rs.Arc({ _0: 3, _1: 4 })]; - for (const _272 of small) { - const arc6 = _272; - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(rs.addr(rs.arcRaw(arc6, arc6.value, "Arc"), 2, 1), 4) === 0 ? 1 : 0, rs.addr(rs.DANGLING, 2, 1) === 1 ? 1 : 0, _272.value._0 ^ _272.value._1)), undefined, true); - } - const arc7 = small[0]; - const _299: Small.Small = rs.arcRaw(arc7, arc7.value, "Arc"); - const arc8 = small[1]; - rs.byteVecPush(out, _299 !== rs.arcRaw(arc8, arc8.value, "Arc") ? 1 : 0); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_221: [rs.Weak, rs.usize]): boolean => { - const _55: rs.Arc | undefined = rs.weakUpgrade(_221[0]); - const _07: boolean = _55 !== undefined; - rs.drop(_55); - return _07; + const small: rs.Arc[] = [rs.arcAt(new rs.Arc({ _0: 1, _1: 2 }), 2, 1), rs.arcAt(new rs.Arc({ _0: 3, _1: 4 }), 2, 1)]; + for (const _303 of small) { + const arc7 = _303; + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(rs.addr(rs.arcRaw(arc7, arc7.value, "Arc"), 2, 1), 4) === 0 ? 1 : 0, rs.addr(rs.DANGLING, 2, 1) === 1 ? 1 : 0, _303.value._0 ^ _303.value._1)), undefined, true); + } + const arc8 = small[0]; + const _330: Small.Small = rs.arcRaw(arc8, arc8.value, "Arc"); + const arc9 = small[1]; + rs.byteVecPush(out, _330 !== rs.arcRaw(arc9, arc9.value, "Arc") ? 1 : 0); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_210: [rs.Weak, rs.usize]): boolean => { + const _56: rs.Arc | undefined = rs.weakUpgrade(_210[0]); + const _010: boolean = _56 !== undefined; + rs.drop(_56); + return _010; })) & 0xff)), undefined, true); rs.dropVec(live); - const _322: rs.u8 = left.value.v; - const _329: rs.RefCell> = rs.refCellBorrow(registry.value); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_322, rs.btreeLen(_329.value) === 0 ? 1 : 0)), undefined, true); - rs.refRelease(_329); + const _353: rs.u8 = left.value.v; + const _360: rs.RefCell> = rs.refCellBorrow(registry.value); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_353, rs.btreeLen(_360.value) === 0 ? 1 : 0)), undefined, true); + rs.refRelease(_360); rs.dropAll(small); rs.dropVec(weaks, drop$tuple2_sync_Weak_pointers_Keyed_usize); left[Symbol.dispose](); diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index e2cdac5ab..0fe0f42d0 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2341,6 +2341,22 @@ pub fn addresses(data: &[u8], out: &mut Vec) { out.push(u8::from(ordered.insert(p) && hashed.insert(p))); weaks.push((Arc::downgrade(&keyed), address)); live.push(keyed); + // Two made and freed together, read only after: apart from each other and + // from every allocation held while they lived. + if byte & 4 != 0 { + let make = |id: u8| { + Arc::new(Keyed { + id, + registry: Rc::clone(®istry), + left: Rc::clone(&left), + }) + }; + let pair = [make(i as u8), make(i as u8)]; + let pointers = pair.each_ref().map(Arc::as_ptr); + drop(pair); + let [a, b] = pointers.map(|p| p as usize); + out.push(u8::from(a != b && !held.contains_key(&a) && !held.contains_key(&b))); + } } 1 if !live.is_empty() => { let keyed = live.remove(usize::from(byte / 4) % live.len()); diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index 226f065c0..de2dd0dff 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -313,51 +313,53 @@ function nowhere(): never { throw new Error("rust2-ts: a read or write through a pointer to no memory"); } -/** The address `addr` gave each `Arc`'s or `Rc`'s value, or `UNREAD` once freed before any. */ +/** The address of each `Arc`'s or `Rc`'s value that `addrNew` gave one. */ const ADDRESSED = /* @__PURE__ */ new WeakMap(); -/** What `ADDRESSED` holds for a value whose allocation was freed before its address was read. */ -const UNREAD = -1; - -/** The block each address `addr` gave is in, by its size and alignment, and where it starts, while its allocation lives. */ +/** The block each address `addrNew` gave is in, by its size and alignment, and where it starts, while its allocation lives. */ const BLOCKS = /* @__PURE__ */ new Map(); -/** The addresses of freed allocations, by the size and alignment of their blocks. */ +/** The starts of freed blocks, by their size and alignment. */ const FREED = /* @__PURE__ */ new Map(); /** Where the next new block starts: past wasm32's first page, as a heap is. */ let brk = 0x10000; -/** Whether `addr` has given an address, after which freeing an allocation frees its own. */ +/** Whether `addrNew` has given an address, after which freeing an allocation frees its own. */ let addressing = false; /** * `p as usize`, of a pointer to a value of `size` bytes aligned to `align`: null's 0, a - * dangling pointer's `align`, an address no memory is at, or else that of the value's - * `Arc` or `Rc` allocation, which `unaddress` frees, as an allocator places it: after its - * two counts, in a block no live allocation's overlaps, reusing a freed one. Running out - * of wasm32's addresses aborts, as a failed allocation does. + * dangling pointer's `align`, an address no memory is at, or else that of the `Arc`'s or + * `Rc`'s value it points at, which the allocation keeps once freed, as a pointer to it + * does in Rust. */ export function addr(p: object, size: number, align: number): number { if (p === NULL) return 0; if (p === DANGLING) return align; if (p instanceof Address) return p.addr; - const at = ADDRESSED.get(p); - if (at !== undefined && at !== UNREAD) return at; + return ADDRESSED.get(p) ?? addrNew(p, size, align); +} + +/** + * Give `value`, a new `Arc`'s or `Rc`'s of `size` bytes aligned to `align`, its address, + * as an allocator places the allocation: after its two counts, at a multiple of `align`, + * in a block no live allocation's overlaps, reusing a freed one, which `unaddress` frees. + * Running out of wasm32's addresses aborts, as a failed allocation does. + */ +export function addrNew(value: object, size: number, align: number): number { addressing = true; - // The two `usize` counts, then the value at a multiple of its alignment. const head = Math.ceil(8 / align) * align; - const block = `${head + size}:${Math.max(align, 4)}`; + const whole = Math.max(align, 4); + const block = `${head + size}:${whole}`; let start = FREED.get(block)?.pop(); if (start === undefined) { - start = Math.ceil(brk / Math.max(align, 4)) * Math.max(align, 4); + start = Math.ceil(brk / whole) * whole; if (start + head + size > 2 ** 32) throw new Abort(`memory allocation of ${size} bytes failed`); brk = start + head + size; } - ADDRESSED.set(p, start + head); + ADDRESSED.set(value, start + head); BLOCKS.set(start + head, [block, start]); - // A pointer to a freed allocation keeps the address it had, which another may reuse. - if (at === UNREAD) unaddress(p); return start + head; } @@ -365,10 +367,9 @@ export function addr(p: object, size: number, align: number): number { export function unaddress(value: unknown): void { if (!addressing || (typeof value !== "object" && typeof value !== "function") || value === null) return; const at = ADDRESSED.get(value); - if (at === undefined) return void ADDRESSED.set(value, UNREAD); - const held = BLOCKS.get(at); + const held = at === undefined ? undefined : BLOCKS.get(at); if (held === undefined) return; - BLOCKS.delete(at); + BLOCKS.delete(at as number); const [block, start] = held; let free = FREED.get(block); if (free === undefined) FREED.set(block, (free = [])); diff --git a/ts/runtime/rs/task.ts b/ts/runtime/rs/task.ts index a5cbd25ed..4657afb9e 100644 --- a/ts/runtime/rs/task.ts +++ b/ts/runtime/rs/task.ts @@ -4,7 +4,7 @@ // A value whose drop JS can observe has `[Symbol.dispose]`, which runs its `Drop` impl // and drops its fields. `None` is `undefined`, which drops as nothing. -import { Abort, addressAt, arrayAssign, type ByteSlice, type CloneFrom, cloneOver, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtDebugTupleFields, fmtPad, fmtStr, type None, NULL, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrErase, ptrHome, ptrNowhere, ptrWrite, type Result, Slice, stringAsBytes, unaddress, type Vec } from "../rs.ts"; +import { Abort, addressAt, addrNew, arrayAssign, type ByteSlice, type CloneFrom, cloneOver, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtDebugTupleFields, fmtPad, fmtStr, type None, NULL, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrErase, ptrHome, ptrNowhere, ptrWrite, type Result, Slice, stringAsBytes, unaddress, type Vec } from "../rs.ts"; import type { Hasher } from "./hash.ts"; import type { Location } from "./location.ts"; @@ -133,6 +133,12 @@ export class Arc implements Disposable { } } +/** `arc`, a new `Arc` or `Rc` of a value of `size` bytes aligned to `align`, given its address as it is made. */ +export function arcAt(arc: Arc, size: number, align: number): Arc { + addrNew(arc.value as object, size, align); + return arc; +} + /** * Release the weak count `arc` holds while its last strong one drops its value, as std's * strong `Arc`s share one, freeing the allocation if no `Weak` is left. diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index ba517b764..c3eed43e0 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -47,12 +47,30 @@ pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Resul } let read = b.t.sited(span, what); b.t.addresses.push((ty, read.0, read.1)); + let stored = super::copy::storage(b.t.tcx, ty); + b.t.addressing.insert(stored); } + Ok(layout(b, ty)) +} + +/// The size and alignment of a `ty`, as `rs.addr` takes them. +fn layout<'tcx>(b: &Body<'_, '_, 'tcx>, ty: Ty<'tcx>) -> [Expr; 2] { let (tcx, env) = (b.t.tcx, b.t.typing_env_pub()); let layout = tcx.layout_of(env.as_query_input(ty)).ok(); let size = layout.map_or(0, |l| l.size.bytes()); let align = layout.map_or(1, |l| l.align.abi.bytes()); - Ok([size, align].map(|n| Expr::lit(n.to_string()))) + [size, align].map(|n| Expr::lit(n.to_string())) +} + +/// `arc`, a new `Arc` or `Rc` of a `value`, given its address as it is made where the +/// runs before read the address of one, as an allocator places an allocation, so +/// allocations whose lives overlap have addresses apart even where a pointer is read only +/// once its allocation is freed. +pub fn numbered<'tcx>(b: &Body<'_, '_, 'tcx>, arc: Expr, value: Ty<'tcx>) -> Expr { + match b.t.addressed.contains(&super::copy::storage(b.t.tcx, value)) { + true => Expr::rs("arcAt", [arc].into_iter().chain(layout(b, value)).collect()), + false => arc, + } } impl<'a, 'tcx> Translator<'a, 'tcx> { diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index a6f9a1f03..425b9146a 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -1665,7 +1665,8 @@ fn mapping<'tcx>( || args.type_at(0).is_sized(tcx, b.t.typing_env())) && !super::byte_array(args.type_at(0)) { - return Some(Expr::New(Box::new(Expr::Rs("Arc".into())), vec![value])); + let arc = Expr::New(Box::new(Expr::Rs("Arc".into())), vec![value]); + return Some(super::address::numbered(b, arc, args.type_at(0))); } // An error, boxed as a `dyn Error`, which `?` converts it by. One of a string boxes // std's own private error type. @@ -4142,7 +4143,10 @@ pub fn default_value<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span, } } Std::Box | Std::Wrapping | Std::Saturating => part(b, args.type_at(0), out), - Std::Arc => Expr::New(Box::new(Expr::Rs("Arc".into())), vec![part(b, args.type_at(0), out)]), + Std::Arc => { + let arc = Expr::New(Box::new(Expr::Rs("Arc".into())), vec![part(b, args.type_at(0), out)]); + super::address::numbered(b, arc, args.type_at(0)) + } Std::Mutex => Expr::New(Box::new(Expr::Rs("Mutex".into())), vec![part(b, args.type_at(0), out)]), Std::RefCell => Expr::New( Box::new(Expr::Rs("RefCell".into())), diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index 9813b671c..564ca456b 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -267,6 +267,11 @@ pub struct Translator<'a, 'tcx> { compared: rustc_data_structures::fx::FxHashSet>, /// Those this run compared. comparing: rustc_data_structures::fx::FxHashSet>, + /// The types whose addresses the runs before read, by their storage, whose `Arc`s are + /// given their addresses as they are made. + addressed: rustc_data_structures::fx::FxHashSet>, + /// Those this run read. + addressing: rustc_data_structures::fx::FxHashSet>, /// Whether the runs before reached a `panic::set_hook`, so every panic carries the /// `rs.Location` its hook reads. pub hooked: bool, @@ -422,6 +427,21 @@ impl Erased { } } +/// What the runs before found, which the next judges by. +#[derive(Default)] +struct Before<'tcx> { + /// The instances whose bodies' writes they read. + read: rustc_data_structures::fx::FxHashSet>, + /// The closures cloned where they can't share. + cloned: rustc_data_structures::fx::FxHashSet, + /// The types whose pointers they compared. + compared: rustc_data_structures::fx::FxHashSet>, + /// The types whose addresses they read. + addressed: rustc_data_structures::fx::FxHashSet>, + /// Whether they reached a `panic::set_hook`. + hooked: bool, +} + impl<'a, 'tcx> Translator<'a, 'tcx> { /// A translator that has run `reach`, with what it copies and refuses settled. Whether /// code writes into a type in place decides that, and of another crate's code, only what @@ -430,7 +450,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// writes it didn't read, or an instance of one writing a type parameter that it didn't /// judge by. A closure's clone may come before or after the closure is made, which /// makes it as its clones need, so it runs again too when it reaches a new one, and a - /// comparison of pointers may come before or after the borrows and copies it decides. + /// comparison of pointers may come before or after the borrows and copies it decides, + /// and an address read before or after the `Arc`s it numbers as they are made. /// Once it reaches a `panic::set_hook`, it runs again with every panic located. fn settle( tcx: TyCtxt<'tcx>, @@ -438,12 +459,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { census: bool, mut reach: impl FnMut(&mut Self) -> R, ) -> Option<(Self, R)> { - let mut read = rustc_data_structures::fx::FxHashSet::default(); - let mut cloned = rustc_data_structures::fx::FxHashSet::default(); - let mut compared = rustc_data_structures::fx::FxHashSet::default(); - let mut hooked = false; + let mut before = Before::default(); loop { - let mut t = Self::new(tcx, config, census, read, cloned, compared, hooked)?; + let mut t = Self::new(tcx, config, census, before)?; t.bind(); let out = reach(&mut t); let reached: Vec> = t @@ -456,19 +474,29 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { if reached.is_empty() && t.cloning.is_subset(&t.cloned) && t.comparing.is_subset(&t.compared) + && t.addressing.is_subset(&t.addressed) && (t.hooked || !t.hooking) { t.refuse_capacity_reads(); t.refuse_addresses(); return Some((t, out)); } - read = std::mem::take(&mut t.read); - read.extend(reached); - cloned = std::mem::take(&mut t.cloned); - cloned.extend(t.cloning.drain()); - compared = std::mem::take(&mut t.compared); - compared.extend(t.comparing.drain()); - hooked |= t.hooking; + before = Before { + read: std::mem::take(&mut t.read).into_iter().chain(reached).collect(), + cloned: std::mem::take(&mut t.cloned) + .into_iter() + .chain(t.cloning.drain()) + .collect(), + compared: std::mem::take(&mut t.compared) + .into_iter() + .chain(t.comparing.drain()) + .collect(), + addressed: std::mem::take(&mut t.addressed) + .into_iter() + .chain(t.addressing.drain()) + .collect(), + hooked: t.hooked || t.hooking, + }; } } @@ -488,15 +516,14 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { self.cloned.contains(&def_id) } - fn new( - tcx: TyCtxt<'tcx>, - config: &'a Config, - census: bool, - read: rustc_data_structures::fx::FxHashSet>, - cloned: rustc_data_structures::fx::FxHashSet, - compared: rustc_data_structures::fx::FxHashSet>, - hooked: bool, - ) -> Option { + fn new(tcx: TyCtxt<'tcx>, config: &'a Config, census: bool, before: Before<'tcx>) -> Option { + let Before { + read, + cloned, + compared, + addressed, + hooked, + } = before; if tcx.data_layout.pointer_size().bits() != 32 { tcx.dcx().err( "rust2-ts: translation needs a 32-bit target, such as wasm32-unknown-unknown, so usize is a number", @@ -547,6 +574,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { cloning: Default::default(), compared, comparing: Default::default(), + addressed, + addressing: Default::default(), hooked, hooking: false, held: false, diff --git a/ts/src/lower/task.rs b/ts/src/lower/task.rs index 2ee75e14f..2612c818f 100644 --- a/ts/src/lower/task.rs +++ b/ts/src/lower/task.rs @@ -137,13 +137,15 @@ pub fn call<'tcx>( // A clone of a reference is that reference, not a clone of what it points at. let owned_std = self_std.filter(|_| self_ty.is_some_and(|t| !t.is_ref())); let e = match (owner, name) { - ("Arc" | "Rc", "new") => Expr::New(Box::new(Expr::Rs("Arc".into())), args), + // JS erases the pin. + ("Arc" | "Rc", "new" | "pin") => { + let arc = Expr::New(Box::new(Expr::Rs("Arc".into())), args); + super::address::numbered(b, arc, call.args.type_at(0)) + } ("Arc" | "Rc", "new_cyclic") => { b.t.cyclic(call.args.type_at(0)); - Expr::rs("arcNewCyclic", args) + super::address::numbered(b, Expr::rs("arcNewCyclic", args), call.args.type_at(0)) } - // JS erases the pin. - ("Arc" | "Rc", "pin") => Expr::New(Box::new(Expr::Rs("Arc".into())), args), // The object of one of a trait object, which every pointer to it shares. A `dyn Any`'s // may hold a copy of a value JS copies, which `Arc::downcast` would not see written. ("Arc" | "Rc", "get_mut") diff --git a/ts/tests/pointers.ts b/ts/tests/pointers.ts index 1a0d137d0..530de814d 100644 --- a/ts/tests/pointers.ts +++ b/ts/tests/pointers.ts @@ -132,31 +132,39 @@ assert.equal(rs.arcErase(rs.ptrAdd(rs.ptrAdd(flagAt, 1), -1), "bool", 1), flagAt assert.equal(rs.arcErase(rs.NULL, "bool", 1), rs.NULL); assert.throws(() => rs.arcErase({ v: false }, "bool", 1), /only an `Arc`'s or `Rc`'s raw pointer keeps the place of/); -// An `Arc`'s value's address, freed with the allocation once its strong and weak counts -// reach 0, which the next of its size reuses, as an allocator does, so churning far past -// wasm32's 4 GiB never runs out. One a `Weak` holds stays apart from every other. +// An `Arc`'s value's address, given as it is made and freed with the allocation once its +// strong and weak counts reach 0, which the next of its size reuses, as an allocator does, +// so churning far past wasm32's 4 GiB never runs out. One a `Weak` holds stays apart from +// every other. const MIB = 2 ** 20; -const churned = new rs.Arc({ n: 0 }); +const made = (n: number) => rs.arcAt(new rs.Arc({ n }), MIB, 8); +const churned = made(0); const churnedAt = rs.addr(churned.value, MIB, 8); churned[Symbol.dispose](); for (let n = 1; n <= 10_000; n++) { - const arc = new rs.Arc({ n }); + const arc = made(n); assert.equal(rs.addr(arc.value, MIB, 8), churnedAt); arc[Symbol.dispose](); } -const weakly = new rs.Arc({ n: 0 }); +const weakly = made(0); const weak = new rs.Weak(weakly); const weaklyAt = rs.addr(weakly.value, MIB, 8); weakly[Symbol.dispose](); -const beside = new rs.Arc({ n: 1 }); +const beside = made(1); assert.notEqual(rs.addr(beside.value, MIB, 8), weaklyAt); assert.equal(rs.addr(rs.weakAsPtr(weak), MIB, 8), weaklyAt); weak[Symbol.dispose](); -assert.equal(rs.addr(new rs.Arc({ n: 2 }).value, MIB, 8), weaklyAt); -// A value whose allocation was freed before its address was read: a pointer to it has -// one, which the next allocation may reuse. -const unread = new rs.Arc({ n: 3 }); -unread[Symbol.dispose](); -const unreadAt = rs.addr(unread.value, MIB, 8); -assert.equal(rs.addr(new rs.Arc({ n: 4 }).value, MIB, 8), unreadAt); -assert.equal(rs.addr(unread.value, MIB, 8), unreadAt); +assert.equal(rs.addr(made(2).value, MIB, 8), weaklyAt); +// Pointers read only once their allocations are freed keep the addresses they had: apart +// where the two lived at once, and never holding a block, so 10,000 more run out of nothing. +const [first, second] = [made(3), made(4)]; +first[Symbol.dispose](); +second[Symbol.dispose](); +assert.notEqual(rs.addr(first.value, MIB, 8), rs.addr(second.value, MIB, 8)); +const freed = []; +for (let n = 0; n < 10_000; n++) { + const arc = made(n); + arc[Symbol.dispose](); + freed.push(arc.value); +} +for (const value of freed) rs.addr(value, MIB, 8); From 8890f47fd468c9c15013293ece263c45529ed151 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:35:51 -0800 Subject: [PATCH 13/13] wip(ts): sameAddr for addressed pointers, checkpoint of the interrupted iterate session Co-Authored-By: Claude Opus 5.5 --- .../lang/pointers/expected/pointers.ts | 177 ++++++++++-------- ts/fixtures/lang/pointers/src/pointers.rs | 14 ++ ts/runtime/rs.ts | 5 + ts/src/lower/address.rs | 8 +- ts/src/lower/pointer.rs | 6 +- 5 files changed, 127 insertions(+), 83 deletions(-) diff --git a/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index a7cf26d34..ef05abfa7 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -2863,13 +2863,14 @@ export function rawPlaces(data: rs.ByteSlice, out: rs.ByteVec): void { * the order's agreement, never an address itself. */ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { - let _59!: boolean; - let _92!: boolean; - let _126!: boolean; - let _162!: boolean; - let _212!: boolean; + let _60!: boolean; + let _93!: boolean; + let _110!: boolean; + let _153!: boolean; + let _189!: boolean; + let _241!: boolean; let agree!: boolean; - let _271!: rs.u8; + let _300!: rs.u8; const registry: rs.Arc>> = new rs.Arc(new rs.RefCell(rs.btreeNew(rs.cmpTuple<[rs.u8, rs.usize], rs.Ordering>([rs.cmp, rs.cmp])) as rs.BTreeMap<[rs.u8, rs.usize], rs.u8>)); const left: rs.Arc> = new rs.Arc(new rs.Cell(0)); const align: rs.usize = 4; @@ -2877,24 +2878,25 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const live: rs.Vec> = new rs.Vec(); const weaks: rs.Vec<[rs.Weak, rs.usize]> = new rs.Vec(); const ordered: rs.BTreeSet = rs.btreeNew(rs.cmpAddr(12, 4)) as rs.BTreeSet; - const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4), rs.same)) as rs.HashSet; - const _16: rs.Weak = new rs.Weak(); - const dangling: Keyed.Keyed = rs.weakAsPtr(_16); - _16[Symbol.dispose](); + const hashed: rs.HashSet = rs.hashNew(rs.hashing(rs.hashAddr(12, 4), rs.sameAddr(12, 4))) as rs.HashSet; + const freed: rs.Vec = new rs.Vec(); + const _17: rs.Weak = new rs.Weak(); + const dangling: Keyed.Keyed = rs.weakAsPtr(_17); + _17[Symbol.dispose](); rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.addr(dangling, 12, 4) & 0xff, ((rs.addr(dangling, 12, 4) << 16) >> 16) & 0xff)), undefined, true); rs.iterExtendBytes(out, rs.iterSlice(rs.bytesMap([(rs.addr(dangling, 12, 4) | 0) === -1, rs.addr(dangling, 12, 4) === 4294967295, rs.addr(rs.NULL, 12, 4) === 0, rs.addr(rs.DANGLING, 12, 4) === align], (a0: boolean): rs.u8 => { return a0 ? 1 : 0; })), undefined, true); const iter: rs.Iter<[rs.usize, rs.u8]> = rs.iterEnumerate(rs.iterSlice(rs.byteSliceView(data))); l1: while (true) { - const _40: [rs.usize, rs.u8] | rs.Done = iter.next(); - if (_40 === rs.DONE) { + const _41: [rs.usize, rs.u8] | rs.Done = iter.next(); + if (_41 === rs.DONE) { break; } - const i: rs.usize = _40[0]; - const byte: rs.u8 = _40[1]; - const _45: rs.u8 = rs.u8Rem(byte, 4); - if (_45 === 0) { + const i: rs.usize = _41[0]; + const byte: rs.u8 = _41[1]; + const _46: rs.u8 = rs.u8Rem(byte, 4); + if (_46 === 0) { const keyed: rs.Arc = rs.arcAt(new rs.Arc(new Keyed.Keyed(i & 0xff, rs.arcClone(registry), rs.arcClone(left))), 12, 4); const arc3 = keyed; const p: Keyed.Keyed = rs.arcRaw(arc3, arc3.value, "Arc"); @@ -2902,26 +2904,43 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { b5: { if (rs.usizeRem(address, align) === 0) { if (address !== 0) { - _59 = rs.addr(p, 12, 4) === address; + _60 = rs.addr(p, 12, 4) === address; break b5; } } - _59 = false; + _60 = false; } - rs.byteVecPush(out, _59 ? 1 : 0); + rs.byteVecPush(out, _60 ? 1 : 0); rs.byteVecPush(out, rs.hashInsert(held, address, keyed.value.id) === undefined ? 1 : 0); - const _77: rs.RefCell> = rs.refCellBorrowMut(registry.value); - const _80: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; - rs.btreeInsert(_77.value, _80, keyed.value.id); - rs.refMutRelease(_77); + const _78: rs.RefCell> = rs.refCellBorrowMut(registry.value); + const _81: [rs.u8, rs.usize] = [rs.u8Rem(keyed.value.id, 4), rs.addr(p, 12, 4)]; + rs.btreeInsert(_78.value, _81, keyed.value.id); + rs.refMutRelease(_78); if (!rs.btreeAdd(ordered, p)) { - _92 = false; + _93 = false; } else { - _92 = rs.hashAdd(hashed, p); + _93 = rs.hashAdd(hashed, p); } - rs.byteVecPush(out, _92 ? 1 : 0); - const _98: [rs.Weak, rs.usize] = [new rs.Weak(keyed), address]; - weaks.items.push(_98); + rs.byteVecPush(out, _93 ? 1 : 0); + for (const f of freed.items) { + const reused: boolean = rs.addr(f, 12, 4) === address; + const _107: Keyed.Keyed[] = [f]; + b10: { + const keys: rs.HashSet = rs.hashExtendKeys(rs.hashNew(rs.hashing(rs.hashAddr(12, 4), rs.sameAddr(12, 4))) as rs.HashSet, rs.iterSlice(_107), undefined, undefined, 234881024); + if (rs.sameAddr(12, 4)(f, p) === reused) { + if (rs.sameAddr(12, 4)(f, p) === reused) { + if (rs.hashContains(keys, p) === reused) { + _110 = rs.cmpAddr(12, 4)(f, p) === 0 === reused; + break b10; + } + } + } + _110 = false; + } + rs.byteVecPush(out, _110 ? 1 : 0); + } + const _125: [rs.Weak, rs.usize] = [new rs.Weak(keyed), address]; + weaks.items.push(_125); live.items.push(keyed); if (!((byte & 4) !== 0)) { continue; @@ -2935,24 +2954,24 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { return rs.arcRaw(arc4, arc4.value, "Arc"); }); rs.dropAll(pair); - const _123: rs.usize[] = rs.arrayMap(pointers, (p4: Keyed.Keyed): rs.usize => { + const _150: rs.usize[] = rs.arrayMap(pointers, (p4: Keyed.Keyed): rs.usize => { return rs.addr(p4, 12, 4); }); - b8: { - const a: rs.usize = _123[0]; - const b: rs.usize = _123[1]; + b12: { + const a: rs.usize = _150[0]; + const b: rs.usize = _150[1]; if (a !== b) { if (!rs.hashContains(held, a)) { - _126 = !rs.hashContains(held, b); - break b8; + _153 = !rs.hashContains(held, b); + break b12; } } - _126 = false; + _153 = false; } - rs.byteVecPush(out, _126 ? 1 : 0); + rs.byteVecPush(out, _153 ? 1 : 0); continue; } - if (_45 === 1) { + if (_46 === 1) { if (!(live.items.length === 0)) { const keyed2: rs.Arc = rs.vecRemove(live, rs.usizeRem(rs.u8Div(byte, 4) as rs.usize, live.items.length)); const arc2 = keyed2; @@ -2960,11 +2979,11 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const address2: rs.usize = rs.addr(p2, 12, 4); rs.byteVecPush(out, rs.hashGet(held, address2) === keyed2.value.id ? 1 : 0); if (!rs.btreeDelete(ordered, p2)) { - _162 = false; + _189 = false; } else { - _162 = rs.hashDelete(hashed, p2); + _189 = rs.hashDelete(hashed, p2); } - rs.byteVecPush(out, _162 ? 1 : 0); + rs.byteVecPush(out, _189 ? 1 : 0); keyed2[Symbol.dispose](); if (!((byte & 4) !== 0)) { continue; @@ -2973,54 +2992,55 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { return _2[1] !== address2; }, drop$tuple2_sync_Weak_pointers_Keyed_usize); rs.hashRemove(held, address2); + freed.items.push(p2); continue; } - } else if (_45 === 2) { - const iterS: [rs.Weak, rs.usize][] = weaks.items; - let iterI: number = 0; + } else if (_46 === 2) { + const iterS2: [rs.Weak, rs.usize][] = weaks.items; + let iterI2: number = 0; while (true) { - const _181: [rs.Weak, rs.usize] | rs.Done = iterI < iterS.length ? iterS[iterI++] : rs.DONE; - if (_181 === rs.DONE) { + const _210: [rs.Weak, rs.usize] | rs.Done = iterI2 < iterS2.length ? iterS2[iterI2++] : rs.DONE; + if (_210 === rs.DONE) { continue l1; } - const at: rs.usize = rs.addr(rs.weakAsPtr(_181[0]), 12, 4); - const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_181[0]), (keyed3: rs.Arc): boolean => { + const at: rs.usize = rs.addr(rs.weakAsPtr(_210[0]), 12, 4); + const alive: boolean = isSomeAnd$0fp5rqa(rs.weakUpgrade(_210[0]), (keyed3: rs.Arc): boolean => { const arc = keyed3; const _02: boolean = rs.addr(rs.arcRaw(arc, arc.value, "Arc"), 12, 4) === at; keyed3[Symbol.dispose](); return _02; }); - rs.byteVecPush(out, rs.u8Add(at === _181[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); + rs.byteVecPush(out, rs.u8Add(at === _210[1] ? 1 : 0, rs.u8Mul(2, alive ? 1 : 0))); rs.byteVecPush(out, rs.hashContains(held, at) ? 1 : 0); } } - const addresses: rs.Vec = rs.iterToVec(rs.iterMap(rs.btreeKeys(ordered), (_22: Keyed.Keyed): rs.usize => { - return rs.addr(_22, 12, 4); + const addresses: rs.Vec = rs.iterToVec(rs.iterMap(rs.btreeKeys(ordered), (_23: Keyed.Keyed): rs.usize => { + return rs.addr(_23, 12, 4); }), undefined, 4, true); if (!rs.sliceIsSorted(rs.sliceFrom(addresses.items), rs.le)) { - _212 = false; + _241 = false; } else { - _212 = addresses.items.length === live.items.length; + _241 = addresses.items.length === live.items.length; } - rs.byteVecPush(out, _212 ? 1 : 0); - const iter3: rs.Iter<[rs.Arc, rs.Arc]> = rs.iterZip(rs.iterSlice(live.items), rs.iterSkip(rs.iterSlice(live.items), 1), [false, false]); + rs.byteVecPush(out, _241 ? 1 : 0); + const iter4: rs.Iter<[rs.Arc, rs.Arc]> = rs.iterZip(rs.iterSlice(live.items), rs.iterSkip(rs.iterSlice(live.items), 1), [false, false]); while (true) { - const _230: [rs.Arc, rs.Arc] | rs.Done = iter3.next(); - if (_230 === rs.DONE) { + const _259: [rs.Arc, rs.Arc] | rs.Done = iter4.next(); + if (_259 === rs.DONE) { break; } - const arc5 = _230[0]; - const _238: Keyed.Keyed = rs.arcRaw(arc5, arc5.value, "Arc"); - const arc6 = _230[1]; - b13: { - const _237: [Keyed.Keyed, Keyed.Keyed] = [_238, rs.arcRaw(arc6, arc6.value, "Arc")]; - const p3: Keyed.Keyed = _238; - const q: Keyed.Keyed = _237[1]; + const arc5 = _259[0]; + const _267: Keyed.Keyed = rs.arcRaw(arc5, arc5.value, "Arc"); + const arc6 = _259[1]; + b17: { + const _266: [Keyed.Keyed, Keyed.Keyed] = [_267, rs.arcRaw(arc6, arc6.value, "Arc")]; + const p3: Keyed.Keyed = _267; + const q: Keyed.Keyed = _266[1]; if (rs.addr(p3, 12, 4) < rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) < rs.addr(q, 12, 4)) { if (rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4) === rs.addr(p3, 12, 4) >= rs.addr(q, 12, 4)) { if (rs.cmpAddr(12, 4)(p3, q) === rs.cmp(rs.addr(p3, 12, 4), rs.addr(q, 12, 4))) { - agree = p3 !== q; - break b13; + agree = !rs.sameAddr(12, 4)(p3, q); + break b17; } } } @@ -3031,8 +3051,8 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { const registry2: rs.RefCell> = rs.refCellBorrow(registry.value); let iterStart: rs.u8 = 0; const iterEnd: rs.u8 = 4; - while ((_271 = iterStart++) < iterEnd) { - const group: rs.u8 = _271; + while ((_300 = iterStart++) < iterEnd) { + const group: rs.u8 = _300; rs.byteVecPush(out, rs.iterCount(rs.btreeRange(registry2.value, new rs.Range([group, 0] as [rs.u8, rs.usize], [rs.u8Add(group, 1), 0] as [rs.u8, rs.usize]))) & 0xff); } rs.byteVecPush(out, rs.iterAll(rs.btreeValues(registry2.value), (id2: rs.u8): boolean => { @@ -3043,25 +3063,26 @@ export function addresses(data: rs.ByteSlice, out: rs.ByteVec): void { rs.refRelease(registry2); } const small: rs.Arc[] = [rs.arcAt(new rs.Arc({ _0: 1, _1: 2 }), 2, 1), rs.arcAt(new rs.Arc({ _0: 3, _1: 4 }), 2, 1)]; - for (const _303 of small) { - const arc7 = _303; - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(rs.addr(rs.arcRaw(arc7, arc7.value, "Arc"), 2, 1), 4) === 0 ? 1 : 0, rs.addr(rs.DANGLING, 2, 1) === 1 ? 1 : 0, _303.value._0 ^ _303.value._1)), undefined, true); + for (const _332 of small) { + const arc7 = _332; + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(rs.usizeRem(rs.addr(rs.arcRaw(arc7, arc7.value, "Arc"), 2, 1), 4) === 0 ? 1 : 0, rs.addr(rs.DANGLING, 2, 1) === 1 ? 1 : 0, _332.value._0 ^ _332.value._1)), undefined, true); } const arc8 = small[0]; - const _330: Small.Small = rs.arcRaw(arc8, arc8.value, "Arc"); + const _359: Small.Small = rs.arcRaw(arc8, arc8.value, "Arc"); const arc9 = small[1]; - rs.byteVecPush(out, _330 !== rs.arcRaw(arc9, arc9.value, "Arc") ? 1 : 0); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_210: [rs.Weak, rs.usize]): boolean => { - const _56: rs.Arc | undefined = rs.weakUpgrade(_210[0]); + const _363: Small.Small = rs.arcRaw(arc9, arc9.value, "Arc"); + rs.byteVecPush(out, !rs.sameAddr(2, 1)(_359, _363) ? 1 : 0); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(left.value.v, rs.iterCount(rs.iterFilter(rs.iterSlice(weaks.items), (_211: [rs.Weak, rs.usize]): boolean => { + const _56: rs.Arc | undefined = rs.weakUpgrade(_211[0]); const _010: boolean = _56 !== undefined; rs.drop(_56); return _010; })) & 0xff)), undefined, true); rs.dropVec(live); - const _353: rs.u8 = left.value.v; - const _360: rs.RefCell> = rs.refCellBorrow(registry.value); - rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_353, rs.btreeLen(_360.value) === 0 ? 1 : 0)), undefined, true); - rs.refRelease(_360); + const _382: rs.u8 = left.value.v; + const _389: rs.RefCell> = rs.refCellBorrow(registry.value); + rs.iterExtendBytes(out, rs.iterSlice(Uint8Array.of(_382, rs.btreeLen(_389.value) === 0 ? 1 : 0)), undefined, true); + rs.refRelease(_389); rs.dropAll(small); rs.dropVec(weaks, drop$tuple2_sync_Weak_pointers_Keyed_usize); left[Symbol.dispose](); diff --git a/ts/fixtures/lang/pointers/src/pointers.rs b/ts/fixtures/lang/pointers/src/pointers.rs index 0fe0f42d0..a72eb0419 100644 --- a/ts/fixtures/lang/pointers/src/pointers.rs +++ b/ts/fixtures/lang/pointers/src/pointers.rs @@ -2311,6 +2311,7 @@ pub fn addresses(data: &[u8], out: &mut Vec) { let mut weaks: Vec<(Weak, usize)> = Vec::new(); let mut ordered: BTreeSet<*const Keyed> = BTreeSet::new(); let mut hashed: HashSet<*const Keyed> = HashSet::new(); + let mut freed: Vec<*const Keyed> = Vec::new(); let dangling = Weak::::new().as_ptr(); out.extend([dangling as u8, dangling as i16 as u8]); out.extend( @@ -2339,6 +2340,18 @@ pub fn addresses(data: &[u8], out: &mut Vec) { .borrow_mut() .insert((keyed.id % 4, p.expose_provenance()), keyed.id); out.push(u8::from(ordered.insert(p) && hashed.insert(p))); + // A pointer to a freed allocation equals one to an allocation reusing its + // address, and hashes and orders as it. + for &f in &freed { + let reused = f as usize == address; + let keys = HashSet::from([f]); + out.push(u8::from( + (f == p) == reused + && ptr::eq(f, p) == reused + && keys.contains(&p) == reused + && f.cmp(&p).is_eq() == reused, + )); + } weaks.push((Arc::downgrade(&keyed), address)); live.push(keyed); // Two made and freed together, read only after: apart from each other and @@ -2369,6 +2382,7 @@ pub fn addresses(data: &[u8], out: &mut Vec) { if byte & 4 != 0 { weaks.retain(|(_, at)| *at != address); held.remove(&address); + freed.push(p); } } // A `Weak` keeps its address once its value drops, as it keeps the allocation. diff --git a/ts/runtime/rs.ts b/ts/runtime/rs.ts index de2dd0dff..78b067175 100644 --- a/ts/runtime/rs.ts +++ b/ts/runtime/rs.ts @@ -341,6 +341,11 @@ export function addr(p: object, size: number, align: number): number { return ADDRESSED.get(p) ?? addrNew(p, size, align); } +/** `==` of raw pointers to values of `size` bytes aligned to `align`, which compares their addresses. */ +export function sameAddr(size: number, align: number): (a: object, b: object) => boolean { + return (a, b) => addr(a, size, align) === addr(b, size, align); +} + /** * Give `value`, a new `Arc`'s or `Rc`'s of `size` bytes aligned to `align`, its address, * as an allocator places the allocation: after its two counts, at a multiple of `align`, diff --git a/ts/src/lower/address.rs b/ts/src/lower/address.rs index c3eed43e0..d2c0c2815 100644 --- a/ts/src/lower/address.rs +++ b/ts/src/lower/address.rs @@ -50,12 +50,12 @@ pub fn read<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Resul let stored = super::copy::storage(b.t.tcx, ty); b.t.addressing.insert(stored); } - Ok(layout(b, ty)) + Ok(layout(b.t, ty)) } /// The size and alignment of a `ty`, as `rs.addr` takes them. -fn layout<'tcx>(b: &Body<'_, '_, 'tcx>, ty: Ty<'tcx>) -> [Expr; 2] { - let (tcx, env) = (b.t.tcx, b.t.typing_env_pub()); +pub(super) fn layout<'tcx>(t: &Translator<'_, 'tcx>, ty: Ty<'tcx>) -> [Expr; 2] { + let (tcx, env) = (t.tcx, t.typing_env_pub()); let layout = tcx.layout_of(env.as_query_input(ty)).ok(); let size = layout.map_or(0, |l| l.size.bytes()); let align = layout.map_or(1, |l| l.align.abi.bytes()); @@ -68,7 +68,7 @@ fn layout<'tcx>(b: &Body<'_, '_, 'tcx>, ty: Ty<'tcx>) -> [Expr; 2] { /// once its allocation is freed. pub fn numbered<'tcx>(b: &Body<'_, '_, 'tcx>, arc: Expr, value: Ty<'tcx>) -> Expr { match b.t.addressed.contains(&super::copy::storage(b.t.tcx, value)) { - true => Expr::rs("arcAt", [arc].into_iter().chain(layout(b, value)).collect()), + true => Expr::rs("arcAt", [arc].into_iter().chain(layout(b.t, value)).collect()), false => arc, } } diff --git a/ts/src/lower/pointer.rs b/ts/src/lower/pointer.rs index 1cf3bdb23..50fe837e1 100644 --- a/ts/src/lower/pointer.rs +++ b/ts/src/lower/pointer.rs @@ -136,8 +136,12 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } /// What tells whether two pointers to `ty` point at the same place where `===` can't: - /// the slices' of an array. + /// the slices' of an array, and the addresses of a type whose address the runs before + /// read, which a pointer to a freed allocation shares with the one that reuses it. pub fn same_fn(&self, ty: Ty<'tcx>) -> Option { + if self.addressed.contains(&super::copy::storage(self.tcx, ty)) { + return Some(Expr::rs("sameAddr", super::address::layout(self, ty).into())); + } (self.pointee(ty) == Pointee::Array).then(|| self.array_rs(ty, "Same")) }