From aba3b47087d8649d2821ababb8ceb3dc1fb144a8 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:19:50 -0800 Subject: [PATCH 1/4] chore(quest): claim quest/m3/ts-lazy-writes.md Quest-Claim: 8179264eb8ad9f1d From 2cb525d613fd35c63ab8a40251fbf7d4d8cb680a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:34:15 -0800 Subject: [PATCH 2/4] feat(ts): write a lazy cell, a lazy lock, a poll_fn, and a thread-local key whole through a reference Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-lazy-writes.md | 19 - ts/README.md | 6 +- ts/fixtures/lang/locals/expected/lib.ts | 4 +- ts/fixtures/lang/locals/expected/locals.ts | 54 ++- .../lang/locals/expected/std/thread/local.ts | 14 + ts/fixtures/lang/locals/src/lib.rs | 5 +- ts/fixtures/lang/locals/src/locals.rs | 38 +- ts/fixtures/lang/statics/caller.ts | 5 +- ts/fixtures/lang/statics/expected/lib.ts | 13 +- .../lang/statics/expected/statics.Dropped.ts | 31 ++ ts/fixtures/lang/statics/expected/statics.ts | 206 +++++++++- .../lang/statics/expected/std/thread/local.ts | 6 + ts/fixtures/lang/statics/src/lib.rs | 1 + ts/fixtures/lang/statics/src/statics.rs | 102 ++++- ts/fixtures/reject/census.txt | 4 - ts/fixtures/reject/expected.txt | 4 - ts/fixtures/reject/src/places.rs | 8 - ts/fixtures/reject/src/statics.rs | 23 +- ts/fixtures/reject/ts.toml | 4 - ts/fixtures/task/expected/futures.ts | 379 ++++++++++++------ ts/fixtures/task/src/futures.rs | 34 +- ts/runtime/rt.ts | 19 +- ts/runtime/rt/task.ts | 37 +- ts/src/lower/mir/place.rs | 15 +- ts/src/lower/mod.rs | 33 +- 26 files changed, 849 insertions(+), 216 deletions(-) delete mode 100644 quest/m3/ts-lazy-writes.md create mode 100644 ts/fixtures/lang/statics/expected/statics.Dropped.ts diff --git a/quest/m3/README.md b/quest/m3/README.md index 8771d9ab8..5f00a0f2b 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -35,7 +35,6 @@ generic std gap is planned here even when moq never calls it. - [An Arc's pointer](/quest/m3/ts-arc-pointers.md) - `Arc::as_ptr` and `Rc::as_ptr` translate, comparable by `ptr::addr_eq` - [The rest of a vector's methods](/quest/m3/ts-vec-rest.md) - `append`, `resize`, `split_off`, `try_reserve`, and `Vec::from(VecDeque)` translate - [A reversed zip's per-pair cost](/quest/m3/ts-zip-rev-overhead.md) - a reversed by-index zip costs per pair what a forward one does, or the gap is explained -- [Whole writes into a lazy cell or thread-local key](/quest/m3/ts-lazy-writes.md) - a whole write through a `&mut` to a `LazyCell`, `LazyLock`, or `LocalKey` keeps its state - [Type errors at the Rust that made them](/quest/m3/ts-span-precision.md) - a `tsc` error on output names the tail expression or call argument that made it - [Fresh writes through a zip, a copy, and a repeat](/quest/m3/ts-fresh-shapes.md) - zipping a `Vec<[T; N]>`, reading a `T` out of `Zip::next`, and `copied` or `vec![x; n]` over a `Copy` type translate - [Python's wake on the loop's own thread](/quest/m3/ffi-python-wake.md) - a wake on the event loop's thread resumes the future with no socket pair diff --git a/quest/m3/ts-lazy-writes.md b/quest/m3/ts-lazy-writes.md deleted file mode 100644 index d431b6860..000000000 --- a/quest/m3/ts-lazy-writes.md +++ /dev/null @@ -1,19 +0,0 @@ -# [S] Whole writes into a lazy cell or thread-local key - -## Goal - -[rust2-ts](/ts/README.md) translates a whole write through a `&mut` to a `LazyCell`, a -`LazyLock`, or a `LocalKey`, as `*r = LazyCell::new(f)` or `mem::swap` of two, keeping -whether each is initialized and what it holds, proven by differential tests against native -Rust that read each after the write. - -## Plan - -Found by [#386](https://github.com/moq-dev/rust2/pull/386), which refuses these writes with -a span in `Translator::write_in`: their runtime classes keep their state in `#private` -fields, which the field copy a whole write lowers to misses, so the write was silently -lost on `main` before. Copy that state across in the runtime (a method each class owns), -or write the object in place, rather than widening the field copy. `PollFn` is refused the -same way; fold it in if the same method fits. - -Public API: the translator's output. diff --git a/ts/README.md b/ts/README.md index 23a57026c..ca6f2b215 100644 --- a/ts/README.md +++ b/ts/README.md @@ -106,8 +106,8 @@ crate translation reaches is refused, and so are two versions of one crate it re | `ControlFlow` | `rt.Result`, `Ok` to continue; what `?` branches on is the value it was applied to, but for a `Poll`'s `Pending` and `Ready(None)`, which continue as `Ok` of themselves | | tuple, `[T; N]`, `&[T]`, `Box<[T]>`, `Vec` | an array; for bytes, `Uint8Array` for `[u8; N]`, `rt.ByteSlice` for `&[u8]`, `&[u8; N]`, and `Box<[u8]>`, and `rt.ByteVec` for `Vec` | | `Cow<[T]>` | `rt.Cow`: the `Vec` it owns, or an `rt.Borrowed` holding the slice it borrows; `rt.ByteCow` for bytes, an `rt.ByteVec` or an `rt.ByteSlice` | -| `&T`, `&mut T`, `Box`, `Reverse`, `NonZero` | the value; a `&mut` to a value JS copies, such as a number, an `Option`, a `String`, or a reference, or to an `Arc` or a `Waker`, is an `rt.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 `rt.ByteSlice` of a `[u8; N]`'s bytes, which compares by the buffer and offset they are at, or an `rt.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 `rt.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; `rt.NULL` for null and `rt.DANGLING` for dangling. A `*const ()` is an identity, such as a waker's data, and `rt.DANGLING_UNIT` when dangling; one to a place of `()` or from a reference to one is refused. A write of a whole object copies into it one object deep, as an assignment through a `&mut` does, 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 | +| `&T`, `&mut T`, `Box`, `Reverse`, `NonZero` | 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 `rt.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 `rt.ByteSlice` of a `[u8; N]`'s bytes, which compares by the buffer and offset they are at, or an `rt.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 `rt.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; `rt.NULL` for null and `rt.DANGLING` for dangling. A `*const ()` is an identity, such as a waker's data, and `rt.DANGLING_UNIT` when dangling; one to a place of `()` or from a reference to one is refused. A write of a whole object copies into it one object deep, as an assignment through a `&mut` does, the state an `rt` class such as `rt.LazyCell` keeps private included, so a pointer to a part kept as an object, such as a field of a tuple or struct type, is refused wherever the crate may put another object there while a pointer into that memory may still be read, which a write into a fresh allocation, or a variable before any pointer into it is taken, can't, whether by index or through std's iterators over its slots; a read copies what the crate writes into in place | | `MaybeUninit` | `T`: `undefined` until written, 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 | @@ -135,7 +135,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `Arc`, `Rc`, `Weak`, `Mutex` | `rt.Arc` for either, of what a `Box` is for a slice or `str`, with its strong and weak counts, an `Arc<[u8]>` of an `Arc`'s bytes sharing its count, and `new_cyclic`'s `Weak` upgrading only once its value is made, `get_mut` lending its value only while no other `Arc` or `Weak` shares it, `make_mut` cloning it into a new `rt.Arc` while another `Arc` shares it, or moving it there while only `Weak`s do, which then dangle, and `into_inner` giving it only from the last strong `Arc`, after which its `Weak`s no longer upgrade; `rt.Weak`, counted in its `rt.Arc` until it drops, and of no `Arc` once made by `Weak::new`; `rt.Mutex`, its own guard, which a lock gives as `Ok` and the guard's drop unlocks: locking it again while held panics and `try_lock` gives `WouldBlock`, as wasm32's do, and a guard's `map` and `filter_map` make `rt.MutexMap`s, each where the part it lends is, which hold the mutex locked until they drop, and a panic aborts, so nothing poisons it and `thread::panicking` is `false`; a `Mutex`'s, a `RefCell`'s, and an `RwLock`'s `into_inner` gives its value and `get_mut` lends it, or an `rt.Place` of it where JS copies it | | `PoisonError`, `TryLockError` | the `T` it holds; `rt.TryLockError`, tagged as `Bound` is | | `Cell`, `UnsafeCell`, `RefCell`, `RwLock` | `rt.Cell`, the place holding its value in `v`, for either of the first two: `UnsafeCell::get`'s pointer is the cell where it is an `rt.Place`, and otherwise the object in `v`, refused where the crate may put another cell or object in its place; `rt.RefCell` for either of the others, its own guard, counting its borrows: borrowing it while that conflicts panics as `RefCell`'s does, and locking it so aborts as wasm32's `RwLock` does; `try_borrow` and `try_borrow_mut` fail with `rt.BorrowError` and `rt.BorrowMutError` instead, a clone borrows the cell, `Ref::clone` counts another borrow of it, and a guard's `map`, `filter_map`, `try_map`, and `map_split`, an `RwLock` guard's included, make `rt.RefMap`s, each where a part it lends is, which a write through a mapped `RefMut` reaches in place, sharing the cell's borrow until the last drops; `RwLockWriteGuard::downgrade` keeps the lock as one read, and `Ref::leak` and `RefMut::leak` keep the borrow until `RefCell::undo_leak` ends it; `Cell::update` calls its function on a copy, and `RefCell::replace_with` on the value, borrowed as `borrow_mut` does, or its place where JS copies it; refused for a part of a sized type JS can't write in place, such as a binding's | -| `thread_local!`, `LocalKey` | an `rt.LocalKey`, a module's value, which makes its value on first use from the `const` or the expression and never drops it, as wasm32's do; `with` calls its function on that value, `try_with` never fails, the `Cell` and `RefCell` helpers lower from std, and a `RefCell` key's `set` and `with_borrow_mut` borrow it as `borrow_mut` does | +| `thread_local!`, `LocalKey` | an `rt.LocalKey`, a module's value that each copy of the key is, which makes its value on first use from the `const` or the expression and never drops it, as wasm32's do; `with` calls its function on that value, `try_with` never fails, the `Cell` and `RefCell` helpers lower from std, and a `RefCell` key's `set` and `with_borrow_mut` borrow it as `borrow_mut` does | | `OnceCell`, `OnceLock` | `rt.OnceCell` and `rt.OnceLock`, holding the `Option` in `v`, which is the place `get_mut` lends where JS copies the value; a `OnceLock` initialized from within its own initialization panics, as wasm32's does, and `get_or_try_init`'s error leaves either empty | | `Once`, `OnceState` | `rt.Once`, an `rt.OnceLock` that holds `true` once `call_once` or `call_once_force` ran its function, and panics as wasm32's does if run again from within it; `rt.OnceState`, that `Once`, which a panic never poisons, as it aborts | | `LazyCell`, `LazyLock` | `rt.LazyCell` and `rt.LazyLock`, which run their function on the first `force` or `Deref`, once, and drop it then, or with the cell if it never ran; forced from within it, either panics as std's does | diff --git a/ts/fixtures/lang/locals/expected/lib.ts b/ts/fixtures/lang/locals/expected/lib.ts index 811fe4e36..3f63204d5 100644 --- a/ts/fixtures/lang/locals/expected/lib.ts +++ b/ts/fixtures/lang/locals/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { locals } from "./locals.ts"; +import { locals, keys } from "./locals.ts"; import type * as Twin from "./locals.Twin.ts"; export function copy$locals_Twin(v: Twin.Twin): Twin.Twin { @@ -19,6 +19,8 @@ export function copy$tuple2_locals_Twin_locals_Twin(v: [Twin.Twin, Twin.Twin]): export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined][] { return [["locals::locals", (_22: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): undefined => { locals(data, out); + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["locals::keys", (_23: rt.u8, data2: rt.ByteSlice, out2: rt.ByteVec): undefined => { + keys(data2, out2); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined]]; } diff --git a/ts/fixtures/lang/locals/expected/locals.ts b/ts/fixtures/lang/locals/expected/locals.ts index 80c45ca44..0afb042af 100644 --- a/ts/fixtures/lang/locals/expected/locals.ts +++ b/ts/fixtures/lang/locals/expected/locals.ts @@ -8,7 +8,7 @@ import { mapOr$18ipla0 as result_mapOr$18ipla0 } from "./core/result.ts"; import * as Noisy from "./locals.Noisy.ts"; import type * as Polling from "./locals.Polling.ts"; import type * as Twin from "./locals.Twin.ts"; -import { set$0st49po, get$0x8wyln, get$1ei2eoj, take$09yi5f2, replace$0kxb7te, withBorrow$1qygtee, withBorrow$1k8k9m1, withBorrow$0r4ucls, withBorrow$1t1sbub, withBorrow$15ql9sa, take$1x34chv, take$122f15k, replace$0bb3zeq, replace$1xk72wn } from "./std/thread/local.ts"; +import { set$0st49po, set$0xuu1fw, get$0x8wyln, get$1ei2eoj, get$18ignl5, take$09yi5f2, replace$0kxb7te, withBorrow$1qygtee, withBorrow$1k8k9m1, withBorrow$0r4ucls, withBorrow$1t1sbub, withBorrow$15ql9sa, take$1x34chv, take$122f15k, replace$0bb3zeq, replace$1xk72wn } from "./std/thread/local.ts"; export const COUNT: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey(COUNT$init, false); @@ -22,6 +22,12 @@ export function HELD$init(): rt.RefCell { return new rt.RefCell(undefined as Noisy.Noisy | undefined); } +export const LEFT: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey(LEFT$init, false); + +export function LEFT$init(): rt.Cell { + return new rt.Cell(1); +} + export const LOG: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey((): rt.RefCell => { return LOG$init(undefined); }, true); @@ -45,6 +51,14 @@ export function POLLING$init(): Polling.Polling { return { current: new rt.Cell(undefined as [rt.u64, rt.u64] | undefined), held: new rt.RefCell(new rt.ByteVec()) }; } +export const RIGHT: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey((): rt.Cell => { + return RIGHT$init(undefined); +}, true); + +export function RIGHT$init(_1: undefined): rt.Cell { + return new rt.Cell(2); +} + export const TALLY: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey((): rt.RefCell => { return TALLY$init(undefined); }, true); @@ -261,4 +275,42 @@ export function NEXT$init$serial(): rt.Cell<[rt.u8, rt.u8]> { return new rt.Cell([0, 4] as [rt.u8, rt.u8]); } +/** `*r = v`, through the reference. */ +export function assign$thread_LocalKey_Cell_u8(r: rt.Place>>, v: rt.LocalKey>): void { + r.v = v; +} + +/** + * Each byte a copy of `LEFT` written whole with `RIGHT` by each way std moves a value, or + * not at all, after which adding the byte through the copy changes only the key it names. + * Each puts what the replaced copy names, then what each key holds. A key is used only + * through a `&'static`, so each copy is leaked. + */ +export function keys(data: rt.ByteSlice, out: rt.ByteVec): void { + for (const op of rt.byteSliceView(data)) { + const key: rt.Place>> = { v: LEFT }; + const _12: rt.u8 = rt.u8Rem(op, 5); + if (_12 === 0) { + assign$thread_LocalKey_Cell_u8(key, RIGHT); + } else if (_12 === 1) { + const old = key.v; + key.v = RIGHT; + rt.byteVecPush(out, get$18ignl5({ v: old }.v)); + } else if (_12 === 2) { + const other: rt.Place>> = { v: RIGHT }; + const swapped = key.v; + key.v = other.v; + other.v = swapped; + rt.byteVecPush(out, get$18ignl5(other.v)); + } else if (_12 === 3) { + rt.cellSet(rt.cellOfPlace(key), RIGHT); + } + const _28: rt.u8 = rt.u8WrappingAdd(get$18ignl5(key.v), op); + set$0xuu1fw(key.v, _28); + rt.iterExtendBytes(out, rt.iterSlice(Uint8Array.of(get$18ignl5(LEFT), get$18ignl5(RIGHT))), undefined, true); + } + set$0xuu1fw(LEFT, 1); + set$0xuu1fw(RIGHT, 2); +} + //# sourceMappingURL=locals.ts.map diff --git a/ts/fixtures/lang/locals/expected/std/thread/local.ts b/ts/fixtures/lang/locals/expected/std/thread/local.ts index 26c85b860..4fbe683cb 100644 --- a/ts/fixtures/lang/locals/expected/std/thread/local.ts +++ b/ts/fixtures/lang/locals/expected/std/thread/local.ts @@ -13,6 +13,14 @@ export function set$0st49po(self: rt.LocalKey>, value: rt.u32): }); } +export function set$0xuu1fw(self: rt.LocalKey>, value: rt.u8): void { + rt.localKeyInitializeWith(self, new rt.Cell(value), (value2: rt.Cell | undefined, cell: rt.Cell): undefined => { + if (value2 !== undefined) { + rt.cellSet(cell, value2.v); + } + }); +} + export function get$0x8wyln(self: rt.LocalKey>): [Twin.Twin, Twin.Twin] { return ((a0: rt.Cell<[Twin.Twin, Twin.Twin]>): [Twin.Twin, Twin.Twin] => { return copy$tuple2_locals_Twin_locals_Twin(a0.v); @@ -25,6 +33,12 @@ export function get$1ei2eoj(self: rt.LocalKey>): rt.u32 { })(self.get()); } +export function get$18ignl5(self: rt.LocalKey>): rt.u8 { + return ((a0: rt.Cell): rt.u8 => { + return a0.v; + })(self.get()); +} + export function take$09yi5f2(self: rt.LocalKey>): rt.u32 { return ((a0: rt.Cell): rt.u32 => { return rt.cellReplace(a0, 0); diff --git a/ts/fixtures/lang/locals/src/lib.rs b/ts/fixtures/lang/locals/src/lib.rs index 91d91dd71..b8ce24820 100644 --- a/ts/fixtures/lang/locals/src/lib.rs +++ b/ts/fixtures/lang/locals/src/lib.rs @@ -15,5 +15,8 @@ use harness::Case; /// Each case's name and what it runs. The first two bytes, big-endian, pick one modulo /// their count, so a case added at the end moves no other. fn cases() -> Vec<(&'static str, Case)> { - vec![("locals::locals", |_, data, out| locals::locals(data, out))] + vec![ + ("locals::locals", |_, data, out| locals::locals(data, out)), + ("locals::keys", |_, data, out| locals::keys(data, out)), + ] } diff --git a/ts/fixtures/lang/locals/src/locals.rs b/ts/fixtures/lang/locals/src/locals.rs index 26047aa16..2f810bb1b 100644 --- a/ts/fixtures/lang/locals/src/locals.rs +++ b/ts/fixtures/lang/locals/src/locals.rs @@ -2,12 +2,15 @@ //! and a struct of both, as kio's `Polling` is, initialized by a `const` block or lazily //! by an expression; read and written through `with`, given a closure or a function, //! `try_with`, and the `Cell` and `RefCell` helpers, as kio's `coop` does; nested; and -//! declared in a function, as kio's `serial` is. Each case leaves +//! declared in a function, as kio's `serial` is; and a copy of a key written whole through +//! a `&mut`, which then names the other's storage. Each case leaves //! every thread-local as it found it, so a fresh instance and a reused one agree. use std::cell::{Cell, RefCell}; use std::marker::PhantomData; +use std::mem; use std::sync::{Arc, Mutex}; +use std::thread::LocalKey; /// Logs its id when it drops. struct Noisy { @@ -49,6 +52,9 @@ thread_local! { x.0 = 3; Cell::new((x, y)) }; + /// The keys `keys` writes over each other's copies, initialized by a `const` block or lazily. + static LEFT: Cell = const { Cell::new(1) }; + static RIGHT: Cell = Cell::new(2); } /// Each byte an operation on the thread-locals; each puts what it read and what dropped. @@ -158,3 +164,33 @@ fn serial(reset: bool) -> u8 { n }) } + +/// `*r = v`, through the reference. +fn assign(r: &mut T, v: T) { + *r = v; +} + +/// Each byte a copy of `LEFT` written whole with `RIGHT` by each way std moves a value, or +/// not at all, after which adding the byte through the copy changes only the key it names. +/// Each puts what the replaced copy names, then what each key holds. A key is used only +/// through a `&'static`, so each copy is leaked. +pub fn keys(data: &[u8], out: &mut Vec) { + for &op in data { + let key: &'static mut LocalKey> = Box::leak(Box::new(LEFT)); + match op % 5 { + 0 => assign(key, RIGHT), + 1 => out.push(Box::leak(Box::new(mem::replace(key, RIGHT))).get()), + 2 => { + let other = Box::leak(Box::new(RIGHT)); + mem::swap(key, other); + out.push(other.get()); + } + 3 => Cell::from_mut(&mut *key).set(RIGHT), + _ => {} + } + key.set(key.get().wrapping_add(op)); + out.extend([LEFT.get(), RIGHT.get()]); + } + LEFT.set(1); + RIGHT.set(2); +} diff --git a/ts/fixtures/lang/statics/caller.ts b/ts/fixtures/lang/statics/caller.ts index d21a45209..874d4c30d 100644 --- a/ts/fixtures/lang/statics/caller.ts +++ b/ts/fixtures/lang/statics/caller.ts @@ -14,4 +14,7 @@ const onces = [[1, 0, 1, 7], [0, 6, 1, 2, 3, 4, 5, 11], [2, 14, 8], [9], [3, 15] /** `statics::lazies` scripts: each lazy cell and lock printed before it is forced, after, and from within its own function. */ const lazies = [[0, 1, 2, 3, 4, 5, 4, 5, 0]].map((bytes) => Uint8Array.of(...op("statics::lazies"), 0, ...bytes)); -export const cases: Uint8Array[] = [...onces, ...lazies]; +/** `statics::writes` scripts: each way of writing a lazy cell and lock, with neither, either, or both forced first. */ +const writes = [Array.from({ length: 56 }, (_, i) => i)].map((bytes) => Uint8Array.of(...op("statics::writes"), 0, ...bytes)); + +export const cases: Uint8Array[] = [...onces, ...lazies, ...writes]; diff --git a/ts/fixtures/lang/statics/expected/lib.ts b/ts/fixtures/lang/statics/expected/lib.ts index 38bbf19e8..ecdb7ce2a 100644 --- a/ts/fixtures/lang/statics/expected/lib.ts +++ b/ts/fixtures/lang/statics/expected/lib.ts @@ -1,9 +1,14 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { statics, cells, onces, lazies } from "./statics.ts"; +import { statics, cells, onces, lazies, writes } from "./statics.ts"; +import type * as Dropped from "./statics.Dropped.ts"; import * as Lock from "./statics.Lock.ts"; +export function drop$cell_LazyCell_statics_Dropped_statics_cell_closure0(value: rt.LazyCell): void { + rt.lazyDrop(value, rt.drop); +} + export function drop$statics_Lock_Rc_unit(_1: Lock.Lock>): void { Lock.drop$ops_Drop_Rc_unit(_1); } @@ -12,6 +17,10 @@ export function drop$statics_Lock_tuple2_u8_Rc_unit(_1: Lock.Lock<[rt.u8, rt.Arc Lock.drop$ops_Drop_tuple2_u8_Rc_unit(_1); } +export function drop$sync_LazyLock_statics_Dropped_statics_lock_closure0(value: rt.LazyLock): void { + rt.lazyDrop(value, rt.drop); +} + export function drop$tuple2_u8_Rc_unit(_1: [rt.u8, rt.Arc]): void { _1[1][Symbol.dispose](); } @@ -29,6 +38,8 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) onces(data3, out3); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["statics::lazies", (_25: rt.u8, data4: rt.ByteSlice, out4: rt.ByteVec): undefined => { lazies(data4, out4); + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["statics::writes", (_26: rt.u8, data5: rt.ByteSlice, out5: rt.ByteVec): undefined => { + writes(data5, out5); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined]]; } diff --git a/ts/fixtures/lang/statics/expected/statics.Dropped.ts b/ts/fixtures/lang/statics/expected/statics.Dropped.ts new file mode 100644 index 000000000..4da0ef4f1 --- /dev/null +++ b/ts/fixtures/lang/statics/expected/statics.Dropped.ts @@ -0,0 +1,31 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { DROPS } from "./statics.ts"; + +export function fmt$fmt_Debug(self: Dropped, f: rt.Formatter): rt.Result { + const _3: string = "Dropped"; + const _4: rt.Fmt = (f: rt.Formatter) => rt.u8Debug(self._0, f); + return rt.debugTupleFinish(rt.debugTupleField(rt.fmtDebugTuple(f, _3), _4)); +} + +/** Logs its id when it drops. */ +export class Dropped { + _0: rt.u8; + + constructor(_0: rt.u8) { + this._0 = _0; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + } +} + +export function drop$ops_Drop(self: Dropped): void { + rt.localKeyWithBorrowMut(DROPS, (drops: rt.ByteVec): undefined => { + rt.byteVecPush(drops, self._0); + }); +} + +//# sourceMappingURL=statics.Dropped.ts.map diff --git a/ts/fixtures/lang/statics/expected/statics.ts b/ts/fixtures/lang/statics/expected/statics.ts index 9c5460c53..018f546e6 100644 --- a/ts/fixtures/lang/statics/expected/statics.ts +++ b/ts/fixtures/lang/statics/expected/statics.ts @@ -1,14 +1,15 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { drop$statics_Lock_Rc_unit, drop$statics_Lock_tuple2_u8_Rc_unit } from "./lib.ts"; +import { drop$cell_LazyCell_statics_Dropped_statics_cell_closure0, drop$statics_Lock_Rc_unit, drop$statics_Lock_tuple2_u8_Rc_unit, drop$sync_LazyLock_statics_Dropped_statics_lock_closure0 } from "./lib.ts"; import { LATER, read } from "./statics/elsewhere.ts"; import * as Collector from "./statics.Collector.ts"; import type * as Counter from "./statics.Counter.ts"; +import * as Dropped from "./statics.Dropped.ts"; import * as Lock from "./statics.Lock.ts"; import type * as Node from "./statics.Node.ts"; import type * as Raw from "./statics.Raw.ts"; -import { withBorrow$1roy6g4, take$1357qwq } from "./std/thread/local.ts"; +import { withBorrow$1roy6g4, take$1357qwq, take$122f15k } from "./std/thread/local.ts"; export const COUNT: rt.Atomic = /* @__PURE__ */ new rt.Atomic({ hi: 0xffffffff, lo: 0xfffffffd } as rt.u64); @@ -431,4 +432,205 @@ export function lazies(data: rt.ByteSlice, out: rt.ByteVec): void { } } +export const DROPS: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey(DROPS$init, false); + +export function DROPS$init(): rt.RefCell { + return new rt.RefCell(new rt.ByteVec()); +} + +export function cell(id: rt.u8): rt.LazyCell { + const held: Dropped.Dropped = new Dropped.Dropped(id); + const captured = held._0; + const _0: rt.LazyCell = new rt.LazyCell((): Dropped.Dropped => { + return new Dropped.Dropped(rt.u8Add(rt.u8Add(captured, 10), rt.atomicFetch(FORCED, rt.u8WrappingAdd, 1, 0))); + }); + held[Symbol.dispose](); + return _0; +} + +export function lock(id: rt.u8): rt.LazyLock { + const held: Dropped.Dropped = new Dropped.Dropped(id); + const captured = held._0; + const _0: rt.LazyLock = new rt.LazyLock((): Dropped.Dropped => { + return new Dropped.Dropped(rt.u8Add(rt.u8Add(captured, 10), rt.atomicFetch(FORCED, rt.u8WrappingAdd, 1, 0))); + }); + held[Symbol.dispose](); + return _0; +} + +/** `*r = v`, through the reference. */ +export function assign$boxed_Box_cell_LazyCell_statics_Dropped_statics_cell_closure0(r: rt.Place>, v: rt.LazyCell): void { + rt.lazyDrop(r.v, rt.drop); + r.v = v; +} + +/** `*r = v`, through the reference. */ +export function assign$boxed_Box_sync_LazyLock_statics_Dropped_statics_lock_closure0(r: rt.Place>, v: rt.LazyLock): void { + rt.lazyDrop(r.v, rt.drop); + r.v = v; +} + +/** `*r = v`, through the reference. */ +export function assign$cell_LazyCell_statics_Dropped_statics_cell_closure0(r: rt.LazyCell, v: rt.LazyCell): void { + rt.lazyDrop(r, rt.drop); + rt.ptrWrite(r, v); +} + +/** `*r = v`, through the reference. */ +export function assign$sync_LazyLock_statics_Dropped_statics_lock_closure0(r: rt.LazyLock, v: rt.LazyLock): void { + rt.lazyDrop(r, rt.drop); + rt.ptrWrite(r, v); +} + +/** + * Each byte a lazy cell or lock written whole through a `&mut` by each way std moves a + * value, after each, the one written and the one given, is forced or not; then it is + * printed, forced, and dropped, and each `Dropped` dropped meanwhile put in order. + */ +export function writes(data: rt.ByteSlice, out: rt.ByteVec): void { + for (const op of rt.byteSliceView(data)) { + if (rt.u8Rem(op, 2) === 0) { + written$cell_LazyCell_statics_Dropped_statics_cell_closure0(rt.u8Div(op, 2), cell, out); + } else { + written$sync_LazyLock_statics_Dropped_statics_lock_closure0(rt.u8Div(op, 2), lock, out); + } + rt.iterExtendBytes(out, rt.byteVecIntoIter(take$122f15k(DROPS)), undefined, true); + rt.byteVecPush(out, rt.atomicSwap(FORCED, 0, 0)); + } +} + +export function written$cell_LazyCell_statics_Dropped_statics_cell_closure0(op: rt.u8, make: (a0: rt.u8) => rt.LazyCell, out: rt.ByteVec): void { + const _18: (a0: rt.LazyCell) => string = (l: rt.LazyCell): string => { + const args2: rt.Fmt[] = [(f: rt.Formatter) => l.debug(f, "LazyCell", (value: Dropped.Dropped, f: rt.Formatter) => Dropped.fmt$fmt_Debug(value, f))]; + return rt.fmtFormat([args2[0], "|"]); + }; + let _103: boolean = false; + let a: rt.LazyCell = make(1); + let b: rt.LazyCell = make(2); + _103 = true; + if ((op & 1) !== 0) { + rt.byteVecPush(out, a.force()._0); + } + if ((op & 2) !== 0) { + rt.byteVecPush(out, b.force()._0); + } + const _20: rt.u8 = rt.u8Rem(op >> 2, 7); + if (_20 === 0) { + const _25: rt.LazyCell = a; + _103 = false; + assign$cell_LazyCell_statics_Dropped_statics_cell_closure0(_25, b); + } else if (_20 === 1) { + const _28: rt.LazyCell = a; + _103 = false; + const old: rt.LazyCell = rt.replaceIn(_28, b); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(old))); + rt.lazyDrop(old, rt.drop); + } else if (_20 === 2) { + b = rt.replaceIn(a, b); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(b))); + rt.byteVecPush(out, b.force()._0); + } else if (_20 === 3) { + const _52: rt.Cell> = rt.cellIn(a); + _103 = false; + const old2: rt.LazyCell = rt.cellReplace(_52, b); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(old2))); + rt.lazyDrop(old2, rt.drop); + } else if (_20 === 4) { + const _62: rt.Cell> = rt.cellIn(a); + _103 = false; + rt.cellSet(_62, b, drop$cell_LazyCell_statics_Dropped_statics_cell_closure0); + } else if (_20 === 5) { + let boxed: rt.LazyCell = a; + _103 = false; + const _69: rt.LazyCell = b; + assign$boxed_Box_cell_LazyCell_statics_Dropped_statics_cell_closure0(rt.place(() => boxed, (value) => boxed = value), _69); + a = boxed; + } else { + const held: rt.RefCell<[rt.u8, rt.LazyCell]> = new rt.RefCell([0, a] as [rt.u8, rt.LazyCell]); + const _79: rt.RefCell<[rt.u8, rt.LazyCell]> = rt.refCellBorrowMut(held); + const _78: rt.RefCell> = rt.refMutMap(_79, rt.partPlace(((p: [rt.u8, rt.LazyCell]): rt.LazyCell => { + return p[1]; + })(_79.value))); + _103 = false; + assign$cell_LazyCell_statics_Dropped_statics_cell_closure0(_78.value, b); + rt.refMutRelease(_78); + a = held.value[1]; + } + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(a))); + rt.byteVecPush(out, a.force()._0); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(a))); + if (_103) { + rt.lazyDrop(b, rt.drop); + } + _103 = false; + rt.lazyDrop(a, rt.drop); +} + +export function written$sync_LazyLock_statics_Dropped_statics_lock_closure0(op: rt.u8, make: (a0: rt.u8) => rt.LazyLock, out: rt.ByteVec): void { + const _18: (a0: rt.LazyLock) => string = (l: rt.LazyLock): string => { + const args2: rt.Fmt[] = [(f: rt.Formatter) => l.debug(f, "LazyLock", (value: Dropped.Dropped, f: rt.Formatter) => Dropped.fmt$fmt_Debug(value, f))]; + return rt.fmtFormat([args2[0], "|"]); + }; + let _103: boolean = false; + let a: rt.LazyLock = make(1); + let b: rt.LazyLock = make(2); + _103 = true; + if ((op & 1) !== 0) { + rt.byteVecPush(out, a.force()._0); + } + if ((op & 2) !== 0) { + rt.byteVecPush(out, b.force()._0); + } + const _20: rt.u8 = rt.u8Rem(op >> 2, 7); + if (_20 === 0) { + const _25: rt.LazyLock = a; + _103 = false; + assign$sync_LazyLock_statics_Dropped_statics_lock_closure0(_25, b); + } else if (_20 === 1) { + const _28: rt.LazyLock = a; + _103 = false; + const old: rt.LazyLock = rt.replaceIn(_28, b); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(old))); + rt.lazyDrop(old, rt.drop); + } else if (_20 === 2) { + b = rt.replaceIn(a, b); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(b))); + rt.byteVecPush(out, b.force()._0); + } else if (_20 === 3) { + const _52: rt.Cell> = rt.cellIn(a); + _103 = false; + const old2: rt.LazyLock = rt.cellReplace(_52, b); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(old2))); + rt.lazyDrop(old2, rt.drop); + } else if (_20 === 4) { + const _62: rt.Cell> = rt.cellIn(a); + _103 = false; + rt.cellSet(_62, b, drop$sync_LazyLock_statics_Dropped_statics_lock_closure0); + } else if (_20 === 5) { + let boxed: rt.LazyLock = a; + _103 = false; + const _69: rt.LazyLock = b; + assign$boxed_Box_sync_LazyLock_statics_Dropped_statics_lock_closure0(rt.place(() => boxed, (value) => boxed = value), _69); + a = boxed; + } else { + const held: rt.RefCell<[rt.u8, rt.LazyLock]> = new rt.RefCell([0, a] as [rt.u8, rt.LazyLock]); + const _79: rt.RefCell<[rt.u8, rt.LazyLock]> = rt.refCellBorrowMut(held); + const _78: rt.RefCell> = rt.refMutMap(_79, rt.partPlace(((p: [rt.u8, rt.LazyLock]): rt.LazyLock => { + return p[1]; + })(_79.value))); + _103 = false; + assign$sync_LazyLock_statics_Dropped_statics_lock_closure0(_78.value, b); + rt.refMutRelease(_78); + a = held.value[1]; + } + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(a))); + rt.byteVecPush(out, a.force()._0); + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(_18(a))); + if (_103) { + rt.lazyDrop(b, rt.drop); + } + _103 = false; + rt.lazyDrop(a, rt.drop); +} + //# sourceMappingURL=statics.ts.map diff --git a/ts/fixtures/lang/statics/expected/std/thread/local.ts b/ts/fixtures/lang/statics/expected/std/thread/local.ts index adee050b4..a53ad3333 100644 --- a/ts/fixtures/lang/statics/expected/std/thread/local.ts +++ b/ts/fixtures/lang/statics/expected/std/thread/local.ts @@ -19,4 +19,10 @@ export function take$1357qwq(self: rt.LocalKey>): rt.ByteVec { + return ((a0: rt.RefCell): rt.ByteVec => { + return rt.refCellReplace(a0, new rt.ByteVec()); + })(self.get()); +} + //# sourceMappingURL=local.ts.map diff --git a/ts/fixtures/lang/statics/src/lib.rs b/ts/fixtures/lang/statics/src/lib.rs index de12a54fb..d26fbf988 100644 --- a/ts/fixtures/lang/statics/src/lib.rs +++ b/ts/fixtures/lang/statics/src/lib.rs @@ -20,5 +20,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("statics::cells", |_, data, out| statics::cells(data, out)), ("statics::onces", |_, data, out| statics::onces(data, out)), ("statics::lazies", |_, data, out| statics::lazies(data, out)), + ("statics::writes", |_, data, out| statics::writes(data, out)), ] } diff --git a/ts/fixtures/lang/statics/src/statics.rs b/ts/fixtures/lang/statics/src/statics.rs index b259e2b24..54f501e45 100644 --- a/ts/fixtures/lang/statics/src/statics.rs +++ b/ts/fixtures/lang/statics/src/statics.rs @@ -5,12 +5,16 @@ //! `LazyLock`, and `Once`, crossbeam-epoch's own `OnceLock` over an `UnsafeCell`, and one //! pointing at a static of a module that reads it back; and `const`s holding cells, each //! use of which is a value of its own, as lock_api's `RawMutex::INIT` is, and each lazy one -//! printed before and after it is forced, and from within its own function. Each case leaves +//! printed before and after it is forced, and from within its own function; and lazy cells +//! and locks written whole through a `&mut`, forced or not, each way std moves a value. +//! Each case leaves //! every static as it found it, or reads only what it holds whatever ran before, so a fresh //! instance and a reused one agree. -use std::cell::{Cell, LazyCell, RefCell, UnsafeCell}; -use std::mem::MaybeUninit; +use std::cell::{Cell, LazyCell, RefCell, RefMut, UnsafeCell}; +use std::fmt::Debug; +use std::mem::{self, MaybeUninit}; +use std::ops::Deref; use std::ptr; use std::rc::Rc; use std::sync::atomic::Ordering::{AcqRel, Acquire, Relaxed, SeqCst}; @@ -467,3 +471,95 @@ pub fn lazies(data: &[u8], out: &mut Vec) { out.extend_from_slice(text.as_bytes()); } } + +thread_local! { + /// The ids of the `Dropped`s dropped, in order. + static DROPS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// Logs its id when it drops. +#[derive(Debug)] +struct Dropped(u8); + +impl Drop for Dropped { + fn drop(&mut self) { + DROPS.with_borrow_mut(|drops| drops.push(self.0)); + } +} + +/// A lazy cell or lock whose function owns a `Dropped` of `id`, which drops once it has +/// run, or with the cell if it never does, and makes one of `id + 10` and the count forced. +macro_rules! lazy { + ($name:ident, $ty:ident) => { + fn $name(id: u8) -> $ty Dropped> { + let held = Dropped(id); + $ty::new(move || Dropped(held.0 + 10 + FORCED.fetch_add(1, Relaxed))) + } + }; +} + +lazy!(cell, LazyCell); +lazy!(lock, LazyLock); + +/// `*r = v`, through the reference. +fn assign(r: &mut T, v: T) { + *r = v; +} + +/// Each byte a lazy cell or lock written whole through a `&mut` by each way std moves a +/// value, after each, the one written and the one given, is forced or not; then it is +/// printed, forced, and dropped, and each `Dropped` dropped meanwhile put in order. +pub fn writes(data: &[u8], out: &mut Vec) { + for &op in data { + match op % 2 { + 0 => written(op / 2, cell, out), + _ => written(op / 2, lock, out), + } + out.extend(DROPS.take()); + out.push(FORCED.swap(0, Relaxed)); + } +} + +fn written + Debug>(op: u8, make: fn(u8) -> L, out: &mut Vec) { + let mut a = make(1); + let mut b = make(2); + if op & 1 != 0 { + out.push(a.0); + } + if op & 2 != 0 { + out.push(b.0); + } + let text = |l: &L| format!("{l:?}|"); + match (op >> 2) % 7 { + 0 => assign(&mut a, b), + 1 => { + let old = mem::replace(&mut a, b); + out.extend_from_slice(text(&old).as_bytes()); + } + 2 => { + mem::swap(&mut a, &mut b); + out.extend_from_slice(text(&b).as_bytes()); + out.push(b.0); + } + 3 => { + let old = Cell::from_mut(&mut a).replace(b); + out.extend_from_slice(text(&old).as_bytes()); + } + 4 => Cell::from_mut(&mut a).set(b), + // A box is what it holds. + 5 => { + let mut boxed = Box::new(a); + assign(&mut boxed, Box::new(b)); + a = *boxed; + } + // Through a guard of the part. + _ => { + let held = RefCell::new((0u8, a)); + assign(&mut *RefMut::map(held.borrow_mut(), |p| &mut p.1), b); + a = held.into_inner().1; + } + } + out.extend_from_slice(text(&a).as_bytes()); + out.push(a.0); + out.extend_from_slice(text(&a).as_bytes()); +} diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 028de0d3f..eae5f0f4b 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -127,7 +127,6 @@ fixtures/reject/src/places.rs:rebound_reference+7:14: a copy of a closure that c fixtures/reject/src/places.rs:rebound_reference+8:14: a copy of a closure that changes what it captured fixtures/reject/src/places.rs:replace_boxed_bytes+4:2: replacing the value behind a reference to `std::pin::Pin>` fixtures/reject/src/places.rs:replace_boxed_future+4:2: an assignment through a reference to `std::pin::Pin>>` -fixtures/reject/src/places.rs:replace_boxed_poll_fn+4:2: replacing the value behind a reference to `std::pin::Pin fn(&'a mut std::task::Context<'b>) -> std::task::Poll>>>` fixtures/reject/src/places.rs:replace_guard+1:2: an assignment through a reference to `std::sync::MutexGuard<'_, u8>` fixtures/reject/src/places.rs:replace_pinned_ref+1:2: an assignment through a reference to `std::pin::Pin<&mut places::Noisy>` fixtures/reject/src/places.rs:swap_guards+1:2: replacing the value behind a reference to `std::sync::MutexGuard<'_, u8>` @@ -217,9 +216,6 @@ fixtures/reject/src/rebound.rs:retargeted+5:10: a raw pointer to `rebound::Targe fixtures/reject/src/rebound.rs:swap_retargeted+6:10: a raw pointer to `(u32, u32)` in a variable assigned again while it may point there fixtures/reject/src/rebound.rs:swapped_in_loop+8:10: a raw pointer to `(u32, u32)` in a variable assigned again while it may point there fixtures/reject/src/rebound.rs:unboxed_rebound+2:16: a reference to `rebound::Unboxed`, through which a raw pointer may be made, in a variable assigned again while that pointer may point there -fixtures/reject/src/statics.rs:key_celled+1:2: a slice or a cell of a mutable reference to `std::thread::LocalKey` that names no place, which a whole write would replace only there -fixtures/reject/src/statics.rs:lazy_assigned+1:2: an assignment through a reference to `std::sync::LazyLock` -fixtures/reject/src/statics.rs:lazy_celled+1:3: a slice or a cell of a mutable reference to `std::cell::LazyCell` that names no place, which a whole write would replace only there fixtures/reject/src/statics.rs:pointer_statics+3:15: comparing pointers to `u32` in an atomic, which JS has no address of fixtures/reject/src/statics.rs:pointer_statics+4:3: a constant of type `&u32`, a pointer into memory translation can't name fixtures/reject/src/statics.rs:statics+1:11: a mutable static, which any code may write diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 5141e4a3b..d6b19fb2f 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -128,7 +128,6 @@ fixtures/reject/src/places.rs:rebound_reference+7:14: a copy of a closure that c fixtures/reject/src/places.rs:rebound_reference+8:14: a copy of a closure that changes what it captured fixtures/reject/src/places.rs:replace_boxed_bytes+4:2: replacing the value behind a reference to `std::pin::Pin>` fixtures/reject/src/places.rs:replace_boxed_future+4:2: an assignment through a reference to `std::pin::Pin>>` -fixtures/reject/src/places.rs:replace_boxed_poll_fn+4:2: replacing the value behind a reference to `std::pin::Pin fn(&'a mut std::task::Context<'b>) -> std::task::Poll>>>` fixtures/reject/src/places.rs:replace_guard+1:2: an assignment through a reference to `std::sync::MutexGuard<'_, u8>` fixtures/reject/src/places.rs:replace_pinned_ref+1:2: an assignment through a reference to `std::pin::Pin<&mut places::Noisy>` fixtures/reject/src/places.rs:swap_guards+1:2: replacing the value behind a reference to `std::sync::MutexGuard<'_, u8>` @@ -217,9 +216,6 @@ fixtures/reject/src/rebound.rs:retargeted+5:10: a raw pointer to `rebound::Targe fixtures/reject/src/rebound.rs:swap_retargeted+6:10: a raw pointer to `(u32, u32)` in a variable assigned again while it may point there fixtures/reject/src/rebound.rs:swapped_in_loop+8:10: a raw pointer to `(u32, u32)` in a variable assigned again while it may point there fixtures/reject/src/rebound.rs:unboxed_rebound+2:16: a reference to `rebound::Unboxed`, through which a raw pointer may be made, in a variable assigned again while that pointer may point there -fixtures/reject/src/statics.rs:key_celled+1:2: a slice or a cell of a mutable reference to `std::thread::LocalKey` that names no place, which a whole write would replace only there -fixtures/reject/src/statics.rs:lazy_assigned+1:2: an assignment through a reference to `std::sync::LazyLock` -fixtures/reject/src/statics.rs:lazy_celled+1:3: a slice or a cell of a mutable reference to `std::cell::LazyCell` that names no place, which a whole write would replace only there fixtures/reject/src/statics.rs:pointer_statics+3:15: comparing pointers to `u32` in an atomic, which JS has no address of fixtures/reject/src/statics.rs:pointer_statics+4:3: a constant of type `&u32`, a pointer into memory translation can't name fixtures/reject/src/statics.rs:statics+1:11: a mutable static, which any code may write diff --git a/ts/fixtures/reject/src/places.rs b/ts/fixtures/reject/src/places.rs index 2bf5e4533..5dc9e8959 100644 --- a/ts/fixtures/reject/src/places.rs +++ b/ts/fixtures/reject/src/places.rs @@ -11,14 +11,6 @@ pub fn replace_guard<'a>(dst: &mut std::sync::MutexGuard<'a, u8>, src: std::sync *dst = src; } -/// A boxed `PollFn` is the `rt.PollFn`, whose callback is private where a copy can't reach. -pub fn replace_boxed_poll_fn( - dst: &mut std::pin::Pin) -> std::task::Poll>>>, - src: std::pin::Pin) -> std::task::Poll>>>, -) -> std::pin::Pin) -> std::task::Poll>>> { - std::mem::replace(dst, src) -} - /// A boxed byte array is a `Uint8Array`, which a replacement can't copy out as an object. pub fn replace_boxed_bytes( dst: &mut std::pin::Pin>, diff --git a/ts/fixtures/reject/src/statics.rs b/ts/fixtures/reject/src/statics.rs index bd6e9611d..e130c2327 100644 --- a/ts/fixtures/reject/src/statics.rs +++ b/ts/fixtures/reject/src/statics.rs @@ -1,4 +1,4 @@ -//! Statics and lazy cells whose state JS can't share or copy. +//! Statics whose state JS can't share or copy. static mut COUNT: u8 = 0; @@ -17,24 +17,3 @@ pub fn pointer_statics(p: *mut u32) -> u32 { let _ = SLOT.compare_exchange(p, std::ptr::null_mut(), Relaxed, Relaxed); *NAMED } - -const LAZY_CELL: std::cell::LazyCell = std::cell::LazyCell::new(|| 2); - -const LAZY_LOCK: std::sync::LazyLock = std::sync::LazyLock::new(|| 2); - -/// A lazy cell's state, which JS keeps private, so copying its fields can't move it. -pub fn lazy_celled(c: &mut std::cell::LazyCell) -> u32 { - *std::cell::Cell::from_mut(c).replace(LAZY_CELL) + **c -} - -/// A lazy lock written whole through a reference, which can't copy its private state. -pub fn lazy_assigned(c: &mut std::sync::LazyLock) -> u32 { - *c = LAZY_LOCK; - **c -} - -/// A thread local's key, whose state JS keeps private, set whole through a cell. -pub fn key_celled(key: &'static mut std::thread::LocalKey, next: std::thread::LocalKey) -> u32 { - std::cell::Cell::from_mut(key).set(next); - key.with(|v| *v) -} diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index b7adcea05..e3af034ad 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -105,7 +105,6 @@ roots = [ "places::rebound_reference", "places::replace_boxed_bytes", "places::replace_boxed_future", - "places::replace_boxed_poll_fn", "places::replace_guard", "places::replace_pinned_ref", "places::swap_guards", @@ -163,9 +162,6 @@ roots = [ "rebound::swap_retargeted", "rebound::swapped_in_loop", "rebound::unboxed_rebound", - "statics::key_celled", - "statics::lazy_assigned", - "statics::lazy_celled", "statics::pointer_statics", "statics::statics", "strs::cow_borrowed", diff --git a/ts/fixtures/task/expected/futures.ts b/ts/fixtures/task/expected/futures.ts index 246a5afa6..e61fd0ae9 100644 --- a/ts/fixtures/task/expected/futures.ts +++ b/ts/fixtures/task/expected/futures.ts @@ -340,6 +340,18 @@ export class wakersFuture { } } +/** Write `value` through `slot`, which a dangling pointer to `()` may give. */ +export function put$Pin_boxed_Box_future_PollFn_futures_owning_closure0(slot: rt.Future, value: rt.Future): void { + slot[Symbol.dispose](); + rt.ptrWrite(slot, value); +} + +/** Write `value` through `slot`, which a dangling pointer to `()` may give. */ +export function put$future_PollFn_futures_owning_closure0(slot: rt.Future, value: rt.Future): void { + slot[Symbol.dispose](); + rt.ptrWrite(slot, value); +} + /** Write `value` through `slot`, which a dangling pointer to `()` may give. */ export function put$unit(slot: rt.Place, _: undefined): void { slot.v = undefined; @@ -348,7 +360,8 @@ export function put$unit(slot: rt.Place, _: undefined): void { /** * Boxed futures over a channel: `Box::pin` of an async fn, awaited, and of a `&mut` to a * `poll_fn` and to a `&mut` to it, whose box drops without dropping it, and a `&mut dyn - * Future` of a `&mut` to it, then a queue of + * Future` of a `&mut` to it; `poll_fn`s written whole through a `&mut`, boxed or not, by + * each way std moves one, each written one's guard dropping first; then a queue of * `Pin>`s of an async block, kio's `Pending`, a struct of the crate's, * and a `poll_fn`, each owning a guard. A `poll_fn` boxed in a [`Forward`] polls the * queue in turn through [`poll_unsized`], as moq's tasks do through kio's @@ -373,10 +386,18 @@ export class boxedFuture { #saved!: rt.u64; #awaitee$3!: rt.Future; #awaitee$4!: rt.Future; + #other!: rt.Future; + #old!: rt.Future; + #boxed!: rt.Future; + #moved!: rt.u64; + #awaitee$5!: rt.Future; #pinned!: Pending.Pending<(a0: Waiter.Waiter) => rt.Poll>; - #awaitee$5!: Forward.Forward; + #awaitee$6!: Forward.Forward; #saved$2!: boolean; #saved$3!: boolean; + #saved$4!: boolean; + #saved$5!: boolean; + #saved$6!: boolean; constructor(rx: Consumer.Consumer, guard: Guard.Guard) { this.#rx = rx; @@ -385,38 +406,65 @@ export class boxedFuture { [Symbol.dispose](): void { b0: { - if (this.#state === 0) { - this.#rx[Symbol.dispose](); - this.#guard[Symbol.dispose](); - return; - } - if (this.#state === 3) { - rt.drop(this.#awaitee); - break b0; - } - if (this.#state === 4) { - rt.drop(this.#awaitee$2); - } else if (this.#state === 5) { - rt.drop(this.#awaitee$3); - } else if (!(this.#state === 6 || this.#state === 7)) { + b1: { + if (this.#state === 0) { + this.#rx[Symbol.dispose](); + this.#guard[Symbol.dispose](); + return; + } + if (this.#state === 3) { + rt.drop(this.#awaitee); + break b0; + } + if (this.#state === 4) { + rt.drop(this.#awaitee$2); + break b1; + } + if (this.#state === 5) { + rt.drop(this.#awaitee$3); + break b1; + } + if (this.#state === 6 || this.#state === 7) { + break b1; + } if (this.#state === 8) { this.#awaitee$5[Symbol.dispose](); + } else if (this.#state === 9) { + this.#old[Symbol.dispose](); + } else if (this.#state === 10) { + this.#old[Symbol.dispose](); + } else if (this.#state === 11) { + this.#old[Symbol.dispose](); + } else if (this.#state === 12) { + this.#awaitee$6[Symbol.dispose](); this.#pinned[Symbol.dispose](); } else { return; } + if (this.#saved$2) { + this.#boxed[Symbol.dispose](); + } + this.#saved$2 = false; + if (this.#saved$3) { + this.#old[Symbol.dispose](); + } + this.#saved$3 = false; + if (this.#saved$4) { + this.#other[Symbol.dispose](); + } + this.#saved$4 = false; } - if (this.#saved$2) { + if (this.#saved$5) { this.#lend[Symbol.dispose](); } - this.#saved$2 = false; + this.#saved$5 = false; this.#log[Symbol.dispose](); } this.#guard[Symbol.dispose](); - if (this.#saved$3) { + if (this.#saved$6) { this.#rx[Symbol.dispose](); } - this.#saved$3 = false; + this.#saved$6 = false; } poll(_2: rt.Context): rt.Poll { @@ -425,109 +473,187 @@ export class boxedFuture { b2: { b3: { b4: { - if (this.#state === 0) { - this.#saved$3 = false; - this.#saved$2 = false; - this.#saved$3 = true; - this.#awaitee = new recvFuture$kio_Consumer(this.#rx); - } else if (this.#state === 1) { - rt.asyncFnResumed(); - } else if (this.#state !== 3) { - if (this.#state === 4) { - break b4; + b5: { + b6: { + b7: { + b8: { + if (this.#state === 0) { + this.#saved$6 = false; + this.#saved$5 = false; + this.#saved$4 = false; + this.#saved$3 = false; + this.#saved$2 = false; + this.#saved$6 = true; + this.#awaitee = new recvFuture$kio_Consumer(this.#rx); + } else if (this.#state === 1) { + rt.asyncFnResumed(); + } else if (this.#state !== 3) { + if (this.#state === 4) { + break b8; + } + if (this.#state === 5) { + break b7; + } + if (this.#state === 6) { + break b6; + } + if (this.#state === 7) { + break b5; + } + if (this.#state === 8) { + break b4; + } + if (this.#state === 9) { + break b3; + } + if (this.#state === 10) { + break b2; + } + if (this.#state === 11) { + break b1; + } + break b0; + } + const _7: rt.Poll = this.#awaitee.poll(_2); + if (_7 !== rt.Pending) { + const result: rt.u64 | undefined = _7; + rt.drop(this.#awaitee); + this.#first = option_unwrapOr$1tvry1i(result, 0); + const arg = this.#first; + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(arg, f)]; + const _17: string = rt.fmtFormat(["first ", args2[0]]); + Guard.note(this.#guard, _17); + const _28: [rt.u32, Log.Log] = [Guard.id(this.#guard), Log.clone$Clone(Guard.log(this.#guard))]; + this.#id = _28[0]; + this.#log = _28[1]; + const captured = Guard.new(rt.u32Add(this.#id, 900), Log.clone$Clone(this.#log)); + this.#lend = new rt.PollFn(rt.closure((_22: rt.Context): rt.Poll => { + Guard.note(captured, "lent"); + return 4; + }, (): void => { + captured[Symbol.dispose](); + }), rt.drop); + this.#saved$5 = true; + this.#awaitee$2 = new rt.DynFuture(this.#lend, rt.futurePoll); + } else { + this.#state = 3; + return rt.Pending; + } + } + const _45: rt.Poll = this.#awaitee$2.poll(_2); + if (_45 !== rt.Pending) { + const lent2: rt.u64 = _45; + rt.drop(this.#awaitee$2); + this.#awaitee$2 = this.#lend; + const twice: rt.Future = new rt.DynFuture(this.#awaitee$2, rt.futurePoll); + this.#saved = lent2; + this.#awaitee$3 = twice; + } else { + this.#state = 4; + return rt.Pending; + } + } + const _58: rt.Poll = this.#awaitee$3.poll(_2); + if (_58 !== rt.Pending) { + const result2: rt.u64 = _58; + rt.drop(this.#awaitee$3); + const lent3: rt.u64 = rt.u64Add(this.#saved, result2); + this.#awaitee$3 = this.#lend; + const direct: rt.Future = this.#awaitee$3; + this.#saved = lent3; + this.#awaitee$4 = direct; + } else { + this.#state = 5; + return rt.Pending; + } } - if (this.#state === 5) { - break b3; + const _70: rt.Poll = this.#awaitee$4.poll(_2); + if (_70 !== rt.Pending) { + this.#saved = rt.u64Add(this.#saved, _70); + this.#awaitee$4 = this.#lend; + } else { + this.#state = 6; + return rt.Pending; } - if (this.#state === 6) { - break b2; - } - if (this.#state === 7) { - break b1; - } - break b0; } - const _7: rt.Poll = this.#awaitee.poll(_2); - if (_7 !== rt.Pending) { - const result: rt.u64 | undefined = _7; - rt.drop(this.#awaitee); - this.#first = option_unwrapOr$1tvry1i(result, 0); - const arg = this.#first; - const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(arg, f)]; - const _17: string = rt.fmtFormat(["first ", args2[0]]); - Guard.note(this.#guard, _17); - const _28: [rt.u32, Log.Log] = [Guard.id(this.#guard), Log.clone$Clone(Guard.log(this.#guard))]; - this.#id = _28[0]; - this.#log = _28[1]; - const captured = Guard.new(rt.u32Add(this.#id, 900), Log.clone$Clone(this.#log)); - this.#lend = new rt.PollFn(rt.closure((_22: rt.Context): rt.Poll => { - Guard.note(captured, "lent"); - return 4; - }, (): void => { - captured[Symbol.dispose](); - }), rt.drop); + const _81: rt.Poll = this.#awaitee$4.poll(_2); + if (_81 !== rt.Pending) { + this.#saved = rt.u64Add(this.#saved, _81); + const arg2 = this.#saved; + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(arg2, f)]; + const _92: string = rt.fmtFormat(["lent ", args4[0]]); + Guard.note(this.#guard, _92); + this.#saved$5 = false; + this.#lend[Symbol.dispose](); + this.#awaitee$4 = owning(Guard.new(rt.u32Add(this.#id, 10), Log.clone$Clone(this.#log))); + this.#other = owning(Guard.new(rt.u32Add(this.#id, 40), Log.clone$Clone(this.#log))); + this.#saved$4 = true; + this.#old = rewrite$futures_owning_closure0(this.#awaitee$4, this.#other, owning, this.#guard); + this.#saved$3 = true; + this.#boxed = owning(Guard.new(rt.u32Add(this.#id, 60), Log.clone$Clone(this.#log))); this.#saved$2 = true; - this.#awaitee$2 = new rt.DynFuture(this.#lend, rt.futurePoll); + const _130: rt.Future = owning(Guard.new(rt.u32Add(this.#id, 70), Log.clone$Clone(this.#log))); + put$Pin_boxed_Box_future_PollFn_futures_owning_closure0(this.#boxed, _130); + this.#awaitee$5 = this.#awaitee$4; } else { - this.#state = 3; + this.#state = 7; return rt.Pending; } } - const _45: rt.Poll = this.#awaitee$2.poll(_2); - if (_45 !== rt.Pending) { - const lent2: rt.u64 = _45; - rt.drop(this.#awaitee$2); - this.#awaitee$2 = this.#lend; - const twice: rt.Future = new rt.DynFuture(this.#awaitee$2, rt.futurePoll); - this.#saved = lent2; - this.#awaitee$3 = twice; + const _140: rt.Poll = this.#awaitee$5.poll(_2); + if (_140 !== rt.Pending) { + this.#moved = _140; + this.#awaitee$5[Symbol.dispose](); + this.#saved$3 = false; + this.#old = this.#old; } else { - this.#state = 4; + this.#state = 8; return rt.Pending; } } - const _58: rt.Poll = this.#awaitee$3.poll(_2); - if (_58 !== rt.Pending) { - const result2: rt.u64 = _58; - rt.drop(this.#awaitee$3); - const lent3: rt.u64 = rt.u64Add(this.#saved, result2); - this.#awaitee$3 = this.#lend; - const direct: rt.Future = this.#awaitee$3; - this.#saved = lent3; - this.#awaitee$4 = direct; + const _150: rt.Poll = this.#old.poll(_2); + if (_150 !== rt.Pending) { + const result6: rt.u64 = _150; + this.#old[Symbol.dispose](); + this.#moved = rt.u64Add(this.#moved, result6); + this.#saved$4 = false; + this.#old = this.#other; } else { - this.#state = 5; + this.#state = 9; return rt.Pending; } } - const _70: rt.Poll = this.#awaitee$4.poll(_2); - if (_70 !== rt.Pending) { - this.#saved = rt.u64Add(this.#saved, _70); - this.#awaitee$4 = this.#lend; + const _161: rt.Poll = this.#old.poll(_2); + if (_161 !== rt.Pending) { + const result7: rt.u64 = _161; + this.#old[Symbol.dispose](); + this.#moved = rt.u64Add(this.#moved, result7); + this.#saved$2 = false; + this.#old = this.#boxed; } else { - this.#state = 6; + this.#state = 10; return rt.Pending; } } - const _81: rt.Poll = this.#awaitee$4.poll(_2); - if (_81 !== rt.Pending) { - this.#saved = rt.u64Add(this.#saved, _81); - const arg2 = this.#saved; - const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(arg2, f)]; - const _92: string = rt.fmtFormat(["lent ", args4[0]]); - Guard.note(this.#guard, _92); - this.#saved$2 = false; - this.#lend[Symbol.dispose](); - const _108: [Guard.Guard, Guard.Guard, Guard.Guard] = [Guard.new(rt.u32Add(this.#id, 500), Log.clone$Clone(this.#log)), Guard.new(rt.u32Add(this.#id, 600), Log.clone$Clone(this.#log)), Guard.new(rt.u32Add(this.#id, 700), Log.clone$Clone(this.#log))]; - const block: Guard.Guard = _108[0]; - const waiting: Guard.Guard = _108[1]; - const ready: Guard.Guard = _108[2]; + const _172: rt.Poll = this.#old.poll(_2); + if (_172 !== rt.Pending) { + const result8: rt.u64 = _172; + this.#old[Symbol.dispose](); + this.#moved = rt.u64Add(this.#moved, result8); + const arg3 = this.#moved; + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(arg3, f)]; + const _183: string = rt.fmtFormat(["moved ", args6[0]]); + Guard.note(this.#guard, _183); + const _197: [Guard.Guard, Guard.Guard, Guard.Guard] = [Guard.new(rt.u32Add(this.#id, 500), Log.clone$Clone(this.#log)), Guard.new(rt.u32Add(this.#id, 600), Log.clone$Clone(this.#log)), Guard.new(rt.u32Add(this.#id, 700), Log.clone$Clone(this.#log))]; + const block: Guard.Guard = _197[0]; + const waiting: Guard.Guard = _197[1]; + const ready: Guard.Guard = _197[2]; const next: Consumer.Consumer = Consumer.clone$Clone(this.#rx); - this.#saved$3 = false; - const _131: rt.Future = new boxedFuture$closure2(this.#rx, block); + this.#saved$6 = false; + const _220: rt.Future = new boxedFuture$closure2(this.#rx, block); let captured2 = next; const captured3 = waiting; - const _134: rt.Future = new rt.DynFuture(wait$futures_boxed_closure0_closure3_u64(rt.closure((waiter: Waiter.Waiter): rt.Poll => { + const _223: rt.Future = new rt.DynFuture(wait$futures_boxed_closure0_closure3_u64(rt.closure((waiter: Waiter.Waiter): rt.Poll => { return poll_map$1vdv176(Consumer.pollRecv(captured2, waiter), (value5: rt.u64 | undefined): rt.u64 => { Guard.note(captured3, "waited"); return rt.u64Add(option_unwrapOr$1tvry1i(value5, 0), 2); @@ -537,7 +663,7 @@ export class boxedFuture { captured3[Symbol.dispose](); })), Pending.poll$Future_futures_boxed_closure0_closure3, rt.drop); const captured4 = ready; - const queue: rt.Future[] = [_131, _134, new rt.PollFn(rt.closure((_23: rt.Context): rt.Poll => { + const queue: rt.Future[] = [_220, _223, new rt.PollFn(rt.closure((_23: rt.Context): rt.Poll => { Guard.note(captured4, "ready"); return 3; }, (): void => { @@ -553,7 +679,7 @@ export class boxedFuture { let captured6 = queue; let captured7: rt.u64 = 0; let captured8 = last; - this.#awaitee$5 = new Forward.Forward(new rt.PollFn(rt.closure((cx: rt.Context): rt.Poll => { + this.#awaitee$6 = new Forward.Forward(new rt.PollFn(rt.closure((cx: rt.Context): rt.Poll => { let i: rt.usize = 0; while (i < captured6.length) { const _83: rt.Poll = pollUnsized$dyn_Future_u64(captured6[rt.idx(captured6, i)], cx); @@ -574,26 +700,29 @@ export class boxedFuture { rt.dropAll(captured6); }), rt.drop)); } else { - this.#state = 7; + this.#state = 11; return rt.Pending; } } - const _159: rt.Poll = Forward.poll$Future(this.#awaitee$5, _2); - if (_159 !== rt.Pending) { - const total: rt.u64 = _159; - this.#awaitee$5[Symbol.dispose](); - const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(total, f)]; - const _169: string = rt.fmtFormat(["boxed ", args6[0]]); - Guard.note(this.#guard, _169); + const _248: rt.Poll = Forward.poll$Future(this.#awaitee$6, _2); + if (_248 !== rt.Pending) { + const total: rt.u64 = _248; + this.#awaitee$6[Symbol.dispose](); + const args8: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(total, f)]; + const _258: string = rt.fmtFormat(["boxed ", args8[0]]); + Guard.note(this.#guard, _258); this.#pinned[Symbol.dispose](); this.#saved$2 = false; + this.#saved$3 = false; + this.#saved$4 = false; + this.#saved$5 = false; this.#log[Symbol.dispose](); this.#guard[Symbol.dispose](); - this.#saved$3 = false; + this.#saved$6 = false; this.#state = 1; return total; } - this.#state = 8; + this.#state = 12; return rt.Pending; } } @@ -645,6 +774,32 @@ export class boxedFuture$closure2 { } } +/** A `poll_fn` that owns `guard`, ready with its id. */ +export function owning(guard: Guard.Guard): rt.Future { + const captured = guard; + return new rt.PollFn(rt.closure((_22: rt.Context): rt.Poll => { + return Guard.id(captured) as rt.u64; + }, (): void => { + captured[Symbol.dispose](); + }), rt.drop); +} + +/** + * `written` and `other` written whole through their `&mut`s with `poll_fn`s `make` makes + * of guards past `guard`'s id: by assignment, `mem::replace`, whose old one it returns, + * `mem::swap`, and `Cell::set`. + */ +export function rewrite$futures_owning_closure0(written: rt.Future, other: rt.Future, make: (a0: Guard.Guard) => rt.Future, guard: Guard.Guard): rt.Future { + const made: (a0: rt.u32) => rt.Future = (n: rt.u32): rt.Future => { + return make(Guard.new(rt.u32Add(Guard.id(guard), n), Log.clone$Clone(Guard.log(guard)))); + }; + put$future_PollFn_futures_owning_closure0(written, made(20)); + const old: rt.Future = rt.replaceIn(written, made(30)); + rt.ptrWrite(written, rt.replaceIn(other, written)); + rt.cellSet(rt.cellIn(other), made(50), rt.drop); + return old; +} + /** Polls `future`, sized or not, as kio's `Waiter::poll_future` does. */ export function pollUnsized$dyn_Future_u64(future: rt.Future, cx: rt.Context): rt.Poll { return future.poll(cx); diff --git a/ts/fixtures/task/src/futures.rs b/ts/fixtures/task/src/futures.rs index 58a417330..c84bdcf1c 100644 --- a/ts/fixtures/task/src/futures.rs +++ b/ts/fixtures/task/src/futures.rs @@ -5,7 +5,7 @@ use std::cell::Cell; use std::future::{self, Future}; use std::pin::{Pin, pin}; -use std::ptr; +use std::{mem, ptr}; use std::sync::Arc; use std::task::{Context, Poll, RawWakerVTable, Wake, Waker}; @@ -208,7 +208,8 @@ type Boxed<'a> = Pin + 'a>>; /// Boxed futures over a channel: `Box::pin` of an async fn, awaited, and of a `&mut` to a /// `poll_fn` and to a `&mut` to it, whose box drops without dropping it, and a `&mut dyn -/// Future` of a `&mut` to it, then a queue of +/// Future` of a `&mut` to it; `poll_fn`s written whole through a `&mut`, boxed or not, by +/// each way std moves one, each written one's guard dropping first; then a queue of /// `Pin>`s of an async block, kio's `Pending`, a struct of the crate's, /// and a `poll_fn`, each owning a guard. A `poll_fn` boxed in a [`Forward`] polls the /// queue in turn through [`poll_unsized`], as moq's tasks do through kio's @@ -234,6 +235,13 @@ pub async fn boxed(mut rx: Consumer, guard: Guard) -> u64 { let lent = lent + direct.await + (&mut lend).await; guard.note(&format!("lent {lent}")); drop(lend); + let mut written = owning(Guard::new(id + 10, log.clone())); + let mut other = owning(Guard::new(id + 40, log.clone())); + let old = rewrite(&mut written, &mut other, owning, &guard); + let mut boxed = Box::pin(owning(Guard::new(id + 60, log.clone()))); + put(&mut boxed, Box::pin(owning(Guard::new(id + 70, log.clone())))); + let moved = written.await + old.await + other.await + boxed.await; + guard.note(&format!("moved {moved}")); let (block, waiting, ready) = ( Guard::new(id + 500, log.clone()), Guard::new(id + 600, log.clone()), @@ -284,6 +292,28 @@ pub async fn boxed(mut rx: Consumer, guard: Guard) -> u64 { total } +/// A `poll_fn` that owns `guard`, ready with its id. +fn owning(guard: Guard) -> future::PollFn) -> Poll> { + future::poll_fn(move |_| Poll::Ready(u64::from(guard.id()))) +} + +/// `written` and `other` written whole through their `&mut`s with `poll_fn`s `make` makes +/// of guards past `guard`'s id: by assignment, `mem::replace`, whose old one it returns, +/// `mem::swap`, and `Cell::set`. +fn rewrite( + written: &mut future::PollFn, + other: &mut future::PollFn, + make: fn(Guard) -> future::PollFn, + guard: &Guard, +) -> future::PollFn { + let made = |n: u32| make(Guard::new(guard.id() + n, guard.log().clone())); + put(written, made(20)); + let old = mem::replace(written, made(30)); + mem::swap(written, other); + Cell::from_mut(other).set(made(50)); + old +} + /// Polls `future`, sized or not, as kio's `Waiter::poll_future` does. fn poll_unsized(future: Pin<&mut F>, cx: &mut Context<'_>) -> Poll { future.poll(cx) diff --git a/ts/runtime/rt.ts b/ts/runtime/rt.ts index d416cb6e7..0adb36f5b 100644 --- a/ts/runtime/rt.ts +++ b/ts/runtime/rt.ts @@ -9,7 +9,7 @@ import type { Ordering } from "./rt/collections.ts"; import type { ParseFloatError } from "./rt/float.ts"; import { type char, type ParseCharError, type ParseIntError, type TryFromIntError, type u32, type usize, u8Debug, usizeDebug, usizeDisplay } from "./rt/int.ts"; import type { Iter, Range, RangeInclusive } from "./rt/iter.ts"; -import { drop as dispose, dropAll, type Pending } from "./rt/task.ts"; +import { drop as dispose, dropAll, MOVE, type Moved, type Pending } from "./rt/task.ts"; import { AtIndex } from "./rt/collections.ts"; import { UNPRINTABLE } from "./rt/unicode.ts"; @@ -417,12 +417,23 @@ export function ptrByteSlice(p: Place, len: number, shared: boolean): By return new ByteSlice(Uint8Array.from({ length: len }, (_, i) => at.array[at.index + i]), 0, len); } +function moved(p: object): p is Moved { + return MOVE in p; +} + +/** A new object holding what the object `p` holds, one object deep. */ +export function objectCopy(p: T): T { + if (moved(p)) return p[MOVE](); + return Object.assign(Object.create(Object.getPrototypeOf(p)) as T, p); +} + /** * `ptr::write` of `value` through a pointer to an object: `value` copied into it, one - * object deep, so every pointer to it sees the write. + * object deep, or moved by `MOVE`, so every pointer to it sees the write. */ export function ptrWrite(p: T, value: T): void { if (ArrayBuffer.isView(p)) (p as unknown as Uint8Array).set(value as unknown as Uint8Array); + else if (moved(value)) value[MOVE](p as T & Moved); else Object.assign(p, value); } @@ -437,8 +448,8 @@ export function replaceIn(p: T, value: T): T { vecAssign(p, value as unknown[]); return old as T; } - const old = Object.assign(Object.create(Object.getPrototypeOf(p)) as T, p); - Object.assign(p, value); + const old = objectCopy(p); + ptrWrite(p, value); return old; } diff --git a/ts/runtime/rt/task.ts b/ts/runtime/rt/task.ts index a1672dafb..fe73b0335 100644 --- a/ts/runtime/rt/task.ts +++ b/ts/runtime/rt/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, type ByteSlice, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtPad, fmtStr, type None, OK, Ok, Panic, panic, type Place, place, ptrWrite, type Result, stringAsBytes, vecAssign } from "../rt.ts"; +import { Abort, type ByteSlice, type Debug, type DebugStruct, debugStructField, debugStructFinish, debugStructFinishNonExhaustive, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtBool, fmtDebugStruct, fmtDebugTuple, fmtPad, fmtStr, type None, objectCopy, OK, Ok, Panic, panic, type Place, place, ptrWrite, type Result, stringAsBytes, vecAssign } from "../rt.ts"; // Drops @@ -825,7 +825,7 @@ export function cellOfPlace(p: Place): Cell { export function cellIn(p: T): Cell { const copy = (): T => { if (ArrayBuffer.isView(p) || Array.isArray(p)) return (p as unknown as T[]).slice() as T; - return Object.assign(Object.create(Object.getPrototypeOf(p)) as T, p); + return objectCopy(p); }; const write = (value: T): void => { if (Array.isArray(p)) vecAssign(p, value as unknown[]); @@ -1206,6 +1206,18 @@ export function onceIsPoisoned(_state: OnceState): boolean { return false; } +/** + * The method that moves the state of an object whose class keeps it private, where + * copying its fields can't reach: into `to`, or else into a new object of its class, + * which it returns. Defined here, as its classes' keys need it before `rt.ts` runs. + */ +export const MOVE: unique symbol = /* @__PURE__ */ Symbol("move"); + +/** An object whose class keeps its state private, which `MOVE` moves. */ +export interface Moved { + [MOVE](to?: this): this; +} + /** * `LazyCell`: the function that makes its value until the first `force` runs it, once, * and then that value. Forced from within that function, it panics as std's does. @@ -1220,6 +1232,15 @@ export class LazyCell { this.#f = f; } + /** Moves its state into `to`, or into a new cell of its class. */ + [MOVE](to?: this): this { + const into = to ?? new (this.constructor as new (f: (() => T) | undefined) => this)(this.#f); + into.#f = this.#f; + into.#value = this.#value; + into.#init = this.#init; + return into; + } + /** `force` and `Deref::deref`: the value, which the function makes the first time. */ force(): T { if (this.#init) return this.#value as T; @@ -1405,14 +1426,22 @@ export interface Future extends Disposable { * drops at all: a borrowed `f` does not. */ export class PollFn implements Future { - readonly #f: (cx: Context) => Poll; - readonly #drop: ((f: (cx: Context) => Poll) => void) | undefined; + #f: (cx: Context) => Poll; + #drop: ((f: (cx: Context) => Poll) => void) | undefined; constructor(f: (cx: Context) => Poll, drop?: (f: (cx: Context) => Poll) => void) { this.#f = f; this.#drop = drop; } + /** Moves its function into `to`, or into a new future. */ + [MOVE](to?: this): this { + if (to === undefined) return new PollFn(this.#f, this.#drop) as this; + to.#f = this.#f; + to.#drop = this.#drop; + return to; + } + poll(cx: Context): Poll { return this.#f(cx); } diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index 30afde223..c91567962 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -772,6 +772,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { Some(ObjectKind::Array) if self.b.t.erased(pointee).is_slice() => { Expr::rt("vecAssign", vec![target, value]) } + Some(_) if self.b.t.moves_private(pointee) => Expr::rt("ptrWrite", vec![target, value]), Some(_) => Expr::ident("Object").member("assign").call(vec![target, value]), None => self.err(span, format!("an assignment through a reference to `{pointee}`")), }, @@ -1297,12 +1298,13 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { } } - /// The `const` item of the thread-local `c` borrows, as `with`'s `&'static self` is: a - /// promoted borrow of the key. + /// The `const` item of the thread-local `c` is, or borrows, as `with`'s `&'static self` + /// is: a promoted borrow of the key. Each copy of a key names the same storage. fn local_key_of(&self, c: &mir::ConstOperand<'tcx>) -> Option { let tcx = self.tcx; - let ty::Ref(_, key, _) = c.ty().kind() else { - return None; + let key = match c.ty().kind() { + ty::Ref(_, key, _) => *key, + _ => c.ty(), }; let ty::Adt(adt, _) = key.kind() else { return None; @@ -1313,7 +1315,10 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let mir::Const::Unevaluated(uv, _) = c.const_ else { return None; }; - let promoted = &tcx.promoted_mir(uv.def)[uv.promoted?]; + let Some(promoted) = uv.promoted else { + return (key == c.ty()).then_some(uv.def); + }; + let promoted = &tcx.promoted_mir(uv.def)[promoted]; promoted .basic_blocks .iter() diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index b47e37ee3..d3ccced32 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -1333,13 +1333,26 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// Whether a JS value of `ty` is a copy that no JS value can point at, such as a number /// or a reference, or an object its clones may share, as an `Arc`'s and a `Waker`'s are, /// pinned or boxed too, which JS erases, so a `&mut` to it is an `rt.Place`: where the - /// value is, read and written through `v`. + /// value is, read and written through `v`. A `LocalKey`'s copies share its storage too. pub fn needs_place(&self, ty: Ty<'tcx>) -> bool { use library::Std; if self.object_kind(ty).is_none() { return true; } - matches!(self.std_of(self.erased(ty)), Some(Std::Arc | Std::Waker)) + matches!( + self.std_of(self.erased(ty)), + Some(Std::Arc | Std::Waker | Std::LocalKey) + ) + } + + /// Whether `ty`'s JS value, through the `Pin`s and `Box`es JS erases, is of an `rt` class + /// that keeps its state private, which `rt.ptrWrite` moves in by `rt.MOVE`. + pub fn moves_private(&self, ty: Ty<'tcx>) -> bool { + use library::Std; + matches!( + self.std_of(self.erased(ty)), + Some(Std::PollFn | Std::LazyCell | Std::LazyLock) + ) } /// What JS keeps of a `ty`: what it holds through any number of `Pin`s and `Box`es. @@ -1356,20 +1369,14 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// The kind of a `pointee`'s object, through the `Pin`s and `Box`es JS erases, where a /// whole value of it can be written into the object in place: one only its owner - /// holds, unlike an `Arc`'s, which its clones share, and whose parts `Object.assign` - /// copies, unlike a binding's, the private fields of an `rt.PollFn`, an `rt.LazyCell`, - /// an `rt.LocalKey`, or a future, or a trait object's, whatever class it is. + /// holds, unlike an `Arc`'s or a `LocalKey`'s, which its copies share, and whose state + /// a write reaches, unlike a binding's, or a future's, or a trait object's, whatever + /// class it is. pub fn write_in(&self, pointee: Ty<'tcx>) -> Option { use library::Std; let erased = self.erased(pointee); let hidden = match erased.kind() { - ty::Adt(adt, _) => { - self.bound_drops(adt.did()).is_some() - || matches!( - Std::of(self.tcx, adt.did()), - Some(Std::PollFn | Std::LazyCell | Std::LazyLock | Std::LocalKey) - ) - } + ty::Adt(adt, _) => self.bound_drops(adt.did()).is_some(), ty::Dynamic(..) | ty::Closure(..) => true, // A `Uint8Array`'s length never changes, as a boxed slice's may. ty::Slice(elem) => matches!(elem.kind(), ty::Uint(ty::UintTy::U8)), @@ -1379,7 +1386,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { let shared = match erased.kind() { ty::Adt(adt, _) => matches!( Std::of(self.tcx, adt.did()), - Some(Std::Arc | Std::Waker | Std::Context | Std::MutexGuard | Std::Ref | Std::RefMut) + Some(Std::Arc | Std::Waker | Std::LocalKey | Std::Context | Std::MutexGuard | Std::Ref | Std::RefMut) ), ty::Ref(..) | ty::Coroutine(..) => true, _ => false, From 41a565aa2b8957f79c09c832cddabbba5dfe3738 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:34:39 -0800 Subject: [PATCH 3/4] style: sort futures fixture imports Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/task/src/futures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/fixtures/task/src/futures.rs b/ts/fixtures/task/src/futures.rs index c84bdcf1c..f149439e8 100644 --- a/ts/fixtures/task/src/futures.rs +++ b/ts/fixtures/task/src/futures.rs @@ -5,9 +5,9 @@ use std::cell::Cell; use std::future::{self, Future}; use std::pin::{Pin, pin}; -use std::{mem, ptr}; use std::sync::Arc; use std::task::{Context, Poll, RawWakerVTable, Wake, Waker}; +use std::{mem, ptr}; use kio::Consumer; From 08bc4d583cf04593cb1b27306926e723a42aeaf8 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:38:55 -0800 Subject: [PATCH 4/4] fix(ts): name the runtime's move so bundlers still drop unused classes Co-Authored-By: Claude Opus 5.5 --- ts/runtime/rt.ts | 19 ++++++++++++++----- ts/runtime/rt/task.ts | 16 ++-------------- ts/src/lower/mod.rs | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/ts/runtime/rt.ts b/ts/runtime/rt.ts index 0adb36f5b..14c8993cf 100644 --- a/ts/runtime/rt.ts +++ b/ts/runtime/rt.ts @@ -9,7 +9,7 @@ import type { Ordering } from "./rt/collections.ts"; import type { ParseFloatError } from "./rt/float.ts"; import { type char, type ParseCharError, type ParseIntError, type TryFromIntError, type u32, type usize, u8Debug, usizeDebug, usizeDisplay } from "./rt/int.ts"; import type { Iter, Range, RangeInclusive } from "./rt/iter.ts"; -import { drop as dispose, dropAll, MOVE, type Moved, type Pending } from "./rt/task.ts"; +import { drop as dispose, dropAll, type Pending } from "./rt/task.ts"; import { AtIndex } from "./rt/collections.ts"; import { UNPRINTABLE } from "./rt/unicode.ts"; @@ -417,23 +417,32 @@ export function ptrByteSlice(p: Place, len: number, shared: boolean): By return new ByteSlice(Uint8Array.from({ length: len }, (_, i) => at.array[at.index + i]), 0, len); } +/** + * An object whose class keeps its state private, where copying its fields can't reach, + * so `$move`, which no Rust field can be named, moves it: into `to`, or else into a new + * object of its class, which it returns. + */ +interface Moved { + $move(to?: this): this; +} + function moved(p: object): p is Moved { - return MOVE in p; + return "$move" in p; } /** A new object holding what the object `p` holds, one object deep. */ export function objectCopy(p: T): T { - if (moved(p)) return p[MOVE](); + if (moved(p)) return p.$move(); return Object.assign(Object.create(Object.getPrototypeOf(p)) as T, p); } /** * `ptr::write` of `value` through a pointer to an object: `value` copied into it, one - * object deep, or moved by `MOVE`, so every pointer to it sees the write. + * object deep, or moved by `$move`, so every pointer to it sees the write. */ export function ptrWrite(p: T, value: T): void { if (ArrayBuffer.isView(p)) (p as unknown as Uint8Array).set(value as unknown as Uint8Array); - else if (moved(value)) value[MOVE](p as T & Moved); + else if (moved(value)) value.$move(p as T & Moved); else Object.assign(p, value); } diff --git a/ts/runtime/rt/task.ts b/ts/runtime/rt/task.ts index fe73b0335..a77db5c28 100644 --- a/ts/runtime/rt/task.ts +++ b/ts/runtime/rt/task.ts @@ -1206,18 +1206,6 @@ export function onceIsPoisoned(_state: OnceState): boolean { return false; } -/** - * The method that moves the state of an object whose class keeps it private, where - * copying its fields can't reach: into `to`, or else into a new object of its class, - * which it returns. Defined here, as its classes' keys need it before `rt.ts` runs. - */ -export const MOVE: unique symbol = /* @__PURE__ */ Symbol("move"); - -/** An object whose class keeps its state private, which `MOVE` moves. */ -export interface Moved { - [MOVE](to?: this): this; -} - /** * `LazyCell`: the function that makes its value until the first `force` runs it, once, * and then that value. Forced from within that function, it panics as std's does. @@ -1233,7 +1221,7 @@ export class LazyCell { } /** Moves its state into `to`, or into a new cell of its class. */ - [MOVE](to?: this): this { + $move(to?: this): this { const into = to ?? new (this.constructor as new (f: (() => T) | undefined) => this)(this.#f); into.#f = this.#f; into.#value = this.#value; @@ -1435,7 +1423,7 @@ export class PollFn implements Future { } /** Moves its function into `to`, or into a new future. */ - [MOVE](to?: this): this { + $move(to?: this): this { if (to === undefined) return new PollFn(this.#f, this.#drop) as this; to.#f = this.#f; to.#drop = this.#drop; diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index d3ccced32..898c950de 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -1346,7 +1346,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } /// Whether `ty`'s JS value, through the `Pin`s and `Box`es JS erases, is of an `rt` class - /// that keeps its state private, which `rt.ptrWrite` moves in by `rt.MOVE`. + /// that keeps its state private, which `rt.ptrWrite` moves in by its `$move`. pub fn moves_private(&self, ty: Ty<'tcx>) -> bool { use library::Std; matches!(