From 5f85158050a4ebc8792309aef3131b4057f3fa71 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:08:57 -0700 Subject: [PATCH 1/8] chore(quest): claim quest/m2/ts-async.md Quest-Claim: b03e7e55f46ed499 From c4df523e4684425ff3f3a5882a025fc6b1d34374 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:31:00 -0700 Subject: [PATCH 2/8] wip(ts): kio across crates, Poll> Co-Authored-By: Claude Opus 5.5 --- ts/Cargo.lock | 7 + ts/README.md | 12 +- ts/fixtures/reject/src/lib.rs | 14 +- ts/fixtures/reject/ts.toml | 1 + ts/fixtures/task/Cargo.toml | 3 + ts/fixtures/task/expected/core/task/poll.ts | 8 + ts/fixtures/task/expected/kio.Closed.ts | 7 + ts/fixtures/task/expected/kio.Consumer.ts | 34 ++ ts/fixtures/task/expected/kio.Park.ts | 31 +- ts/fixtures/task/expected/kio.Pending.ts | 65 +++- ts/fixtures/task/expected/kio.Waiter.ts | 28 +- ts/fixtures/task/expected/kio.WaiterList.ts | 31 +- ts/fixtures/task/expected/kio.ts | 99 +++--- ts/fixtures/task/expected/model.Watch.ts | 27 ++ ts/fixtures/task/expected/model.ts | 304 +++++++++++++++++- ts/fixtures/task/kio/Cargo.toml | 13 + .../task/{src/kio.rs => kio/src/lib.rs} | 109 ++++++- ts/fixtures/task/src/drops.rs | 2 +- ts/fixtures/task/src/futures.rs | 2 +- ts/fixtures/task/src/harness.rs | 6 + ts/fixtures/task/src/lib.rs | 9 +- ts/fixtures/task/src/model.rs | 63 +++- ts/fixtures/task/ts.toml | 1 + ts/runtime/rt/task.ts | 6 + ts/src/census.rs | 2 + ts/src/lower/copy.rs | 2 +- ts/src/lower/mir/place.rs | 8 +- ts/src/lower/mir/rvalue.rs | 2 +- ts/src/lower/mod.rs | 119 +++++-- ts/tests/fixtures.rs | 4 +- ts/tests/task.ts | 14 + 31 files changed, 835 insertions(+), 198 deletions(-) create mode 100644 ts/fixtures/task/expected/kio.Closed.ts create mode 100644 ts/fixtures/task/expected/model.Watch.ts create mode 100644 ts/fixtures/task/kio/Cargo.toml rename ts/fixtures/task/{src/kio.rs => kio/src/lib.rs} (70%) diff --git a/ts/Cargo.lock b/ts/Cargo.lock index 18f7f76ed..02fdcf6e9 100644 --- a/ts/Cargo.lock +++ b/ts/Cargo.lock @@ -256,6 +256,13 @@ dependencies = [ [[package]] name = "task-fixture" version = "0.0.0" +dependencies = [ + "task-kio", +] + +[[package]] +name = "task-kio" +version = "0.0.0" [[package]] name = "thiserror" diff --git a/ts/README.md b/ts/README.md index a173645e3..03cd6b539 100644 --- a/ts/README.md +++ b/ts/README.md @@ -106,7 +106,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `OnceCell`, `OnceLock` | `rt.OnceCell` and `rt.OnceLock`, holding the `Option` in `v`; a `OnceLock` initialized from within its own initialization panics, as wasm32's does | | `AtomicU32` and the others of `bool` and integers | `rt.Atomic`, the `rt.Place` of its value; an `atomic::Ordering` is its discriminant, and one an operation can't take panics as std's does | | `async fn` | a function returning `rt.Future`: a class of its own, its coroutine's state machine | -| `Poll`, `Waker`, `Context` | `T \| typeof rt.Pending`, refused if `T` can already be pending; `rt.Waker`, which is also the context | +| `Poll`, `Waker`, `Context` | `T \| typeof rt.Pending`; `T \| rt.OuterPending` where `T` can already be pending, as `Poll>` can, so `Ready(Pending)` keeps apart from `Pending`; `rt.Waker`, which is also the context | | `pin!(x)`, `poll_fn(f)`, `future::pending()` | `x`; `rt.PollFn`, which drops `f` with itself; `rt.NEVER` | | `Box::pin(x)`, `dyn Future` | `x`; `rt.Future`: an `async` body or std's future as it is, and another future as an `rt.DynFuture` over its `poll`, which drops it with itself unless borrowed | @@ -481,7 +481,7 @@ they need. `Box`, or `Arc` ([trait objects](/quest/m3/ts-dyn.md)), fail translation. Until they translate, an item that needs one, such as a boxed closure, binds to hand-written TS ([bindings](#bindings)). -These fail translation: formatting and parsing an `f64`, and `f64::clamp`, whose panic formats them ([float formatting](/quest/m2/ts-model/floats.md)), `f32` ([`f32`](/quest/m3/ts-f32.md)), statics, async closures, a closure that owns a value +These fail translation: formatting and parsing an `f64`, and `f64::clamp`, whose panic formats them ([float formatting](/quest/m2/ts-model/floats.md)), `f32` ([`f32`](/quest/m3/ts-f32.md)), statics, async closures, a `Poll` of a `Poll` of a `Poll`, a future whose output can be pending, an instance of a generic type whose field is a `Poll` of a type parameter that can be pending, a closure that owns a value that drops given to a std function that drops it beside another value that drops, as `for_each` of items that drop does, or keeps it other than in an iterator adaptor, and values that drop in some iterators ([drop order](/quest/m3/ts-drop-order.md)), a `Cow` of other than `str`, @@ -547,9 +547,11 @@ built natively for wasm32 and run beside its translation on the same inputs by `lang`'s other writes into the same type would hide a write the analysis misses: a byte array overwritten whole through a type parameter, or, under its features, by std called directly or through a pointer a generic function calls. -- `task` trims moq's kio to a channel of `Arc`ed state, waiters, and wakers, under async - functions and std's future helpers, with wakers of the crate's `Wake` impls, beside - values that drop inside std's types. Its caller is hand-written TS (`tests/task.ts`): a FIFO executor that runs a +- `task` trims moq's kio to a dependency of its own, `kio`, reached across crates as moq-net + reaches kio: a channel of `Arc`ed state, waiters, and wakers, kio's `Task`, `Pending`, and + `Waiter::poll_future`, and a `Consumer::poll` whose closure is itself pending, a + `Poll>`. Async functions and std's future helpers run over it, with wakers of + the crate's `Wake` impls, beside values that drop inside std's types. Its caller is hand-written TS (`tests/task.ts`): a FIFO executor that runs a script of spawns, sends, polls, and drops, and logs every poll, wake, and drop, as the fixture's native executor does. The logs must match byte for byte. `tests/unwind.ts` also runs the scripts natively on the host (`examples/unwind.rs`), where a panic diff --git a/ts/fixtures/reject/src/lib.rs b/ts/fixtures/reject/src/lib.rs index 20e169cb1..0544f6a47 100644 --- a/ts/fixtures/reject/src/lib.rs +++ b/ts/fixtures/reject/src/lib.rs @@ -101,11 +101,21 @@ pub fn replace_shared(dst: &mut std::sync::Arc, src: std::sync::Arc) { *dst = src; } -/// `Ready(Some(Pending))` would be `rt.Pending`, as the outer `Pending` is. -pub fn nested_poll(p: std::task::Poll>>) -> bool { +/// `Ready(Some(Ready(Pending)))` would be `rt.OuterPending`, as the outer `Pending` is. +pub fn nested_poll(p: std::task::Poll>>>) -> bool { matches!(p, std::task::Poll::Pending) } +/// A generic type's `Poll` is `rt.Poll`, which can't hold `rt.OuterPending`, the +/// `Pending` of a `Poll>`. +pub struct Waiting { + pub poll: std::task::Poll, +} + +pub fn generic_pending(poll: std::task::Poll>) -> bool { + Waiting { poll }.poll.is_pending() +} + /// A future's `Ready(Pending)` would be `rt.Pending`, as its own pending is. pub async fn pending_output() -> std::task::Poll { std::task::Poll::Pending diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 8e1ec8273..82c40a5bf 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -18,6 +18,7 @@ roots = [ "generic", "replace_shared", "nested_poll", + "generic_pending", "pending_output", "replace_pinned", "replace_guard", diff --git a/ts/fixtures/task/Cargo.toml b/ts/fixtures/task/Cargo.toml index b58b1039f..86a514415 100644 --- a/ts/fixtures/task/Cargo.toml +++ b/ts/fixtures/task/Cargo.toml @@ -14,5 +14,8 @@ crate-type = ["lib", "cdylib"] # The wasm32 build exports the benchmark's handoff in place of the harness. bench = [] +[dependencies] +kio = { package = "task-kio", path = "kio" } + [lints] workspace = true diff --git a/ts/fixtures/task/expected/core/task/poll.ts b/ts/fixtures/task/expected/core/task/poll.ts index c810551bb..b52a002e6 100644 --- a/ts/fixtures/task/expected/core/task/poll.ts +++ b/ts/fixtures/task/expected/core/task/poll.ts @@ -1,6 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "../../rt.ts"; +import type * as Closed from "../../kio.Closed.ts"; export function map$Poll_Option_u64_Result_u64_u64_helpers_closure0_closure1_closure0(self: rt.Poll, f: (a0: rt.u64 | undefined) => rt.Result): rt.Poll> { if (self !== rt.Pending) { @@ -30,6 +31,13 @@ export function isReady$Poll_Option_u64(self: rt.Poll): bool return false; } +export function isReady$Poll_Result_unit_Closed(self: rt.Poll>): boolean { + if (self !== rt.Pending) { + return true; + } + return false; +} + export function isReady$Poll_u64(self: rt.Poll): boolean { if (self !== rt.Pending) { return true; diff --git a/ts/fixtures/task/expected/kio.Closed.ts b/ts/fixtures/task/expected/kio.Closed.ts new file mode 100644 index 000000000..3fdebbfcd --- /dev/null +++ b/ts/fixtures/task/expected/kio.Closed.ts @@ -0,0 +1,7 @@ +// Generated by rust2-ts. Do not edit. + +/** The error of a poll once the channel closed. */ +export interface Closed { +} + +//# sourceMappingURL=kio.Closed.ts.map diff --git a/ts/fixtures/task/expected/kio.Consumer.ts b/ts/fixtures/task/expected/kio.Consumer.ts index 24ea321e7..46d3436be 100644 --- a/ts/fixtures/task/expected/kio.Consumer.ts +++ b/ts/fixtures/task/expected/kio.Consumer.ts @@ -1,6 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; +import type * as Closed from "./kio.Closed.ts"; import type * as State from "./kio.State.ts"; import * as Waiter from "./kio.Waiter.ts"; @@ -39,4 +40,37 @@ export function pollRecv(self: Consumer, waiter: Waiter.Waiter): rt.Poll rt.Poll | rt.OuterPending): rt.Poll, Closed.Closed>> { + const state: rt.Mutex = self.state.value; + const _8: rt.Poll | rt.OuterPending = f(state.value.queue); + if (_8 !== rt.OuterPending) { + return rt.Ok(_8); + } + if (!state.value.closed) { + Waiter.register(waiter, state.value.waiters); + return rt.Pending; + } + return rt.Err({}); +} + +/** + * `f` of every value sent so far, once it is ready, or the error once the channel + * closed and `f` is not, registering `waiter` otherwise. + */ +export function poll$at_closure1_unit(self: Consumer, waiter: Waiter.Waiter, f: (a0: rt.u64[]) => rt.Poll): rt.Poll> { + const state: rt.Mutex = self.state.value; + if (f(state.value.queue) !== rt.Pending) { + return rt.OK; + } + if (!state.value.closed) { + Waiter.register(waiter, state.value.waiters); + return rt.Pending; + } + return rt.Err({}); +} + //# sourceMappingURL=kio.Consumer.ts.map diff --git a/ts/fixtures/task/expected/kio.Park.ts b/ts/fixtures/task/expected/kio.Park.ts index 9cd651cc7..30d2f0410 100644 --- a/ts/fixtures/task/expected/kio.Park.ts +++ b/ts/fixtures/task/expected/kio.Park.ts @@ -26,30 +26,17 @@ export class Park { */ export function hold(self: Park, cx: rt.Context): Waiter.Waiter { let reuse!: boolean; - let _10!: Waiter.Waiter | undefined; - let unwind: number = 0; - try { - if (self._0 === undefined) { - reuse = false; - } else { - reuse = cx.willWake(self._0.waker); - } - if (!reuse) { - const _11: Waiter.Waiter = Waiter.new(cx.clone()); - unwind = 12; - _10 = _11; - rt.drop(self._0); - self._0 = _10; - } - unwind = 0; - return rt.optionUnwrap(self._0); - } catch (e) { - if (unwind === 0) { - throw e; - } + if (self._0 === undefined) { + reuse = false; + } else { + reuse = cx.willWake(self._0.waker); + } + if (!reuse) { + const _10: Waiter.Waiter | undefined = Waiter.new(cx.clone()); + rt.drop(self._0); self._0 = _10; - throw e; } + return rt.optionUnwrap(self._0); } //# sourceMappingURL=kio.Park.ts.map diff --git a/ts/fixtures/task/expected/kio.Pending.ts b/ts/fixtures/task/expected/kio.Pending.ts index 9ac1bae2f..90479a2b9 100644 --- a/ts/fixtures/task/expected/kio.Pending.ts +++ b/ts/fixtures/task/expected/kio.Pending.ts @@ -1,23 +1,25 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; +import { poll$Task_boxed_closure0_closure3, poll$Task_boxed_closure0_closure5, poll$Task_recv_closure0_closure0, poll$Task_watch_closure0_closure0 } from "./kio.ts"; import * as Park from "./kio.Park.ts"; import type * as Waiter from "./kio.Waiter.ts"; +import * as Watch from "./model.Watch.ts"; -/** What [`wait`] returns. */ -export class Pending { - poll: F; +/** A [`Task`] as a [`Future`], parking its waiter between polls. Derefs to the task. */ +export class Pending

{ + inner: P; park: Park.Park; - constructor(fields: { poll: F; park: Park.Park }) { - this.poll = fields.poll; + constructor(fields: { inner: P; park: Park.Park }) { + this.inner = fields.inner; this.park = fields.park; } [Symbol.dispose](): void { let dropped: number = 0; try { - rt.drop(this.poll); + rt.drop(this.inner); dropped = 1; rt.drop(this.park); } catch (e) { @@ -29,19 +31,64 @@ export class Pending { } } +export function new$Watch(inner: Watch.Watch): Pending { + return new Pending({ inner, park: Park.default$Default() }); +} + +export function new$boxed_closure0_closure3(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { + return new Pending({ inner, park: Park.default$Default() }); +} + +export function new$boxed_closure0_closure5(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { + return new Pending({ inner, park: Park.default$Default() }); +} + +export function new$recv_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { + return new Pending({ inner, park: Park.default$Default() }); +} + +export function new$watch_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { + return new Pending({ inner, park: Park.default$Default() }); +} + +export function intoInner$Watch(self: Pending): Watch.Watch { + const _0: Watch.Watch = self.inner; + rt.drop(self.park); + return _0; +} + +export function derefMut$DerefMut_Watch(self: Pending): Watch.Watch { + return self.inner; +} + +export function poll$Future_Watch(self: Pending, cx: rt.Context): rt.Poll { + const this_: Pending = self; + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return Watch.poll$Task(this_.inner, waiter); +} + export function poll$Future_boxed_closure0_closure3(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; - return this_.poll(Park.hold(this_.park, cx)); + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return poll$Task_boxed_closure0_closure3(this_.inner, waiter); } export function poll$Future_boxed_closure0_closure5(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; - return this_.poll(Park.hold(this_.park, cx)); + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return poll$Task_boxed_closure0_closure5(this_.inner, waiter); } export function poll$Future_recv_closure0_closure0(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; - return this_.poll(Park.hold(this_.park, cx)); + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return poll$Task_recv_closure0_closure0(this_.inner, waiter); +} + +export function poll$Future_watch_closure0_closure0(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { + const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return poll$Task_watch_closure0_closure0(this_.inner, waiter); } //# sourceMappingURL=kio.Pending.ts.map diff --git a/ts/fixtures/task/expected/kio.Waiter.ts b/ts/fixtures/task/expected/kio.Waiter.ts index 3e1eee364..1b79b94b0 100644 --- a/ts/fixtures/task/expected/kio.Waiter.ts +++ b/ts/fixtures/task/expected/kio.Waiter.ts @@ -29,18 +29,8 @@ export class Waiter { } function new_(waker: rt.Waker): Waiter { - let unwind: number = 0; - try { - unwind = 4; - const shared: rt.Arc = new rt.Arc(waker.clone()); - return new Waiter({ waker, shared }); - } catch (e) { - if (unwind === 0) { - throw e; - } - rt.drop(waker); - throw e; - } + const shared: rt.Arc = new rt.Arc(waker.clone()); + return new Waiter({ waker, shared }); } /** Register with `list`, a no-op while the list still holds this waiter. */ @@ -48,6 +38,20 @@ export function register(self: Waiter, list: WaiterList.WaiterList): void { WaiterList.register(list, self); } +/** A [`Context`] that wakes this waiter's task. */ +export function context(self: Waiter): rt.Context { + return self.waker; +} + +/** Poll a std [`Future`], sized or not, so it wakes this waiter's task. */ +export function pollFuture$dyn_Future_Option_u64(self: Waiter, future: rt.Future): rt.Poll { + const _3: rt.Poll = future.poll(context(self)); + if (_3 !== rt.Pending) { + return _3; + } + return rt.Pending; +} + export { new_ as new }; //# sourceMappingURL=kio.Waiter.ts.map diff --git a/ts/fixtures/task/expected/kio.WaiterList.ts b/ts/fixtures/task/expected/kio.WaiterList.ts index 92963af63..ae75b668b 100644 --- a/ts/fixtures/task/expected/kio.WaiterList.ts +++ b/ts/fixtures/task/expected/kio.WaiterList.ts @@ -6,7 +6,6 @@ import type * as Waiter from "./kio.Waiter.ts"; /** Wakers waiting for one event, held weakly. */ export interface WaiterList { entries: rt.Weak[]; - /** Where the next registration looks for a dead slot to reuse. */ cursor: rt.usize; } @@ -58,28 +57,16 @@ export function take(self: WaiterList): WaiterList { /** Wake every live waiter, draining the list. */ export function wake(self: WaiterList): void { - let waker!: rt.Arc; - let unwind: number = 0; - try { - self.cursor = 0; - const taken = self.entries; - self.entries = []; - for (const _6 of taken) { - const _10: rt.Arc | undefined = rt.weakUpgrade(_6); - if (_10 !== undefined) { - unwind = 15; - waker = _10; - waker.value.wakeByRef(); - unwind = 0; - rt.drop(waker); - } - } - } catch (e) { - if (unwind === 0) { - throw e; + self.cursor = 0; + const taken = self.entries; + self.entries = []; + for (const _6 of taken) { + const _10: rt.Arc | undefined = rt.weakUpgrade(_6); + if (_10 !== undefined) { + const waker: rt.Arc = _10; + waker.value.wakeByRef(); + rt.drop(waker); } - rt.drop(waker); - throw e; } } diff --git a/ts/fixtures/task/expected/kio.ts b/ts/fixtures/task/expected/kio.ts index 7d3bb89c4..8ab4bda85 100644 --- a/ts/fixtures/task/expected/kio.ts +++ b/ts/fixtures/task/expected/kio.ts @@ -2,47 +2,43 @@ import * as rt from "./rt.ts"; import * as Consumer from "./kio.Consumer.ts"; -import * as Park from "./kio.Park.ts"; import * as Pending from "./kio.Pending.ts"; import type * as Waiter from "./kio.Waiter.ts"; +export function poll$Task_boxed_closure0_closure3(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { + return self(waiter); +} + +export function poll$Task_boxed_closure0_closure5(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { + return self(waiter); +} + +export function poll$Task_recv_closure0_closure0(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { + return self(waiter); +} + +export function poll$Task_watch_closure0_closure0(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { + return self(waiter); +} + /** A future from a poll function that receives a [`Waiter`]. */ export function wait$boxed_closure0_closure3_u64(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { - let _2!: (a0: Waiter.Waiter) => rt.Poll; - let unwind: number = 0; - try { - unwind = 3; - _2 = poll; - return new Pending.Pending({ poll: _2, park: Park.default$Default() }); - } catch (e) { - if (unwind === 0) { - throw e; - } - rt.drop(_2); - throw e; - } + return Pending.new$boxed_closure0_closure3(poll); } /** A future from a poll function that receives a [`Waiter`]. */ export function wait$boxed_closure0_closure5_u64(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { - let _2!: (a0: Waiter.Waiter) => rt.Poll; - let unwind: number = 0; - try { - unwind = 3; - _2 = poll; - return new Pending.Pending({ poll: _2, park: Park.default$Default() }); - } catch (e) { - if (unwind === 0) { - throw e; - } - rt.drop(_2); - throw e; - } + return Pending.new$boxed_closure0_closure5(poll); } /** A future from a poll function that receives a [`Waiter`]. */ export function wait$recv_closure0_closure0_Option_u64(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { - return new Pending.Pending({ poll, park: Park.default$Default() }); + return Pending.new$recv_closure0_closure0(poll); +} + +/** A future from a poll function that receives a [`Waiter`]. */ +export function wait$watch_closure0_closure0_Option_u64(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { + return Pending.new$watch_closure0_closure0(poll); } export class recvFuture { @@ -61,39 +57,22 @@ export class recvFuture { } poll(_2: rt.Context): rt.Poll { - let unwind: number = 0; - try { - if (this.#state === 0) { - unwind = 12; - const self: Consumer.Consumer = this.#self; - this.#awaitee = wait$recv_closure0_closure0_Option_u64((waiter: Waiter.Waiter): rt.Poll => { - return Consumer.pollRecv(self, waiter); - }); - } else if (this.#state === 1) { - rt.asyncFnResumed(); - } else if (this.#state === 2) { - rt.asyncFnResumedPanic(); - } - unwind = 11; - const _7: rt.Poll = Pending.poll$Future_recv_closure0_closure0(this.#awaitee, _2); - if (_7 !== rt.Pending) { - unwind = 12; - rt.drop(this.#awaitee); - this.#state = 1; - return _7; - } - this.#state = 3; - return rt.Pending; - } catch (e) { - if (unwind === 0) { - throw e; - } - if (unwind === 11) { - rt.drop(this.#awaitee); - } - this.#state = 2; - throw e; + if (this.#state === 0) { + const self: Consumer.Consumer = this.#self; + this.#awaitee = wait$recv_closure0_closure0_Option_u64((waiter: Waiter.Waiter): rt.Poll => { + return Consumer.pollRecv(self, waiter); + }); + } else if (this.#state === 1) { + rt.asyncFnResumed(); + } + const _7: rt.Poll = Pending.poll$Future_recv_closure0_closure0(this.#awaitee, _2); + if (_7 !== rt.Pending) { + rt.drop(this.#awaitee); + this.#state = 1; + return _7; } + this.#state = 3; + return rt.Pending; } } diff --git a/ts/fixtures/task/expected/model.Watch.ts b/ts/fixtures/task/expected/model.Watch.ts new file mode 100644 index 000000000..e0f72ed80 --- /dev/null +++ b/ts/fixtures/task/expected/model.Watch.ts @@ -0,0 +1,27 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import type * as Consumer from "./kio.Consumer.ts"; +import type * as Waiter from "./kio.Waiter.ts"; +import { at } from "./model.ts"; + +/** The value at `index` of a channel, read by [`at`]. */ +export class Watch { + rx: Consumer.Consumer; + index: rt.usize; + + constructor(fields: { rx: Consumer.Consumer; index: rt.usize }) { + this.rx = fields.rx; + this.index = fields.index; + } + + [Symbol.dispose](): void { + rt.drop(this.rx); + } +} + +export function poll$Task(self: Watch, waiter: Waiter.Waiter): rt.Poll { + return at(self.rx, self.index, waiter); +} + +//# sourceMappingURL=model.Watch.ts.map diff --git a/ts/fixtures/task/expected/model.ts b/ts/fixtures/task/expected/model.ts index 9fac06669..3fca3f671 100644 --- a/ts/fixtures/task/expected/model.ts +++ b/ts/fixtures/task/expected/model.ts @@ -3,8 +3,12 @@ import * as rt from "./rt.ts"; import { copy$Tally, drop$ControlFlow_Guard_u32, drop$Result_u32_Guard } from "./lib.ts"; import { unwrapOr$Option_u64 } from "./core/option.ts"; -import { recvFuture } from "./kio.ts"; -import type * as Consumer from "./kio.Consumer.ts"; +import { isReady$Poll_Result_unit_Closed, isPending$Poll_Option_u64 } from "./core/task/poll.ts"; +import { wait$watch_closure0_closure0_Option_u64, recvFuture } from "./kio.ts"; +import type * as Closed from "./kio.Closed.ts"; +import * as Consumer from "./kio.Consumer.ts"; +import * as Pending from "./kio.Pending.ts"; +import * as Waiter from "./kio.Waiter.ts"; import * as Log from "./log.Log.ts"; import * as Countdown from "./model.Countdown.ts"; import * as Guard from "./model.Guard.ts"; @@ -12,6 +16,7 @@ import * as Inner from "./model.Inner.ts"; import type * as Mark from "./model.Mark.ts"; import * as Outer from "./model.Outer.ts"; import type * as Tally from "./model.Tally.ts"; +import * as Watch from "./model.Watch.ts"; /** * Every value until the channel closes, summed, holding a guard of its own across @@ -171,6 +176,301 @@ export class sumFuture { } } +/** + * Every value until the channel closes, summed, read as moq reads kio's channels: the + * first by `Waiter::poll_future` of a pinned `recv` as a `dyn Future`, under + * `kio::wait`, then each by a `Task` of the crate's that a `Pending` drives, reached + * through its `DerefMut` between awaits, which polls with [`at`]. + */ +export function watch(rx: Consumer.Consumer, guard: Guard.Guard): rt.Future { + return new watchFuture(rx, guard); +} + +export class watchFuture { + #state: number = 0; + #rx: Consumer.Consumer; + #guard: Guard.Guard; + #first!: rt.u64 | undefined; + #next!: rt.Future; + #pinned!: rt.Future; + #next$2!: rt.Future; + #awaitee!: Pending.Pending<(a0: Waiter.Waiter) => rt.Poll>; + #pending!: Pending.Pending; + #total!: rt.u64; + #awaitee$2!: Pending.Pending; + #saved!: boolean; + #saved$2!: boolean; + + constructor(rx: Consumer.Consumer, guard: Guard.Guard) { + this.#rx = rx; + this.#guard = guard; + } + + [Symbol.dispose](): void { + let unwind: number = 0; + try { + if (this.#state === 0) { + unwind = 28; + rt.drop(this.#rx); + unwind = 0; + rt.drop(this.#guard); + return; + } + if (this.#state === 3) { + unwind = 16; + rt.drop(this.#awaitee); + unwind = 18; + rt.drop(this.#pinned); + } else if (this.#state === 4) { + unwind = 15; + rt.drop(this.#pending); + this.#saved$2 = false; + } else { + return; + } + unwind = 20; + rt.drop(this.#guard); + if (this.#saved) { + unwind = 21; + rt.drop(this.#rx); + } + this.#saved = false; + } catch (e) { + b2: { + b3: { + b4: { + if (unwind === 0) { + throw e; + } + if (unwind === 15) { + this.#saved$2 = false; + } else if (unwind === 16) { + rt.drop(this.#pinned); + } else if (unwind !== 18) { + if (unwind === 20) { + break b4; + } + if (unwind === 21) { + break b3; + } + rt.drop(this.#guard); + break b2; + } + rt.drop(this.#guard); + } + if (this.#saved) { + rt.drop(this.#rx); + } + } + this.#saved = false; + } + throw e; + } + } + + poll(_2: rt.Context): rt.Poll { + let watch_!: Watch.Watch; + let unwind: number = 0; + try { + let bb0: number = 0; + while (true) { + switch (bb0) { + case 0: + if (this.#state === 0) { + unwind = 59; + this.#saved = false; + this.#saved$2 = false; + this.#saved = true; + const _3: rt.Future = new recvFuture(this.#rx); + unwind = 58; + this.#pinned = _3; + this.#next = this.#pinned; + this.#next$2 = this.#next; + this.#awaitee = wait$watch_closure0_closure0_Option_u64((waiter: Waiter.Waiter): rt.Poll => { + return Waiter.pollFuture$dyn_Future_Option_u64(waiter, this.#next$2); + }); + bb0 = 6; + continue; + } + if (this.#state === 1) { + rt.asyncFnResumed(); + } else if (this.#state === 2) { + rt.asyncFnResumedPanic(); + } else { + if (this.#state === 3) { + bb0 = 6; + continue; + } + bb0 = 24; + continue; + } + case 6: + unwind = 57; + const _12: rt.Poll = Pending.poll$Future_watch_closure0_closure0(this.#awaitee, _2); + if (_12 !== rt.Pending) { + unwind = 58; + this.#first = _12; + rt.drop(this.#awaitee); + unwind = 59; + rt.drop(this.#pinned); + const arg = this.#first; + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtOption(arg, f, rt.u64Debug)]; + const _24: string = rt.fmtFormat(["first ", args2[0]]); + unwind = 56; + Guard.note(this.#guard, _24); + unwind = 55; + this.#saved = false; + this.#pending = Pending.new$Watch(new Watch.Watch({ rx: this.#rx, index: 1 })); + unwind = 62; + this.#saved$2 = true; + this.#total = unwrapOr$Option_u64(this.#first, 0); + bb0 = 22; + continue; + } + this.#state = 3; + return rt.Pending; + case 22: + unwind = 62; + this.#awaitee$2 = this.#pending; + bb0 = 24; + continue; + case 24: + unwind = 62; + const _38: rt.Poll = Pending.poll$Future_Watch(this.#awaitee$2, _2); + if (_38 !== rt.Pending) { + const result2: rt.u64 | undefined = _38; + if (result2 !== undefined) { + const value: rt.u64 = result2; + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(value, f)]; + const _52: string = rt.fmtFormat(["watched ", args4[0]]); + unwind = 54; + Guard.note(this.#guard, _52); + unwind = 62; + this.#total = rt.u64Add(this.#total, value); + const _62: Watch.Watch = Pending.derefMut$DerefMut_Watch(this.#pending); + _62.index = rt.usizeAdd(_62.index, 1); + bb0 = 22; + continue; + } + this.#saved$2 = false; + watch_ = Pending.intoInner$Watch(this.#pending); + unwind = 53; + const arg2 = watch_.index; + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(arg2, f)]; + const _71: string = rt.fmtFormat(["watched to ", args6[0]]); + unwind = 52; + Guard.note(this.#guard, _71); + unwind = 62; + const _83: rt.u64 = this.#total; + rt.drop(watch_); + unwind = 64; + this.#saved$2 = false; + rt.drop(this.#guard); + this.#saved = false; + this.#state = 1; + return _83; + } + this.#state = 4; + return rt.Pending; + } + } + } catch (e) { + b1: { + b2: { + b3: { + b4: { + b5: { + if (unwind === 0) { + throw e; + } + if (unwind === 52) { + break b5; + } + if (unwind === 53) { + break b5; + } + if (unwind === 54) { + break b4; + } + if (unwind === 55) { + break b3; + } + if (unwind === 56) { + break b2; + } + if (unwind === 57) { + rt.drop(this.#awaitee); + } else if (unwind !== 58) { + if (unwind === 59) { + break b2; + } + if (unwind === 62) { + break b4; + } + break b1; + } + rt.drop(this.#pinned); + break b2; + } + rt.drop(watch_); + } + if (this.#saved$2) { + rt.drop(this.#pending); + } + } + this.#saved$2 = false; + } + rt.drop(this.#guard); + } + if (this.#saved) { + rt.drop(this.#rx); + } + this.#saved = false; + this.#state = 2; + throw e; + } + } +} + +/** + * The value at `index`, or `None` once the channel closed without it, as moq's + * `poll_group` reads its cache: kio's `Consumer::poll` with a closure pending itself, a + * `Poll>`, outside until the value arrives, and inside while it is even and the + * one after it hasn't: then it waits on the channel, and the close drops it. + */ +export function at(rx: Consumer.Consumer, index: rt.usize, waiter: Waiter.Waiter): rt.Poll { + let res2!: rt.Poll; + const res: rt.Poll, Closed.Closed>> = Consumer.poll$at_closure0_Poll_Option_u64(rx, waiter, (queue: rt.u64[]): rt.Poll | rt.OuterPending => { + const _3: rt.u64 | undefined = rt.sliceGet(queue, index); + if (_3 === undefined) { + return rt.OuterPending; + } + if (rt.u64Rem(_3, 2) === 0) { + if (queue.length === rt.usizeAdd(index, 1)) { + return rt.Pending; + } + } + return _3; + }); + if (res !== rt.Pending) { + if (res.ok) { + res2 = res.value; + } else { + res2 = undefined as rt.Poll; + } + } else { + res2 = rt.Pending as rt.Poll; + } + if (isPending$Poll_Option_u64(res2)) { + if (isReady$Poll_Result_unit_Closed(Consumer.poll$at_closure1_unit(rx, waiter, (_2: rt.u64[]): rt.Poll => { + return rt.Pending; + }))) { + return undefined; + } + } + return res2; +} + /** * One value read through the captures only an async block makes: a borrow, which * dropping the block must not drop; a copy, which a later change to the original must diff --git a/ts/fixtures/task/kio/Cargo.toml b/ts/fixtures/task/kio/Cargo.toml new file mode 100644 index 000000000..9ab51999e --- /dev/null +++ b/ts/fixtures/task/kio/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "task-kio" +description = "moq's kio, trimmed to what the task fixture's futures hinge on, which it reaches as a dependency" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false + +[lib] +name = "kio" + +[lints] +workspace = true diff --git a/ts/fixtures/task/src/kio.rs b/ts/fixtures/task/kio/src/lib.rs similarity index 70% rename from ts/fixtures/task/src/kio.rs rename to ts/fixtures/task/kio/src/lib.rs index f14620b99..2a65088cd 100644 --- a/ts/fixtures/task/src/kio.rs +++ b/ts/fixtures/task/kio/src/lib.rs @@ -1,5 +1,6 @@ //! moq's kio, trimmed to what its futures hinge on, at the moq revision `just ts moq` -//! pins (`rs/kio/src/waiter.rs`, `pending.rs`, `producer.rs`, and `consumer.rs`). +//! pins (`rs/kio/src/waiter.rs`, `pending.rs`, `task.rs`, `producer.rs`, and +//! `consumer.rs`): the task fixture reaches it across crates, as moq-net reaches kio. //! //! A `WaiterList` holds each waiter's waker weakly, so a waiter that drops, as its //! `Park` replaces it or its future is cancelled, frees its slots with no deregister. @@ -7,6 +8,7 @@ //! inline storage are left out: they change no event the test logs. use std::future::Future; +use std::ops::{Deref, DerefMut}; use std::pin::Pin; use std::sync::{Arc, Mutex, Weak}; use std::task::{Context, Poll, Waker}; @@ -27,6 +29,19 @@ impl Waiter { pub fn register(&self, list: &mut WaiterList) { list.register(self); } + + /// A [`Context`] that wakes this waiter's task. + pub fn context(&self) -> Context<'_> { + Context::from_waker(&self.waker) + } + + /// Poll a std [`Future`], sized or not, so it wakes this waiter's task. + pub fn poll_future(&self, future: Pin<&mut F>) -> Poll { + match future.poll(&mut self.context()) { + Poll::Ready(value) => Poll::Ready(value), + Poll::Pending => Poll::Pending, + } + } } /// Wakers waiting for one event, held weakly. @@ -114,36 +129,75 @@ impl Park { } } -/// A future from a poll function that receives a [`Waiter`]. -pub fn wait(poll: F) -> Pending +/// A computation polled with a [`Waiter`]. +pub trait Task { + type Output; + + fn poll(&mut self, waiter: &Waiter) -> Poll; +} + +impl Task for F where - F: FnMut(&Waiter) -> Poll + Unpin, + F: FnMut(&Waiter) -> Poll, { - Pending { - poll, - park: Park::default(), + type Output = R; + + fn poll(&mut self, waiter: &Waiter) -> Poll { + self(waiter) } } -/// What [`wait`] returns. -pub struct Pending { - poll: F, +/// A [`Task`] as a [`Future`], parking its waiter between polls. Derefs to the task. +pub struct Pending

{ + inner: P, park: Park, } -impl Future for Pending -where - F: FnMut(&Waiter) -> Poll + Unpin, -{ - type Output = R; +impl

Pending

{ + pub fn new(inner: P) -> Self { + Self { + inner, + park: Park::default(), + } + } + + pub fn into_inner(self) -> P { + self.inner + } +} + +impl

Deref for Pending

{ + type Target = P; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + fn deref(&self) -> &P { + &self.inner + } +} + +impl

DerefMut for Pending

{ + fn deref_mut(&mut self) -> &mut P { + &mut self.inner + } +} + +impl Future for Pending

{ + type Output = P::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = &mut *self; let waiter = this.park.hold(cx); - (this.poll)(waiter) + this.inner.poll(waiter) } } +/// A future from a poll function that receives a [`Waiter`]. +pub fn wait(poll: F) -> impl Future +where + F: FnMut(&Waiter) -> Poll + Unpin, +{ + Pending::new(poll) +} + struct State { queue: Vec, closed: bool, @@ -211,6 +265,10 @@ impl Drop for Producer { } } +/// The error of a poll once the channel closed. +#[derive(Debug)] +pub struct Closed; + /// The receiving side of a channel: every value sent, in order. #[derive(Clone)] pub struct Consumer { @@ -234,6 +292,23 @@ impl Consumer { Poll::Pending } + /// `f` of every value sent so far, once it is ready, or the error once the channel + /// closed and `f` is not, registering `waiter` otherwise. + pub fn poll(&self, waiter: &Waiter, f: F) -> Poll> + where + F: FnOnce(&[u64]) -> Poll, + { + let mut state = self.state.lock().unwrap(); + if let Poll::Ready(res) = f(&state.queue) { + return Poll::Ready(Ok(res)); + } + if state.closed { + return Poll::Ready(Err(Closed)); + } + waiter.register(&mut state.waiters); + Poll::Pending + } + /// kio's one-line wrapper over a `poll_*` method. pub async fn recv(&mut self) -> Option { wait(|waiter| self.poll_recv(waiter)).await diff --git a/ts/fixtures/task/src/drops.rs b/ts/fixtures/task/src/drops.rs index c58eb2a33..d597a44f4 100644 --- a/ts/fixtures/task/src/drops.rs +++ b/ts/fixtures/task/src/drops.rs @@ -7,7 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque, btree_map use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; -use crate::kio::Consumer; +use kio::Consumer; use crate::log::Log; use crate::model::Guard; diff --git a/ts/fixtures/task/src/futures.rs b/ts/fixtures/task/src/futures.rs index 33b9b4163..9d3a10386 100644 --- a/ts/fixtures/task/src/futures.rs +++ b/ts/fixtures/task/src/futures.rs @@ -7,7 +7,7 @@ use std::pin::{Pin, pin}; use std::sync::Arc; use std::task::{Context, Poll, Wake, Waker}; -use crate::kio::{self, Consumer}; +use kio::Consumer; use crate::log::Log; use crate::model::Guard; diff --git a/ts/fixtures/task/src/harness.rs b/ts/fixtures/task/src/harness.rs index 3f41617b4..3d33f12cc 100644 --- a/ts/fixtures/task/src/harness.rs +++ b/ts/fixtures/task/src/harness.rs @@ -145,6 +145,12 @@ fn steps() -> Vec<(&'static str, Step)> { ("rfolded", |log, exec, _, arg| { drops::rfolded(log, 1000 * exec.len() as u32, arg) }), + ("watch", |log, exec, channels, arg| { + if let Some(c) = pick(channels.len(), arg) { + let guard = Guard::new(exec.len() as u32, log.clone()); + exec.spawn(model::watch(channels[c].consumer(), guard)); + } + }), ] } diff --git a/ts/fixtures/task/src/lib.rs b/ts/fixtures/task/src/lib.rs index f8e3771b6..ba2b8900a 100644 --- a/ts/fixtures/task/src/lib.rs +++ b/ts/fixtures/task/src/lib.rs @@ -3,10 +3,10 @@ //! //! `drops` holds values that drop in what std holds them in. //! -//! `kio` trims moq's kio to what its futures hinge on: a `Waiter` holding the task's -//! waker behind an `Arc` that `WaiterList`s hold weakly, `wait`, and a channel whose -//! last producer closes it on drop. `model` holds the async functions, and `futures` one -//! over std's future helpers. `exec` is a deterministic FIFO executor that logs every +//! The `kio` dependency trims moq's kio to what its futures hinge on: a `Waiter` holding +//! the task's waker behind an `Arc` that `WaiterList`s hold weakly, `Task`, `Pending`, +//! `wait`, and a channel whose last producer closes it on drop. `model` holds the async +//! functions, and `futures` one over std's future helpers. `exec` is a deterministic FIFO executor that logs every //! poll, wake, and waker clone and drop, and `harness` runs a script of executor steps //! through it; `tests/task.ts` runs the same script on the translation with its own //! executor. `bench` holds the handoff `just ts bench` times. None of `exec`, `harness`, @@ -17,7 +17,6 @@ pub mod drops; pub mod exec; pub mod futures; pub mod harness; -pub mod kio; pub mod log; pub mod model; #[cfg(target_arch = "wasm32")] diff --git a/ts/fixtures/task/src/model.rs b/ts/fixtures/task/src/model.rs index d6c24317e..a80dd23fc 100644 --- a/ts/fixtures/task/src/model.rs +++ b/ts/fixtures/task/src/model.rs @@ -1,9 +1,12 @@ //! What the harness spawns and drives: a consumer loop holding guards across its awaits, //! the same loop under a stack of async functions, and the channels they read. +use std::future::Future; use std::ops::ControlFlow; +use std::pin::{Pin, pin}; +use std::task::Poll; -use crate::kio::{Consumer, Producer}; +use kio::{Closed, Consumer, Producer, Waiter}; use crate::log::Log; /// A channel's open producers, and a consumer to clone for each task. @@ -115,6 +118,64 @@ pub async fn sum(mut rx: Consumer, guard: Guard) -> u64 { total } +/// Every value until the channel closes, summed, read as moq reads kio's channels: the +/// first by `Waiter::poll_future` of a pinned `recv` as a `dyn Future`, under +/// `kio::wait`, then each by a `Task` of the crate's that a `Pending` drives, reached +/// through its `DerefMut` between awaits, which polls with [`at`]. +pub async fn watch(mut rx: Consumer, guard: Guard) -> u64 { + let first = { + let mut next = pin!(rx.recv()); + let mut next: Pin<&mut dyn Future>> = next.as_mut(); + kio::wait(|waiter| waiter.poll_future(next.as_mut())).await + }; + guard.note(&format!("first {first:?}")); + let mut pending = kio::Pending::new(Watch { rx, index: 1 }); + let mut total = first.unwrap_or(0); + while let Some(value) = (&mut pending).await { + guard.note(&format!("watched {value}")); + total += value; + pending.index += 1; + } + let watch = pending.into_inner(); + guard.note(&format!("watched to {}", watch.index)); + total +} + +/// The value at `index` of a channel, read by [`at`]. +struct Watch { + rx: Consumer, + index: usize, +} + +impl kio::Task for Watch { + type Output = Option; + + fn poll(&mut self, waiter: &Waiter) -> Poll> { + at(&self.rx, self.index, waiter) + } +} + +/// The value at `index`, or `None` once the channel closed without it, as moq's +/// `poll_group` reads its cache: kio's `Consumer::poll` with a closure pending itself, a +/// `Poll>`, outside until the value arrives, and inside while it is even and the +/// one after it hasn't: then it waits on the channel, and the close drops it. +fn at(rx: &Consumer, index: usize, waiter: &Waiter) -> Poll> { + let res = rx.poll(waiter, |queue| match queue.get(index) { + None => Poll::Pending, + Some(value) if value % 2 == 0 && queue.len() == index + 1 => Poll::Ready(Poll::Pending), + Some(value) => Poll::Ready(Poll::Ready(Some(*value))), + }); + let res = match res { + Poll::Ready(Ok(res)) => res, + Poll::Ready(Err(Closed)) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }; + if res.is_pending() && rx.poll(waiter, |_| Poll::<()>::Pending).is_ready() { + return Poll::Ready(None); + } + res +} + /// One value read through the captures only an async block makes: a borrow, which /// dropping the block must not drop; a copy, which a later change to the original must /// not reach; a mutable borrow the block replaces whole, and one of a number it writes diff --git a/ts/fixtures/task/ts.toml b/ts/fixtures/task/ts.toml index d2a3f5ac3..b5a566c29 100644 --- a/ts/fixtures/task/ts.toml +++ b/ts/fixtures/task/ts.toml @@ -8,6 +8,7 @@ roots = [ "model::sum", "model::drain", "model::blocks", + "model::watch", "drops::run", "drops::held", "drops::edited", diff --git a/ts/runtime/rt/task.ts b/ts/runtime/rt/task.ts index cb8477650..12b94170f 100644 --- a/ts/runtime/rt/task.ts +++ b/ts/runtime/rt/task.ts @@ -380,6 +380,12 @@ export const Pending: unique symbol = /* @__PURE__ */ Symbol("Pending"); /** `Poll`. */ export type Poll = T | typeof Pending; +/** `Poll::Pending` of a `Poll` whose `Ready` can already be `Pending`, as `Poll>`'s is. */ +export const OuterPending: unique symbol = /* @__PURE__ */ Symbol("OuterPending"); + +/** The type of `OuterPending`. */ +export type OuterPending = typeof OuterPending; + /** `Waker`: what an executor hands a future to wake its task. */ export interface Waker extends Disposable { wake(): void; diff --git a/ts/src/census.rs b/ts/src/census.rs index 330384cfb..d7d3e8b36 100644 --- a/ts/src/census.rs +++ b/ts/src/census.rs @@ -154,6 +154,7 @@ const VERBATIM: &[&str] = &[ "Octal", "Option", "Ord", + "Pending", "Pointer", "Ready", "Reborrow", @@ -181,6 +182,7 @@ const VERBATIM: &[&str] = &[ "next", "ref mut", "rt.None", + "rt.OuterPending", "self", "undefined", "zip", diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 14c1f5b15..e38c3a19a 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -124,7 +124,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }), ty::Adt(adt, args) => match Std::of(tcx, adt.did()) { Some(Std::Option) => ret(self.copy_unless(self.none(args.type_at(0)), args.type_at(0), span)), - Some(Std::Poll) => ret(self.copy_unless(Expr::Rt("Pending".into()), args.type_at(0), span)), + Some(Std::Poll) => ret(self.copy_unless(self.pending(args.type_at(0)), args.type_at(0), span)), Some(Std::Result) => ret(self.copy_result(args.type_at(0), args.type_at(1), span)), Some(Std::Reverse) => ret(self.copied(v(), args.type_at(0), span)), Some(Std::Bound) => { diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index b609aa985..adb74c4d1 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -546,7 +546,9 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { // place that could change. Operand::Copy(place) => { let ty = place.ty(&self.mir.local_decls, self.tcx).ty; - if changes_captures(ty) { + // Optimized MIR also copies what isn't `Copy`, where it is a move: copy + // propagation does in a body without cleanups, as a dependency's are. + if changes_captures(ty) && self.b.t.is_copy(ty) { return self.err(span, "a copy of a closure that changes what it captured"); } let value = self.place_expr(*place, span, out); @@ -916,8 +918,8 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { _ => scrut.clone().binary("!==", self.b.t.none(args.type_at(0))), }, Some(Std::Poll) => match index.index() { - 0 => scrut.clone().binary("!==", Expr::Rt("Pending".into())), - _ => scrut.clone().binary("===", Expr::Rt("Pending".into())), + 0 => scrut.clone().binary("!==", self.b.t.pending(args.type_at(0))), + _ => scrut.clone().binary("===", self.b.t.pending(args.type_at(0))), }, Some(Std::Result) => match index.index() { 0 => scrut.clone().member("ok"), diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index a0bea14fc..cd5746931 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -477,7 +477,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { if let Some(std) = Std::of(tcx, def_id) { return match (std, variant.index()) { (Std::Option, 0) => self.b.t.none(args.type_at(0)), - (Std::Poll, 1) => Expr::Rt("Pending".into()), + (Std::Poll, 1) => self.b.t.pending(args.type_at(0)), (Std::Option | Std::Poll | Std::Pin, _) => values.pop().unwrap(), // Either variant is its text. (Std::Cow, _) if crate::lower::library::string(tcx, ty) => values.pop().unwrap(), diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index ada9e1754..69533ba5c 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -277,12 +277,13 @@ pub enum ObjectKind { /// The JS values a type's own can be that `Option` and `Poll` erase to: `None` is /// `undefined`, or `rt.None` where `Some` can already be `undefined`, and `Pending` is -/// `rt.Pending`. +/// `rt.Pending`, or `rt.OuterPending` where `Ready` can already be `rt.Pending`. #[derive(Clone, Copy, Default)] pub struct Erased { pub undefined: bool, pub none: bool, pub pending: bool, + pub outer_pending: bool, /// A type parameter's value, which each instance decides. pub param: bool, } @@ -299,6 +300,18 @@ impl Erased { }, } } + + /// What a `Poll` of a type erased so erases to: its `Pending` is `rt.Pending`, or + /// `rt.OuterPending` where `Ready` can already be `rt.Pending`. + fn poll(self) -> Self { + match self.pending { + true => Self { + outer_pending: true, + ..self + }, + false => Self { pending: true, ..self }, + } + } } impl<'a, 'tcx> Translator<'a, 'tcx> { @@ -1203,10 +1216,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } ty::Adt(adt, args) => self.adt_ty(*adt, args, span), ty::Pat(base, _) => self.ty(*base, span), - ty::Coroutine(_, args) => Type::Rt("Future", vec![self.ty(args.as_coroutine().return_ty(), span)]), - ty::Dynamic(..) if let Some(output) = self.dyn_future(ty) => { - Type::Rt("Future", vec![self.ty(output, span)]) - } + ty::Coroutine(_, args) => self.future_ty(args.as_coroutine().return_ty(), span), + ty::Dynamic(..) if let Some(output) = self.dyn_future(ty) => self.future_ty(output, span), // `&dyn Debug` and `&dyn Display` are values ready to write. ty::Dynamic(preds, ..) if preds @@ -1277,9 +1288,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } library::Std::Poll => { let inner = args.type_at(0); - self.erases_to(inner) - .pending - .then(|| format!("`Poll<{inner}>` is not supported: it would be pending two ways with one value")) + let erased = self.erases_to(inner); + (erased.pending && erased.outer_pending).then(|| { + format!("`Poll<{inner}>` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending`") + }) } library::Std::ControlFlow if library::Flow::of(self.tcx, args) == library::Flow::Option => { let erased = self.erases_to(args.type_at(1)); @@ -1382,12 +1394,12 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { if let Some(msg) = self.erasure_conflict(Ty::new_adt(tcx, adt, args)) { self.err(span, msg); } - Type::Rt("Poll", vec![self.ty(args.type_at(0), span)]) + self.poll_ty(args.type_at(0), span) } library::Std::Pin => self.ty(args.type_at(0), span), library::Std::PollFn | library::Std::Pending => { let output = self.future_output(Ty::new_adt(tcx, adt, args), span); - Type::Rt("Future", vec![self.ty(output, span)]) + self.future_ty(output, span) } library::Std::Range => Type::Rt("Range", vec![self.ty(args.type_at(0), span)]), library::Std::RangeInclusive => Type::Rt("RangeInclusive", vec![self.ty(args.type_at(0), span)]), @@ -1461,7 +1473,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { return Type::Builtin("unknown"); } let info = self.adt(adt, span); - if let Some(msg) = self.generic_none(adt, args) { + if let Some(msg) = self.generic_erasure(adt, args) { self.err(span, msg); } let args = args.types().map(|t| self.ty(t, span)).collect(); @@ -1469,35 +1481,50 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } /// Why an instance of the crate's generic `adt` with `args` can't hold its fields' values, - /// if it can't: an `Option` over a type parameter that its definition types `T | - /// undefined`, whose `None` this instance's `T` makes `rt.None`. - fn generic_none(&self, adt: AdtDef<'tcx>, args: GenericArgsRef<'tcx>) -> Option { + /// if it can't: an `Option` or a `Poll` over a type parameter that its definition types + /// `T | undefined` or `rt.Poll`, whose `None` or `Pending` this instance's `T` makes + /// `rt.None` or `rt.OuterPending`. + fn generic_erasure(&self, adt: AdtDef<'tcx>, args: GenericArgsRef<'tcx>) -> Option { let tcx = self.tcx; - if !args.types().any(|t| self.erases_to(t).undefined) { + if !args.types().any(|t| { + let erased = self.erases_to(t); + erased.undefined || erased.pending + }) { return None; } adt.all_fields().find_map(|field| { let generic = tcx.type_of(field.did).instantiate_identity().skip_norm_wip(); generic.walk().filter_map(|arg| arg.as_type()).find_map(|part| { - let ty::Adt(option, option_args) = part.kind() else { + let ty::Adt(wrapper, wrapper_args) = part.kind() else { return None; }; - if library::Std::of(tcx, option.did()) != Some(library::Std::Option) { + let std = library::Std::of(tcx, wrapper.did()); + if !matches!(std, Some(library::Std::Option | library::Std::Poll)) { return None; } - let inner = option_args.type_at(0); + let inner = wrapper_args.type_at(0); let erased = self.erases_to(inner); - if !erased.param || erased.undefined { - return None; - } let instance = ty::EarlyBinder::bind(tcx, inner).instantiate(tcx, args).skip_norm_wip(); - self.erases_to(instance).undefined.then(|| { - let ty = Ty::new_adt(tcx, adt, args); - format!( - "`{ty}` is not supported: its field `{}` is `Option<{inner}>`, typed `{inner} | undefined`, whose `None` would be `rt.None` as `Option<{instance}>`", - field.name - ) - }) + let ty = Ty::new_adt(tcx, adt, args); + match std { + Some(library::Std::Option) if erased.param && !erased.undefined => { + self.erases_to(instance).undefined.then(|| { + format!( + "`{ty}` is not supported: its field `{}` is `Option<{inner}>`, typed `{inner} | undefined`, whose `None` would be `rt.None` as `Option<{instance}>`", + field.name + ) + }) + } + Some(library::Std::Poll) if erased.param && !erased.pending => { + self.erases_to(instance).pending.then(|| { + format!( + "`{ty}` is not supported: its field `{}` is `Poll<{inner}>`, typed `rt.Poll<{inner}>`, whose `Pending` would be `rt.OuterPending` as `Poll<{instance}>`", + field.name + ) + }) + } + _ => None, + } }) }) } @@ -1523,6 +1550,37 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Type::Union(vec![self.ty(inner, span), none]) } + /// The TypeScript type of a future of `output`, an `rt.Future`, whose `poll` returns + /// `rt.Pending` as its own `Pending`, so an `output` that can already be one is refused. + fn future_ty(&mut self, output: Ty<'tcx>, span: Span) -> Type { + if self.erases_to(output).pending { + self.unrepresented( + span, + format!("a future of `{output}`, which can be `rt.Pending`, as the future's own `Pending` is"), + ); + } + Type::Rt("Future", vec![self.ty(output, span)]) + } + + /// The TypeScript type of a `Poll`: `rt.Poll`, or `T | rt.OuterPending` where + /// `T` can already be `rt.Pending`. + fn poll_ty(&mut self, inner: Ty<'tcx>, span: Span) -> Type { + let ready = self.ty(inner, span); + match self.erases_to(inner).pending { + true => Type::Union(vec![ready, Type::Rt("OuterPending", vec![])]), + false => Type::Rt("Poll", vec![ready]), + } + } + + /// The JS value of `Pending` in a `Poll`: `rt.Pending`, or `rt.OuterPending` + /// where `inner` can already be `rt.Pending`. + pub fn pending(&self, inner: Ty<'tcx>) -> ts::Expr { + match self.erases_to(inner).pending { + true => ts::Expr::Rt("OuterPending".into()), + false => ts::Expr::Rt("Pending".into()), + } + } + /// The JS value of `None` in an `Option`, and of `Break` in a `ControlFlow` /// that `?` on one makes: `undefined`, or `rt.None` where `inner` can already be /// `undefined`. @@ -1556,10 +1614,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { self.erases_to(args.type_at(0)) } Some(library::Std::Option) => self.erases_to(args.type_at(0)).option(), - Some(library::Std::Poll) => Erased { - pending: true, - ..self.erases_to(args.type_at(0)) - }, + Some(library::Std::Poll) => self.erases_to(args.type_at(0)).poll(), Some(library::Std::ControlFlow) if library::Flow::of(self.tcx, args) == library::Flow::Option => { self.erases_to(args.type_at(1)).option() } diff --git a/ts/tests/fixtures.rs b/ts/tests/fixtures.rs index 196ca04ca..17da8bb43 100644 --- a/ts/tests/fixtures.rs +++ b/ts/tests/fixtures.rs @@ -416,13 +416,13 @@ fn task_differential(out: &Path) { import * as Log from "./log.Log.ts"; import * as Channel from "./model.Channel.ts"; import * as Guard from "./model.Guard.ts"; -import {{ blocks, drain, sum }} from "./model.ts"; +import {{ blocks, drain, sum, watch }} from "./model.ts"; import * as drops from "./drops.ts"; import * as futures from "./futures.ts"; import {{ harness }} from {harness:?}; export {{ cases, steps }} from {harness:?}; -const fixture = {{ Pending: rt.Pending, u64: {{ of: rt.u64Of }}, Log, Channel, Guard, sum, drain, blocks, drops, futures }}; +const fixture = {{ Pending: rt.Pending, u64: {{ of: rt.u64Of }}, Log, Channel, Guard, sum, drain, blocks, watch, drops, futures }}; const script = harness(fixture); export const run = (input: rt.ByteSlice) => script(rt.byteSliceView(input)); export const unwind = harness(fixture, true); diff --git a/ts/tests/task.ts b/ts/tests/task.ts index fb21e5750..b2cde608a 100644 --- a/ts/tests/task.ts +++ b/ts/tests/task.ts @@ -46,6 +46,7 @@ export interface Fixture { sum(rx: Consumer, guard: Guard): Future; drain(rx: Consumer, depth: number): Future; blocks(rx: Consumer, guard: Guard, moves: boolean): Future; + watch(rx: Consumer, guard: Guard): Future; drops: { run(log: Log, id: number, arg: number): void; held(rx: Consumer, guard: Guard): Future; @@ -293,6 +294,13 @@ export const steps: [string, (r: Run, arg: number) => void][] = [ ["cells", (r, arg) => r.f.drops.cells(r.log, 1000 * r.exec.tasks.length, arg)], ["folded", (r, arg) => r.f.drops.folded(r.log, 1000 * r.exec.tasks.length, arg)], ["rfolded", (r, arg) => r.f.drops.rfolded(r.log, 1000 * r.exec.tasks.length, arg)], + [ + "watch", + (r, arg) => { + const c = pick(r.channels.length, arg); + if (c !== undefined) r.exec.spawn(r.f.watch(r.f.Channel.consumer(r.channels[c]), guard(r))); + }, + ], ]; /** @@ -373,6 +381,12 @@ export const cases: Uint8Array[] = [ script(["open", 0], ["boxed", 0], ["run", 0], ["send", 5], ["run", 0], ["send", 6], ["step", 0], ["send", 7], ["run", 0]), script(["open", 0], ["boxed", 0], ["boxed", 1], ["run", 0], ["send", 4], ["run", 0], ["drop", 1], ["send", 5], ["step", 0], ["drop", 0]), script(["open", 0], ["boxed", 0], ["run", 0], ["send", 5], ["run", 0], ["send", 99], ["run", 0], ["poll", 0], ["drop", 0]), + // kio's API across crates: `Waiter::poll_future`, then a `Pending` of a `Task` polling + // a `Poll>`, whose even value waits for the next, or for the close that drops + // it; and cancelled while it waits. + script(["open", 0], ["watch", 0], ["run", 0], ["send", 3], ["run", 0], ["send", 4], ["run", 0], ["send", 5], ["run", 0], ["close", 0], ["run", 0]), + script(["open", 0], ["watch", 0], ["run", 0], ["send", 1], ["run", 0], ["send", 2], ["run", 0], ["close", 0], ["run", 0]), + script(["open", 0], ["watch", 0], ["watch", 0], ["run", 0], ["send", 6], ["step", 0], ["send", 8], ["drop", 1], ["run", 0], ["drop", 0]), // Each scenario of values std holds that drop, panicking at each point or none. ...Array.from({ length: 253 }, (_, arg) => script(["drops", arg])), // Each of a `Vec`'s edits, its and a tuple's `clone`, and `Ord`'s defaults, panicking at each point or none. From 111e047332bc5a17123e492d825baa7ad387740a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:34:10 -0700 Subject: [PATCH 3/8] wip: closures that only share, reject cases, docs Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-async.md | 9 +- ts/fixtures/reject/census.md | 41 +++- ts/fixtures/reject/expected.txt | 5 +- ts/fixtures/reject/src/lib.rs | 20 +- ts/fixtures/reject/ts.toml | 2 +- ts/fixtures/task/expected/core/task/poll.ts | 5 +- ts/fixtures/task/expected/kio.Consumer.ts | 12 +- ts/fixtures/task/expected/kio.Pending.ts | 12 +- ts/fixtures/task/expected/kio.ts | 43 ++++ ts/fixtures/task/expected/model.ts | 234 ++++++++++++-------- ts/fixtures/task/kio/src/lib.rs | 15 ++ ts/fixtures/task/src/model.rs | 3 +- ts/src/census.rs | 1 + ts/src/lower/mir/place.rs | 11 +- 14 files changed, 273 insertions(+), 140 deletions(-) diff --git a/quest/m2/ts-async.md b/quest/m2/ts-async.md index 4605a0f66..d6e871201 100644 --- a/quest/m2/ts-async.md +++ b/quest/m2/ts-async.md @@ -18,15 +18,14 @@ A handoff through 1, 4, and 16 async functions measured 1.5-2.7x faster than #16 generators in Node, 1.8-5.5x in Bun, and 1.4-3.5x in Chrome, and past depth 1 faster than moq's hand-written promises. `rt.spawn` stays. -std's future helpers translate: `Poll`'s methods, `pin!`, `poll_fn`, `future::pending`, +std's future helpers translate: `Poll`'s methods, `Poll>`, `pin!`, `poll_fn`, `future::pending`, `Waker::from(Arc)`, and `Waker::noop`, and so do `Box::pin` and `dyn Future`. What moq-net still needs, by its census from MIR at the pinned revision: - Async closures, which only moq-net's tests write today. -- kio's own API, reached across crates (`Pending`, `Consumer::poll`, - `Waiter::poll_future`, `Tasks`), translates from source as a - [dependency](/ts/README.md), and kio's `RawWaker` vtable and - `Box` tasks are [bindings](/ts/README.md#bindings). +- kio's `Tasks`, whose `RawWaker` vtable and `Box` tasks are + [bindings](/ts/README.md#bindings). The rest of kio's API translates from source + across crates; what it still refuses is std's. - The edge: `rt.spawn`'s shape for callers, and cancelling a spawned task. An async block that captures part of a variable, which THIR refused 47 times, is a diff --git a/ts/fixtures/reject/census.md b/ts/fixtures/reject/census.md index 877c7576e..d60638995 100644 --- a/ts/fixtures/reject/census.md +++ b/ts/fixtures/reject/census.md @@ -1,7 +1,7 @@ # Census of `reject-fixture` -- Refusals: 73 -- Function instances: 83 +- Refusals: 74 +- Function instances: 86 - Generic functions no instance reached, lowered over their type parameters: 6 ## By construct @@ -13,7 +13,6 @@ | 4 | an assignment through a reference to `_` | fixtures/reject/src/lib.rs:101:2 | | 3 | replacing the value behind a reference to `_` | fixtures/reject/src/lib.rs:85:2 | | 3 | unsupported type `_` | fixtures/reject/src/lib.rs:7:14 | -| 2 | `_` is not supported: it would be pending two ways with one value | fixtures/reject/src/lib.rs:105:20 | | 2 | `_` is not supported: its `None` and `Some` would both be `rt.None` | fixtures/reject/src/lib.rs:16:15 | | 2 | `_` of `_`, whose `None` is `rt.None`, which is not supported yet | fixtures/reject/src/lib.rs:41:42 | | 2 | `_` of a reference to an array of bytes it is generic over, which its mapping would take as the array, not as a slice of it | fixtures/reject/src/lib.rs:247:16 | @@ -25,7 +24,9 @@ | 2 | passing `_`, a closure that owns a value that drops, to `_`, which is not supported yet | fixtures/reject/src/lib.rs:369:13 | | 1 | `Pointer` formatting, as in `{:p}`, is not supported | fixtures/reject/src/lib.rs:68:2 | | 1 | `_` is not supported yet | fixtures/reject/src/lib.rs:393:18 | +| 1 | `_` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending` | fixtures/reject/src/lib.rs:105:20 | | 1 | `_` is not supported: its field `_` is `_`, typed `_`, whose `None` would be `rt.None` as `_` | fixtures/reject/src/lib.rs:31:2 | +| 1 | `_` is not supported: its field `_` is `_`, typed `_`, whose `Pending` would be `rt.OuterPending` as `_` | fixtures/reject/src/lib.rs:512:2 | | 1 | `_` of `_` by `_`, which is not supported yet | fixtures/reject/src/lib.rs:286:6 | | 1 | `_` of `_`, which drops `_`, which is not supported yet | fixtures/reject/src/lib.rs:53:13 | | 1 | `_` through a held type's own, which is not supported yet | fixtures/reject/src/lib.rs:318:2 | @@ -38,6 +39,7 @@ | 1 | a coercion to `_` | fixtures/reject/src/lib.rs:341:2 | | 1 | a constant of type `_` | fixtures/reject/src/lib.rs:80:2 | | 1 | a copy of a closure that changes what it captured | fixtures/reject/src/lib.rs:257:14 | +| 1 | a future of `_`, which can be `rt.Pending`, as the future's own `Pending` is | fixtures/reject/src/lib.rs:110:34 | | 1 | a mutable borrow of a subslice of `_`, which JS would copy, having no views | fixtures/reject/src/lib.rs:226:13 | | 1 | a mutable reference into a `Result`'s payload, passed on or kept | fixtures/reject/src/lib.rs:413:12 | | 1 | a printable type must implement `Display` or `Debug` | fixtures/reject/src/lib.rs:92:1 | @@ -87,11 +89,14 @@ The type or module each refused type or path belongs to. | 1 | `Noisy` | fixtures/reject/src/lib.rs:53:13 | | 1 | `T \| undefined` | fixtures/reject/src/lib.rs:31:2 | | 1 | `Tail` | fixtures/reject/src/lib.rs:501:30 | +| 1 | `Waiting` | fixtures/reject/src/lib.rs:512:2 | | 1 | `[u16; 1]` | fixtures/reject/src/lib.rs:240:22 | | 1 | `[u8; 4]` | fixtures/reject/src/lib.rs:491:26 | | 1 | `dyn std::ops::Fn(u8) -> u8` | fixtures/reject/src/lib.rs:11:16 | | 1 | `f64` | fixtures/reject/src/lib.rs:449:2 | | 1 | `into_keys` | fixtures/reject/src/lib.rs:53:13 | +| 1 | `poll` | fixtures/reject/src/lib.rs:512:2 | +| 1 | `rt.Poll` | fixtures/reject/src/lib.rs:512:2 | | 1 | `std::iter::Chain` | fixtures/reject/src/lib.rs:172:51 | | 1 | `std::iter::Rev` | fixtures/reject/src/lib.rs:166:14 | | 1 | `std::option::Option` | fixtures/reject/src/lib.rs:420:2 | @@ -99,6 +104,7 @@ The type or module each refused type or path belongs to. | 1 | `std::string::String` | fixtures/reject/src/lib.rs:74:10 | | 1 | `std::sync::MutexGuard` | fixtures/reject/src/lib.rs:121:2 | | 1 | `std::sync::Weak` | fixtures/reject/src/lib.rs:465:40 | +| 1 | `std::task::Poll` | fixtures/reject/src/lib.rs:110:34 | | 1 | `str` | fixtures/reject/src/lib.rs:377:4 | | 1 | `u32` | fixtures/reject/src/lib.rs:226:13 | | 1 | `u64` | fixtures/reject/src/lib.rs:96:2 | @@ -111,14 +117,15 @@ The type or module each refused type or path belongs to. | ---: | --- | --- | --- | | 4 | a variant of `_`, which is not supported: either is the same JS string | `std::borrow::Cow` | fixtures/reject/src/lib.rs:382:2 | | 3 | unsupported type `_` | `f32` | fixtures/reject/src/lib.rs:7:14 | -| 2 | `_` is not supported: it would be pending two ways with one value | `Poll` | fixtures/reject/src/lib.rs:105:20 | | 2 | `_` is not supported: its `None` and `Some` would both be `rt.None` | `Option` | fixtures/reject/src/lib.rs:16:15 | | 2 | `_`, a trait object of the generic trait `_`, which is not supported yet | `dyn Scaled`, `Scaled` | fixtures/reject/src/lib.rs:474:23 | | 2 | an assignment through a reference to `_` | `std::pin::Pin` | fixtures/reject/src/lib.rs:116:2 | | 2 | replacing the value behind a reference to `_` | `Meters` | fixtures/reject/src/lib.rs:85:2 | | 2 | unsupported call to `_` | `::sum` | fixtures/reject/src/lib.rs:439:17 | | 1 | `_` is not supported yet | `std::borrow::Cow` | fixtures/reject/src/lib.rs:393:18 | +| 1 | `_` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending` | `Poll` | fixtures/reject/src/lib.rs:105:20 | | 1 | `_` is not supported: its field `_` is `_`, typed `_`, whose `None` would be `rt.None` as `_` | `Maybe`, `value`, `Option`, `T \| undefined`, `Option` | fixtures/reject/src/lib.rs:31:2 | +| 1 | `_` is not supported: its field `_` is `_`, typed `_`, whose `Pending` would be `rt.OuterPending` as `_` | `Waiting`, `poll`, `Poll`, `rt.Poll`, `Poll` | fixtures/reject/src/lib.rs:512:2 | | 1 | `_` of `_` by `_`, which is not supported yet | `HashMap::contains_key`, `std::collections::HashMap`, `u8` | fixtures/reject/src/lib.rs:286:6 | | 1 | `_` of `_`, which drops `_`, which is not supported yet | `into_keys`, `std::collections::HashMap`, `Noisy` | fixtures/reject/src/lib.rs:53:13 | | 1 | `_` of `_`, whose `None` is `rt.None`, which is not supported yet | `HashMap::get`, `Option` | fixtures/reject/src/lib.rs:263:6 | @@ -135,6 +142,7 @@ The type or module each refused type or path belongs to. | 1 | a coercion to `_`, which is not supported yet | `&Tail` | fixtures/reject/src/lib.rs:501:30 | | 1 | a coercion to `_`, which is not supported yet | `std::sync::Weak` | fixtures/reject/src/lib.rs:465:40 | | 1 | a constant of type `_` | `&u8` | fixtures/reject/src/lib.rs:80:2 | +| 1 | a future of `_`, which can be `rt.Pending`, as the future's own `Pending` is | `std::task::Poll` | fixtures/reject/src/lib.rs:110:34 | | 1 | a mutable borrow of a subslice of `_`, which JS would copy, having no views | `u32` | fixtures/reject/src/lib.rs:226:13 | | 1 | a scalar constant of type `_` | `f32` | fixtures/reject/src/lib.rs:8:2 | | 1 | a trait object of `_`, which is not supported yet | `[u8; 4]` | fixtures/reject/src/lib.rs:491:26 | @@ -219,13 +227,6 @@ The type or module each refused type or path belongs to. -

`_` is not supported: it would be pending two ways with one value (2) - -- fixtures/reject/src/lib.rs:105:20: `Poll>>` is not supported: it would be pending two ways with one value -- fixtures/reject/src/lib.rs:110:54: `Poll>` is not supported: it would be pending two ways with one value - -
-
`_` is not supported: its `None` and `Some` would both be `rt.None` (2) - fixtures/reject/src/lib.rs:16:15: `Option>>` is not supported: its `None` and `Some` would both be `rt.None` @@ -301,12 +302,24 @@ The type or module each refused type or path belongs to.
+
`_` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending` (1) + +- fixtures/reject/src/lib.rs:105:20: `Poll>>>` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending` + +
+
`_` is not supported: its field `_` is `_`, typed `_`, whose `None` would be `rt.None` as `_` (1) - fixtures/reject/src/lib.rs:31:2: `Maybe<()>` is not supported: its field `value` is `Option`, typed `T | undefined`, whose `None` would be `rt.None` as `Option<()>`
+
`_` is not supported: its field `_` is `_`, typed `_`, whose `Pending` would be `rt.OuterPending` as `_` (1) + +- fixtures/reject/src/lib.rs:512:2: `Waiting>` is not supported: its field `poll` is `Poll`, typed `rt.Poll`, whose `Pending` would be `rt.OuterPending` as `Poll>` + +
+
`_` of `_` by `_`, which is not supported yet (1) - fixtures/reject/src/lib.rs:286:6: `HashMap::contains_key` of `std::collections::HashMap` by `u8`, which is not supported yet @@ -379,6 +392,12 @@ The type or module each refused type or path belongs to.
+
a future of `_`, which can be `rt.Pending`, as the future's own `Pending` is (1) + +- fixtures/reject/src/lib.rs:110:34: a future of `std::task::Poll`, which can be `rt.Pending`, as the future's own `Pending` is + +
+
a mutable borrow of a subslice of `_`, which JS would copy, having no views (1) - fixtures/reject/src/lib.rs:226:13: a mutable borrow of a subslice of `u32`, which JS would copy, having no views diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index eed8a9d79..bd652bd7e 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -21,8 +21,8 @@ a constant of type `&u8` at fixtures/reject/src/lib.rs:80:2 replacing the value behind a reference to `Meters` at fixtures/reject/src/lib.rs:85:2 replacing the value behind a reference to `Meters` at fixtures/reject/src/lib.rs:89:2 an assignment through a reference to `std::sync::Arc` at fixtures/reject/src/lib.rs:101:2 -`Poll>>` is not supported: it would be pending two ways with one value at fixtures/reject/src/lib.rs:105:20 -`Poll>` is not supported: it would be pending two ways with one value at fixtures/reject/src/lib.rs:110:54 +`Poll>>>` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending` at fixtures/reject/src/lib.rs:105:20 +a future of `std::task::Poll`, which can be `rt.Pending`, as the future's own `Pending` is at fixtures/reject/src/lib.rs:110:34 an assignment through a reference to `std::pin::Pin>` at fixtures/reject/src/lib.rs:116:2 an assignment through a reference to `std::sync::MutexGuard<'_, u8>` at fixtures/reject/src/lib.rs:121:2 unsupported call to ` as std::iter::Iterator>::cycle` at fixtures/reject/src/lib.rs:126:11 @@ -65,6 +65,7 @@ a coercion to `std::sync::Weak`, which is not supported yet at fixt `dyn Scaled`, a trait object of the generic trait `Scaled`, which is not supported yet at fixtures/reject/src/lib.rs:475:4 a trait object of `[u8; 4]`, which is not supported yet at fixtures/reject/src/lib.rs:491:26 a coercion to `&Tail`, which is not supported yet at fixtures/reject/src/lib.rs:501:30 +`Waiting>` is not supported: its field `poll` is `Poll`, typed `rt.Poll`, whose `Pending` would be `rt.OuterPending` as `Poll>` at fixtures/reject/src/lib.rs:512:2 overriding `Iterator::fold`, which std's adaptors call in place of `next` or beside it at fixtures/reject/src/lib.rs:139:2 an iterator of the crate implementing `DoubleEndedIterator`, which is not supported yet at fixtures/reject/src/lib.rs:159:1 overriding `Iterator::size_hint`, which std's adaptors call in place of `next` or beside it at fixtures/reject/src/lib.rs:193:2 diff --git a/ts/fixtures/reject/src/lib.rs b/ts/fixtures/reject/src/lib.rs index 0544f6a47..d91fab2a4 100644 --- a/ts/fixtures/reject/src/lib.rs +++ b/ts/fixtures/reject/src/lib.rs @@ -106,16 +106,6 @@ pub fn nested_poll(p: std::task::Poll matches!(p, std::task::Poll::Pending) } -/// A generic type's `Poll` is `rt.Poll`, which can't hold `rt.OuterPending`, the -/// `Pending` of a `Poll>`. -pub struct Waiting { - pub poll: std::task::Poll, -} - -pub fn generic_pending(poll: std::task::Poll>) -> bool { - Waiting { poll }.poll.is_pending() -} - /// A future's `Ready(Pending)` would be `rt.Pending`, as its own pending is. pub async fn pending_output() -> std::task::Poll { std::task::Poll::Pending @@ -511,3 +501,13 @@ pub fn tail_read(t: &Tail) -> u8 { let r: &Tail = t; r.value.read() } + +/// A generic type's `Poll` is `rt.Poll`, which can't hold `rt.OuterPending`, the +/// `Pending` of a `Poll>`. +pub struct Waiting { + pub poll: std::task::Poll, +} + +pub fn generic_pending(poll: std::task::Poll>) -> bool { + Waiting { poll }.poll.is_pending() +} diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 82c40a5bf..319decb11 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -18,7 +18,6 @@ roots = [ "generic", "replace_shared", "nested_poll", - "generic_pending", "pending_output", "replace_pinned", "replace_guard", @@ -59,5 +58,6 @@ roots = [ "first_of", "tail_read", "missing", + "generic_pending", ] printable = ["Meters"] diff --git a/ts/fixtures/task/expected/core/task/poll.ts b/ts/fixtures/task/expected/core/task/poll.ts index b52a002e6..e976bd694 100644 --- a/ts/fixtures/task/expected/core/task/poll.ts +++ b/ts/fixtures/task/expected/core/task/poll.ts @@ -1,7 +1,6 @@ // Generated by rust2-ts. Do not edit. import * as rt from "../../rt.ts"; -import type * as Closed from "../../kio.Closed.ts"; export function map$Poll_Option_u64_Result_u64_u64_helpers_closure0_closure1_closure0(self: rt.Poll, f: (a0: rt.u64 | undefined) => rt.Result): rt.Poll> { if (self !== rt.Pending) { @@ -31,14 +30,14 @@ export function isReady$Poll_Option_u64(self: rt.Poll): bool return false; } -export function isReady$Poll_Result_unit_Closed(self: rt.Poll>): boolean { +export function isReady$Poll_u64(self: rt.Poll): boolean { if (self !== rt.Pending) { return true; } return false; } -export function isReady$Poll_u64(self: rt.Poll): boolean { +export function isReady$Poll_unit(self: rt.Poll): boolean { if (self !== rt.Pending) { return true; } diff --git a/ts/fixtures/task/expected/kio.Consumer.ts b/ts/fixtures/task/expected/kio.Consumer.ts index 46d3436be..d9444fde6 100644 --- a/ts/fixtures/task/expected/kio.Consumer.ts +++ b/ts/fixtures/task/expected/kio.Consumer.ts @@ -57,20 +57,14 @@ export function poll$at_closure0_Poll_Option_u64(self: Consumer, waiter: Waiter. return rt.Err({}); } -/** - * `f` of every value sent so far, once it is ready, or the error once the channel - * closed and `f` is not, registering `waiter` otherwise. - */ -export function poll$at_closure1_unit(self: Consumer, waiter: Waiter.Waiter, f: (a0: rt.u64[]) => rt.Poll): rt.Poll> { +/** Ready once the channel closed, registering `waiter` until then. */ +export function pollClosed(self: Consumer, waiter: Waiter.Waiter): rt.Poll { const state: rt.Mutex = self.state.value; - if (f(state.value.queue) !== rt.Pending) { - return rt.OK; - } if (!state.value.closed) { Waiter.register(waiter, state.value.waiters); return rt.Pending; } - return rt.Err({}); + return undefined; } //# sourceMappingURL=kio.Consumer.ts.map diff --git a/ts/fixtures/task/expected/kio.Pending.ts b/ts/fixtures/task/expected/kio.Pending.ts index 90479a2b9..2b6b46438 100644 --- a/ts/fixtures/task/expected/kio.Pending.ts +++ b/ts/fixtures/task/expected/kio.Pending.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { poll$Task_boxed_closure0_closure3, poll$Task_boxed_closure0_closure5, poll$Task_recv_closure0_closure0, poll$Task_watch_closure0_closure0 } from "./kio.ts"; +import { poll$Task_boxed_closure0_closure3, poll$Task_boxed_closure0_closure5, poll$Task_closed_closure0_closure0, poll$Task_recv_closure0_closure0, poll$Task_watch_closure0_closure0 } from "./kio.ts"; import * as Park from "./kio.Park.ts"; import type * as Waiter from "./kio.Waiter.ts"; import * as Watch from "./model.Watch.ts"; @@ -43,6 +43,10 @@ export function new$boxed_closure0_closure5(inner: (a0: Waiter.Waiter) => rt.Pol return new Pending({ inner, park: Park.default$Default() }); } +export function new$closed_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { + return new Pending({ inner, park: Park.default$Default() }); +} + export function new$recv_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { return new Pending({ inner, park: Park.default$Default() }); } @@ -79,6 +83,12 @@ export function poll$Future_boxed_closure0_closure5(self: Pending<(a0: Waiter.Wa return poll$Task_boxed_closure0_closure5(this_.inner, waiter); } +export function poll$Future_closed_closure0_closure0(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { + const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return poll$Task_closed_closure0_closure0(this_.inner, waiter); +} + export function poll$Future_recv_closure0_closure0(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; const waiter: Waiter.Waiter = Park.hold(this_.park, cx); diff --git a/ts/fixtures/task/expected/kio.ts b/ts/fixtures/task/expected/kio.ts index 8ab4bda85..01766f7d5 100644 --- a/ts/fixtures/task/expected/kio.ts +++ b/ts/fixtures/task/expected/kio.ts @@ -13,6 +13,10 @@ export function poll$Task_boxed_closure0_closure5(self: (a0: Waiter.Waiter) => r return self(waiter); } +export function poll$Task_closed_closure0_closure0(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { + return self(waiter); +} + export function poll$Task_recv_closure0_closure0(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { return self(waiter); } @@ -31,6 +35,11 @@ export function wait$boxed_closure0_closure5_u64(poll: (a0: Waiter.Waiter) => rt return Pending.new$boxed_closure0_closure5(poll); } +/** A future from a poll function that receives a [`Waiter`]. */ +export function wait$closed_closure0_closure0_unit(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { + return Pending.new$closed_closure0_closure0(poll); +} + /** A future from a poll function that receives a [`Waiter`]. */ export function wait$recv_closure0_closure0_Option_u64(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { return Pending.new$recv_closure0_closure0(poll); @@ -41,6 +50,40 @@ export function wait$watch_closure0_closure0_Option_u64(poll: (a0: Waiter.Waiter return Pending.new$watch_closure0_closure0(poll); } +export class closedFuture { + #state: number = 0; + #self: Consumer.Consumer; + #awaitee!: Pending.Pending<(a0: Waiter.Waiter) => rt.Poll>; + + constructor(self: Consumer.Consumer) { + this.#self = self; + } + + [Symbol.dispose](): void { + if (this.#state === 3) { + rt.drop(this.#awaitee); + } + } + + poll(_2: rt.Context): rt.Poll { + if (this.#state === 0) { + const self: Consumer.Consumer = this.#self; + this.#awaitee = wait$closed_closure0_closure0_unit((waiter: Waiter.Waiter): rt.Poll => { + return Consumer.pollClosed(self, waiter); + }); + } else if (this.#state === 1) { + rt.asyncFnResumed(); + } + if (Pending.poll$Future_closed_closure0_closure0(this.#awaitee, _2) !== rt.Pending) { + rt.drop(this.#awaitee); + this.#state = 1; + return undefined; + } + this.#state = 3; + return rt.Pending; + } +} + export class recvFuture { #state: number = 0; #self: Consumer.Consumer; diff --git a/ts/fixtures/task/expected/model.ts b/ts/fixtures/task/expected/model.ts index 3fca3f671..9318ae75f 100644 --- a/ts/fixtures/task/expected/model.ts +++ b/ts/fixtures/task/expected/model.ts @@ -3,8 +3,8 @@ import * as rt from "./rt.ts"; import { copy$Tally, drop$ControlFlow_Guard_u32, drop$Result_u32_Guard } from "./lib.ts"; import { unwrapOr$Option_u64 } from "./core/option.ts"; -import { isReady$Poll_Result_unit_Closed, isPending$Poll_Option_u64 } from "./core/task/poll.ts"; -import { wait$watch_closure0_closure0_Option_u64, recvFuture } from "./kio.ts"; +import { isReady$Poll_unit, isPending$Poll_Option_u64 } from "./core/task/poll.ts"; +import { wait$watch_closure0_closure0_Option_u64, closedFuture, recvFuture } from "./kio.ts"; import type * as Closed from "./kio.Closed.ts"; import * as Consumer from "./kio.Consumer.ts"; import * as Pending from "./kio.Pending.ts"; @@ -198,6 +198,8 @@ export class watchFuture { #pending!: Pending.Pending; #total!: rt.u64; #awaitee$2!: Pending.Pending; + #watch!: Watch.Watch; + #awaitee$3!: rt.Future; #saved!: boolean; #saved$2!: boolean; @@ -209,67 +211,94 @@ export class watchFuture { [Symbol.dispose](): void { let unwind: number = 0; try { - if (this.#state === 0) { - unwind = 28; - rt.drop(this.#rx); - unwind = 0; - rt.drop(this.#guard); - return; - } - if (this.#state === 3) { - unwind = 16; - rt.drop(this.#awaitee); - unwind = 18; - rt.drop(this.#pinned); - } else if (this.#state === 4) { - unwind = 15; - rt.drop(this.#pending); - this.#saved$2 = false; - } else { - return; + b0: { + if (this.#state === 0) { + unwind = 38; + rt.drop(this.#rx); + unwind = 0; + rt.drop(this.#guard); + return; + } + if (this.#state === 3) { + unwind = 24; + rt.drop(this.#awaitee); + unwind = 26; + rt.drop(this.#pinned); + break b0; + } + if (this.#state !== 4) { + if (this.#state === 5) { + unwind = 20; + rt.drop(this.#awaitee$3); + unwind = 22; + rt.drop(this.#watch); + } else { + return; + } + } + if (this.#saved) { + unwind = 23; + rt.drop(this.#pending); + } + this.#saved = false; } - unwind = 20; + unwind = 28; rt.drop(this.#guard); - if (this.#saved) { - unwind = 21; + if (this.#saved$2) { + unwind = 29; rt.drop(this.#rx); } - this.#saved = false; + this.#saved$2 = false; } catch (e) { - b2: { - b3: { - b4: { - if (unwind === 0) { - throw e; - } - if (unwind === 15) { - this.#saved$2 = false; - } else if (unwind === 16) { - rt.drop(this.#pinned); - } else if (unwind !== 18) { - if (unwind === 20) { - break b4; - } - if (unwind === 21) { - break b3; + b4: { + b5: { + b6: { + b7: { + b8: { + b9: { + if (unwind === 0) { + throw e; + } + if (unwind === 20) { + rt.drop(this.#watch); + break b9; + } + if (unwind === 22) { + break b9; + } + if (unwind === 23) { + break b8; + } + if (unwind === 24) { + rt.drop(this.#pinned); + } else if (unwind !== 26) { + if (unwind === 28) { + break b6; + } + if (unwind === 29) { + break b5; + } + rt.drop(this.#guard); + break b4; + } + break b7; + } } - rt.drop(this.#guard); - break b2; + this.#saved = false; } rt.drop(this.#guard); } - if (this.#saved) { + if (this.#saved$2) { rt.drop(this.#rx); } } - this.#saved = false; + this.#saved$2 = false; } throw e; } } poll(_2: rt.Context): rt.Poll { - let watch_!: Watch.Watch; let unwind: number = 0; try { let bb0: number = 0; @@ -277,12 +306,12 @@ export class watchFuture { switch (bb0) { case 0: if (this.#state === 0) { - unwind = 59; - this.#saved = false; + unwind = 68; this.#saved$2 = false; - this.#saved = true; + this.#saved = false; + this.#saved$2 = true; const _3: rt.Future = new recvFuture(this.#rx); - unwind = 58; + unwind = 67; this.#pinned = _3; this.#next = this.#pinned; this.#next$2 = this.#next; @@ -301,28 +330,32 @@ export class watchFuture { bb0 = 6; continue; } - bb0 = 24; + if (this.#state === 4) { + bb0 = 24; + continue; + } + bb0 = 44; continue; } case 6: - unwind = 57; + unwind = 66; const _12: rt.Poll = Pending.poll$Future_watch_closure0_closure0(this.#awaitee, _2); if (_12 !== rt.Pending) { - unwind = 58; + unwind = 67; this.#first = _12; rt.drop(this.#awaitee); - unwind = 59; + unwind = 68; rt.drop(this.#pinned); const arg = this.#first; const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtOption(arg, f, rt.u64Debug)]; const _24: string = rt.fmtFormat(["first ", args2[0]]); - unwind = 56; + unwind = 65; Guard.note(this.#guard, _24); - unwind = 55; - this.#saved = false; + unwind = 64; + this.#saved$2 = false; this.#pending = Pending.new$Watch(new Watch.Watch({ rx: this.#rx, index: 1 })); - unwind = 62; - this.#saved$2 = true; + unwind = 71; + this.#saved = true; this.#total = unwrapOr$Option_u64(this.#first, 0); bb0 = 22; continue; @@ -330,12 +363,12 @@ export class watchFuture { this.#state = 3; return rt.Pending; case 22: - unwind = 62; + unwind = 71; this.#awaitee$2 = this.#pending; bb0 = 24; continue; case 24: - unwind = 62; + unwind = 71; const _38: rt.Poll = Pending.poll$Future_Watch(this.#awaitee$2, _2); if (_38 !== rt.Pending) { const result2: rt.u64 | undefined = _38; @@ -343,34 +376,45 @@ export class watchFuture { const value: rt.u64 = result2; const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(value, f)]; const _52: string = rt.fmtFormat(["watched ", args4[0]]); - unwind = 54; + unwind = 63; Guard.note(this.#guard, _52); - unwind = 62; + unwind = 71; this.#total = rt.u64Add(this.#total, value); const _62: Watch.Watch = Pending.derefMut$DerefMut_Watch(this.#pending); _62.index = rt.usizeAdd(_62.index, 1); bb0 = 22; continue; } - this.#saved$2 = false; - watch_ = Pending.intoInner$Watch(this.#pending); - unwind = 53; - const arg2 = watch_.index; - const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(arg2, f)]; - const _71: string = rt.fmtFormat(["watched to ", args6[0]]); - unwind = 52; - Guard.note(this.#guard, _71); + this.#saved = false; + this.#watch = Pending.intoInner$Watch(this.#pending); unwind = 62; - const _83: rt.u64 = this.#total; - rt.drop(watch_); - unwind = 64; - this.#saved$2 = false; - rt.drop(this.#guard); + this.#awaitee$3 = new closedFuture(this.#watch.rx); + bb0 = 44; + continue; + } + this.#state = 4; + return rt.Pending; + case 44: + unwind = 61; + if (this.#awaitee$3.poll(_2) !== rt.Pending) { + unwind = 62; + rt.drop(this.#awaitee$3); + const arg2 = this.#watch.index; + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(arg2, f)]; + const _81: string = rt.fmtFormat(["watched to ", args6[0]]); + unwind = 60; + Guard.note(this.#guard, _81); + unwind = 71; + const _93: rt.u64 = this.#total; + rt.drop(this.#watch); + unwind = 73; this.#saved = false; + rt.drop(this.#guard); + this.#saved$2 = false; this.#state = 1; - return _83; + return _93; } - this.#state = 4; + this.#state = 5; return rt.Pending; } } @@ -383,28 +427,32 @@ export class watchFuture { if (unwind === 0) { throw e; } - if (unwind === 52) { + if (unwind === 60) { break b5; } - if (unwind === 53) { + if (unwind === 61) { + rt.drop(this.#awaitee$3); break b5; } - if (unwind === 54) { + if (unwind === 62) { + break b5; + } + if (unwind === 63) { break b4; } - if (unwind === 55) { + if (unwind === 64) { break b3; } - if (unwind === 56) { + if (unwind === 65) { break b2; } - if (unwind === 57) { + if (unwind === 66) { rt.drop(this.#awaitee); - } else if (unwind !== 58) { - if (unwind === 59) { + } else if (unwind !== 67) { + if (unwind === 68) { break b2; } - if (unwind === 62) { + if (unwind === 71) { break b4; } break b1; @@ -412,20 +460,20 @@ export class watchFuture { rt.drop(this.#pinned); break b2; } - rt.drop(watch_); + rt.drop(this.#watch); } - if (this.#saved$2) { + if (this.#saved) { rt.drop(this.#pending); } } - this.#saved$2 = false; + this.#saved = false; } rt.drop(this.#guard); } - if (this.#saved) { + if (this.#saved$2) { rt.drop(this.#rx); } - this.#saved = false; + this.#saved$2 = false; this.#state = 2; throw e; } @@ -462,9 +510,7 @@ export function at(rx: Consumer.Consumer, index: rt.usize, waiter: Waiter.Waiter res2 = rt.Pending as rt.Poll; } if (isPending$Poll_Option_u64(res2)) { - if (isReady$Poll_Result_unit_Closed(Consumer.poll$at_closure1_unit(rx, waiter, (_2: rt.u64[]): rt.Poll => { - return rt.Pending; - }))) { + if (isReady$Poll_unit(Consumer.pollClosed(rx, waiter))) { return undefined; } } diff --git a/ts/fixtures/task/kio/src/lib.rs b/ts/fixtures/task/kio/src/lib.rs index 2a65088cd..4573be792 100644 --- a/ts/fixtures/task/kio/src/lib.rs +++ b/ts/fixtures/task/kio/src/lib.rs @@ -309,6 +309,21 @@ impl Consumer { Poll::Pending } + /// Ready once the channel closed, registering `waiter` until then. + pub fn poll_closed(&self, waiter: &Waiter) -> Poll<()> { + let mut state = self.state.lock().unwrap(); + if state.closed { + return Poll::Ready(()); + } + waiter.register(&mut state.waiters); + Poll::Pending + } + + /// Wait for the channel to close, through a closure that only borrows `self`. + pub async fn closed(&self) { + wait(move |waiter| self.poll_closed(waiter)).await + } + /// kio's one-line wrapper over a `poll_*` method. pub async fn recv(&mut self) -> Option { wait(|waiter| self.poll_recv(waiter)).await diff --git a/ts/fixtures/task/src/model.rs b/ts/fixtures/task/src/model.rs index a80dd23fc..428824acc 100644 --- a/ts/fixtures/task/src/model.rs +++ b/ts/fixtures/task/src/model.rs @@ -137,6 +137,7 @@ pub async fn watch(mut rx: Consumer, guard: Guard) -> u64 { pending.index += 1; } let watch = pending.into_inner(); + watch.rx.closed().await; guard.note(&format!("watched to {}", watch.index)); total } @@ -170,7 +171,7 @@ fn at(rx: &Consumer, index: usize, waiter: &Waiter) -> Poll> { Poll::Ready(Err(Closed)) => Poll::Ready(None), Poll::Pending => Poll::Pending, }; - if res.is_pending() && rx.poll(waiter, |_| Poll::<()>::Pending).is_ready() { + if res.is_pending() && rx.poll_closed(waiter).is_ready() { return Poll::Ready(None); } res diff --git a/ts/src/census.rs b/ts/src/census.rs index d7d3e8b36..2af105aea 100644 --- a/ts/src/census.rs +++ b/ts/src/census.rs @@ -183,6 +183,7 @@ const VERBATIM: &[&str] = &[ "ref mut", "rt.None", "rt.OuterPending", + "rt.Pending", "self", "undefined", "zip", diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index adb74c4d1..3f055b25d 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -1242,13 +1242,18 @@ pub fn transparent<'tcx>(t: &crate::lower::Translator<'_, 'tcx>, ty: Ty<'tcx>) - ) } -/// Whether `ty` holds an `FnMut` closure by value: a copy of its arrow function shares -/// the variables it captured, which in Rust each copy has its own of. +/// Whether `ty` holds an `FnMut` closure by value that owns a capture: a copy of its arrow +/// function shares the variables it captured, which in Rust each copy has its own of. One +/// that only borrows them shared, which is `FnMut` where its bound asks for one, changes +/// none of them. fn changes_captures(ty: Ty<'_>) -> bool { match ty.kind() { ty::Closure(_, args) => { let closure = args.as_closure(); - closure.kind() == ty::ClosureKind::FnMut || closure.upvar_tys().iter().any(changes_captures) + closure.upvar_tys().iter().any(|t| { + let shared = matches!(t.kind(), ty::Ref(_, _, ty::Mutability::Not)); + closure.kind() == ty::ClosureKind::FnMut && !shared || changes_captures(t) + }) } ty::Tuple(tys) => tys.iter().any(changes_captures), ty::Array(t, _) => changes_captures(*t), From c681daca8f14ac5572050afe5422ff82a75ace7c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:38:41 -0700 Subject: [PATCH 4/8] wip: fmt Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/task/src/drops.rs | 1 + ts/fixtures/task/src/futures.rs | 1 + ts/fixtures/task/src/model.rs | 1 + ts/src/lower/mod.rs | 4 +++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/fixtures/task/src/drops.rs b/ts/fixtures/task/src/drops.rs index d597a44f4..24ac05b02 100644 --- a/ts/fixtures/task/src/drops.rs +++ b/ts/fixtures/task/src/drops.rs @@ -8,6 +8,7 @@ use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use kio::Consumer; + use crate::log::Log; use crate::model::Guard; diff --git a/ts/fixtures/task/src/futures.rs b/ts/fixtures/task/src/futures.rs index 9d3a10386..2c1a46d1a 100644 --- a/ts/fixtures/task/src/futures.rs +++ b/ts/fixtures/task/src/futures.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use std::task::{Context, Poll, Wake, Waker}; use kio::Consumer; + use crate::log::Log; use crate::model::Guard; diff --git a/ts/fixtures/task/src/model.rs b/ts/fixtures/task/src/model.rs index 428824acc..979ca7e7b 100644 --- a/ts/fixtures/task/src/model.rs +++ b/ts/fixtures/task/src/model.rs @@ -7,6 +7,7 @@ use std::pin::{Pin, pin}; use std::task::Poll; use kio::{Closed, Consumer, Producer, Waiter}; + use crate::log::Log; /// A channel's open producers, and a consumer to clone for each task. diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index 69533ba5c..2fb7ebd64 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -1290,7 +1290,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { let inner = args.type_at(0); let erased = self.erases_to(inner); (erased.pending && erased.outer_pending).then(|| { - format!("`Poll<{inner}>` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending`") + format!( + "`Poll<{inner}>` is not supported: its `Pending` and `Ready` would both be `rt.OuterPending`" + ) }) } library::Std::ControlFlow if library::Flow::of(self.tcx, args) == library::Flow::Option => { From b4c2f78fe09c323679b31f5d561fa5eb42815d35 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:45:19 -0700 Subject: [PATCH 5/8] wip: ? on Poll and Poll> Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 2 +- ts/fixtures/task/expected/core/option.ts | 7 ++ ts/fixtures/task/expected/core/task/poll.ts | 14 ++++ ts/fixtures/task/expected/kio.Consumer.ts | 4 +- ts/fixtures/task/expected/model.Watch.ts | 12 ++- ts/fixtures/task/expected/model.ts | 81 +++++++++++++++------ ts/fixtures/task/src/model.rs | 38 +++++++--- ts/runtime/rt/task.ts | 31 +++++++- ts/src/lower/mir/call.rs | 30 +++++++- ts/tests/task.ts | 2 +- 10 files changed, 177 insertions(+), 44 deletions(-) diff --git a/ts/README.md b/ts/README.md index 03cd6b539..4bd49dfe5 100644 --- a/ts/README.md +++ b/ts/README.md @@ -88,7 +88,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `enum` | a union of objects tagged by variant | | `Option` | `T \| undefined`; `T \| rt.None` where `T` can already be `undefined`, as `()` and `Option` can, so `Option<()>` and `Option>` keep `None` apart from each `Some` | | `Result` | `rt.Result`: `{ ok: true, value }` or `{ ok: false, error }`; `rt.OK` or `rt.ERR` where the payload is `undefined`, as in `Ok(())` | -| `ControlFlow` | `rt.Result`, `Ok` to continue; what `?` branches on is the value it was applied to | +| `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]`, `Vec` | an array; for bytes, `Uint8Array` for `[u8; N]`, `rt.ByteSlice` for `&[u8]` and `&[u8; N]`, and `rt.ByteVec` for `Vec` | | `&T`, `&mut T`, `Box`, `Reverse`, `NonZero` | the value; a `&mut` to a value JS copies, such as a number, an `Option`, or a `String`, is an `rt.Place` | | an iterator | `rt.Iter`, whose adaptors are runtime functions: `rt.iterMap(rt.iterSlice(v), f)` | diff --git a/ts/fixtures/task/expected/core/option.ts b/ts/fixtures/task/expected/core/option.ts index 9caa60071..6f4f6bcbd 100644 --- a/ts/fixtures/task/expected/core/option.ts +++ b/ts/fixtures/task/expected/core/option.ts @@ -86,6 +86,13 @@ export function map$Option_u32_u32_discarded_closure16(self: rt.u32 | undefined, return _0; } +export function map$Option_u64_Result_u64_u64_streamed_closure0_closure0(self: rt.u64 | undefined, f: (a0: rt.u64) => rt.Result): rt.Result | undefined { + if (self === undefined) { + return undefined; + } + return f(self); +} + export function map$Option_usize_Guard_usize_u32_folded_closure103(self: [rt.usize, Guard.Guard] | undefined, f: (a0: [rt.usize, Guard.Guard]) => [rt.usize, rt.u32]): [rt.usize, rt.u32] | undefined { if (self === undefined) { return undefined; diff --git a/ts/fixtures/task/expected/core/task/poll.ts b/ts/fixtures/task/expected/core/task/poll.ts index e976bd694..f7f31203a 100644 --- a/ts/fixtures/task/expected/core/task/poll.ts +++ b/ts/fixtures/task/expected/core/task/poll.ts @@ -2,6 +2,13 @@ import * as rt from "../../rt.ts"; +export function map$Poll_Option_u64_Option_Result_u64_u64_streamed_closure0(self: rt.Poll, f: (a0: rt.u64 | undefined) => rt.Result | undefined): rt.Poll | undefined> { + if (self !== rt.Pending) { + return f(self); + } + return rt.Pending; +} + export function map$Poll_Option_u64_Result_u64_u64_helpers_closure0_closure1_closure0(self: rt.Poll, f: (a0: rt.u64 | undefined) => rt.Result): rt.Poll> { if (self !== rt.Pending) { return f(self); @@ -16,6 +23,13 @@ export function map$Poll_Option_u64_u64_boxed_closure0_closure3_closure0(self: r return rt.Pending; } +export function map$Poll_u64_Option_u64_Some(self: rt.Poll, _: (a0: rt.u64) => rt.u64 | undefined): rt.Poll { + if (self !== rt.Pending) { + return self; + } + return rt.Pending; +} + export function map$Poll_u64_u64_boxed_closure0_closure1_closure0(self: rt.Poll, f: (a0: rt.u64) => rt.u64): rt.Poll { if (self !== rt.Pending) { return f(self); diff --git a/ts/fixtures/task/expected/kio.Consumer.ts b/ts/fixtures/task/expected/kio.Consumer.ts index d9444fde6..a9ea3eb22 100644 --- a/ts/fixtures/task/expected/kio.Consumer.ts +++ b/ts/fixtures/task/expected/kio.Consumer.ts @@ -44,9 +44,9 @@ export function pollRecv(self: Consumer, waiter: Waiter.Waiter): rt.Poll rt.Poll | rt.OuterPending): rt.Poll, Closed.Closed>> { +export function poll$held_closure0_Poll_u64(self: Consumer, waiter: Waiter.Waiter, f: (a0: rt.u64[]) => rt.Poll | rt.OuterPending): rt.Poll, Closed.Closed>> { const state: rt.Mutex = self.state.value; - const _8: rt.Poll | rt.OuterPending = f(state.value.queue); + const _8: rt.Poll | rt.OuterPending = f(state.value.queue); if (_8 !== rt.OuterPending) { return rt.Ok(_8); } diff --git a/ts/fixtures/task/expected/model.Watch.ts b/ts/fixtures/task/expected/model.Watch.ts index e0f72ed80..768acd64a 100644 --- a/ts/fixtures/task/expected/model.Watch.ts +++ b/ts/fixtures/task/expected/model.Watch.ts @@ -3,7 +3,7 @@ import * as rt from "./rt.ts"; import type * as Consumer from "./kio.Consumer.ts"; import type * as Waiter from "./kio.Waiter.ts"; -import { at } from "./model.ts"; +import { streamed, at } from "./model.ts"; /** The value at `index` of a channel, read by [`at`]. */ export class Watch { @@ -20,8 +20,16 @@ export class Watch { } } +/** + * What [`at`] reads, through [`streamed`]: a value of 3 modulo 4 errs, and is + * watched as ten times itself. + */ export function poll$Task(self: Watch, waiter: Waiter.Waiter): rt.Poll { - return at(self.rx, self.index, waiter); + const _3: rt.Result, rt.u64> = streamed(at(self.rx, self.index, waiter)); + if (_3.ok) { + return _3.value; + } + return rt.u64Mul(_3.error, 10); } //# sourceMappingURL=model.Watch.ts.map diff --git a/ts/fixtures/task/expected/model.ts b/ts/fixtures/task/expected/model.ts index 9318ae75f..9e28d9d89 100644 --- a/ts/fixtures/task/expected/model.ts +++ b/ts/fixtures/task/expected/model.ts @@ -2,8 +2,8 @@ import * as rt from "./rt.ts"; import { copy$Tally, drop$ControlFlow_Guard_u32, drop$Result_u32_Guard } from "./lib.ts"; -import { unwrapOr$Option_u64 } from "./core/option.ts"; -import { isReady$Poll_unit, isPending$Poll_Option_u64 } from "./core/task/poll.ts"; +import { unwrapOr$Option_u64, map$Option_u64_Result_u64_u64_streamed_closure0_closure0 } from "./core/option.ts"; +import { map$Poll_Option_u64_Option_Result_u64_u64_streamed_closure0, map$Poll_u64_Option_u64_Some, isReady$Poll_unit, isPending$Poll_Option_u64 } from "./core/task/poll.ts"; import { wait$watch_closure0_closure0_Option_u64, closedFuture, recvFuture } from "./kio.ts"; import type * as Closed from "./kio.Closed.ts"; import * as Consumer from "./kio.Consumer.ts"; @@ -480,6 +480,25 @@ export class watchFuture { } } +/** + * `polled` as a stream's `poll_next` returns it, erring on a value of 3 modulo 4, then + * back through `?`, which continues with its `Pending` and `Ready(None)`. + */ +export function streamed(polled: rt.Poll): rt.Result, rt.u64> { + const _3: rt.Result, rt.u64> = rt.pollOptionBranch(map$Poll_Option_u64_Option_Result_u64_u64_streamed_closure0(polled, (value: rt.u64 | undefined): rt.Result | undefined => { + return map$Option_u64_Result_u64_u64_streamed_closure0_closure0(value, (value2: rt.u64): rt.Result => { + if (!(rt.u64Rem(value2, 4) === 3)) { + return rt.Ok(value2); + } + return rt.Err(value2); + }); + }), rt.Pending, undefined); + if (_3.ok) { + return rt.Ok(_3.value); + } + return _3; +} + /** * The value at `index`, or `None` once the channel closed without it, as moq's * `poll_group` reads its cache: kio's `Consumer::poll` with a closure pending itself, a @@ -487,8 +506,33 @@ export class watchFuture { * one after it hasn't: then it waits on the channel, and the close drops it. */ export function at(rx: Consumer.Consumer, index: rt.usize, waiter: Waiter.Waiter): rt.Poll { - let res2!: rt.Poll; - const res: rt.Poll, Closed.Closed>> = Consumer.poll$at_closure0_Poll_Option_u64(rx, waiter, (queue: rt.u64[]): rt.Poll | rt.OuterPending => { + let res!: rt.Poll; + const _5: rt.Result | rt.OuterPending, Closed.Closed> = held(rx, index, waiter); + if (_5.ok) { + if (_5.value !== rt.OuterPending) { + res = map$Poll_u64_Option_u64_Some(_5.value, (a0: rt.u64): rt.u64 | undefined => { + return a0; + }); + } else { + res = rt.Pending as rt.Poll; + } + } else { + res = undefined as rt.Poll; + } + if (isPending$Poll_Option_u64(res)) { + if (isReady$Poll_unit(Consumer.pollClosed(rx, waiter))) { + return undefined; + } + } + return res; +} + +/** + * [`at`]'s poll, through `?` on what kio's `Consumer::poll` returns, which continues with + * its `Pending`. + */ +export function held(rx: Consumer.Consumer, index: rt.usize, waiter: Waiter.Waiter): rt.Result | rt.OuterPending, Closed.Closed> { + const _4: rt.Result | rt.OuterPending, Closed.Closed> = rt.pollBranch(Consumer.poll$held_closure0_Poll_u64(rx, waiter, (queue: rt.u64[]): rt.Poll | rt.OuterPending => { const _3: rt.u64 | undefined = rt.sliceGet(queue, index); if (_3 === undefined) { return rt.OuterPending; @@ -499,22 +543,11 @@ export function at(rx: Consumer.Consumer, index: rt.usize, waiter: Waiter.Waiter } } return _3; - }); - if (res !== rt.Pending) { - if (res.ok) { - res2 = res.value; - } else { - res2 = undefined as rt.Poll; - } - } else { - res2 = rt.Pending as rt.Poll; - } - if (isPending$Poll_Option_u64(res2)) { - if (isReady$Poll_unit(Consumer.pollClosed(rx, waiter))) { - return undefined; - } + }), rt.OuterPending); + if (_4.ok) { + return rt.Ok(_4.value); } - return res2; + return rt.Err({}); } /** @@ -1872,7 +1905,7 @@ export function turns(log: Log.Log, count: rt.u32): void { let _4!: Guard.Guard; let kept!: Guard.Guard; let pinned!: Guard.Guard; - let held!: Guard.Guard; + let held_!: Guard.Guard; let _34!: Guard.Guard; let boxed!: Guard.Guard; let stacked!: Guard.Guard[] | undefined; @@ -1894,9 +1927,9 @@ export function turns(log: Log.Log, count: rt.u32): void { pinned = Guard.new(rt.u32Add(count, 40), Log.clone$Clone(log)); unwind = 152; Guard.note(pinned, "pinned"); - held = Guard.new(rt.u32Add(count, 50), Log.clone$Clone(log)); + held_ = Guard.new(rt.u32Add(count, 50), Log.clone$Clone(log)); unwind = 151; - Guard.note(held, "borrowed"); + Guard.note(held_, "borrowed"); _34 = Guard.new(rt.u32Add(count, 60), Log.clone$Clone(log)); unwind = 150; Guard.note(_34, "extended"); @@ -1992,7 +2025,7 @@ export function turns(log: Log.Log, count: rt.u32): void { unwind = 151; rt.drop(_34); unwind = 152; - rt.drop(held); + rt.drop(held_); unwind = 153; rt.drop(pinned); unwind = 156; @@ -2066,7 +2099,7 @@ export function turns(log: Log.Log, count: rt.u32): void { } rt.drop(_34); } - rt.drop(held); + rt.drop(held_); } rt.drop(pinned); } diff --git a/ts/fixtures/task/src/model.rs b/ts/fixtures/task/src/model.rs index 979ca7e7b..f0d485020 100644 --- a/ts/fixtures/task/src/model.rs +++ b/ts/fixtures/task/src/model.rs @@ -152,25 +152,32 @@ struct Watch { impl kio::Task for Watch { type Output = Option; + /// What [`at`] reads, through [`streamed`]: a value of 3 modulo 4 errs, and is + /// watched as ten times itself. fn poll(&mut self, waiter: &Waiter) -> Poll> { - at(&self.rx, self.index, waiter) + match streamed(at(&self.rx, self.index, waiter)) { + Ok(polled) => polled, + Err(value) => Poll::Ready(Some(value * 10)), + } } } +/// `polled` as a stream's `poll_next` returns it, erring on a value of 3 modulo 4, then +/// back through `?`, which continues with its `Pending` and `Ready(None)`. +fn streamed(polled: Poll>) -> Result>, u64> { + let item = polled.map(|value| value.map(|value| if value % 4 == 3 { Err(value) } else { Ok(value) })); + Ok(item?) +} + /// The value at `index`, or `None` once the channel closed without it, as moq's /// `poll_group` reads its cache: kio's `Consumer::poll` with a closure pending itself, a /// `Poll>`, outside until the value arrives, and inside while it is even and the /// one after it hasn't: then it waits on the channel, and the close drops it. fn at(rx: &Consumer, index: usize, waiter: &Waiter) -> Poll> { - let res = rx.poll(waiter, |queue| match queue.get(index) { - None => Poll::Pending, - Some(value) if value % 2 == 0 && queue.len() == index + 1 => Poll::Ready(Poll::Pending), - Some(value) => Poll::Ready(Poll::Ready(Some(*value))), - }); - let res = match res { - Poll::Ready(Ok(res)) => res, - Poll::Ready(Err(Closed)) => Poll::Ready(None), - Poll::Pending => Poll::Pending, + let res = match held(rx, index, waiter) { + Ok(Poll::Ready(res)) => res.map(Some), + Ok(Poll::Pending) => Poll::Pending, + Err(Closed) => Poll::Ready(None), }; if res.is_pending() && rx.poll_closed(waiter).is_ready() { return Poll::Ready(None); @@ -178,6 +185,17 @@ fn at(rx: &Consumer, index: usize, waiter: &Waiter) -> Poll> { res } +/// [`at`]'s poll, through `?` on what kio's `Consumer::poll` returns, which continues with +/// its `Pending`. +fn held(rx: &Consumer, index: usize, waiter: &Waiter) -> Result>, Closed> { + let polled = rx.poll(waiter, |queue| match queue.get(index) { + None => Poll::Pending, + Some(value) if value % 2 == 0 && queue.len() == index + 1 => Poll::Ready(Poll::Pending), + Some(value) => Poll::Ready(Poll::Ready(*value)), + })?; + Ok(polled) +} + /// One value read through the captures only an async block makes: a borrow, which /// dropping the block must not drop; a copy, which a later change to the original must /// not reach; a mutable borrow the block replaces whole, and one of a number it writes diff --git a/ts/runtime/rt/task.ts b/ts/runtime/rt/task.ts index 12b94170f..99ae6f826 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 { Err, type None, Ok, Panic, panic, type Place, type Result } from "../rt.ts"; +import { Err, type None, OK, Ok, Panic, panic, type Place, type Result } from "../rt.ts"; // Drops @@ -386,6 +386,35 @@ export const OuterPending: unique symbol = /* @__PURE__ */ Symbol("OuterPending" /** The type of `OuterPending`. */ export type OuterPending = typeof OuterPending; +/** `Ok(Pending)` and `Ok(OuterPending)`, shared as `OK` is. */ +const OK_PENDING = /* @__PURE__ */ Object.freeze({ ok: true, value: Pending } as const); +const OK_OUTER_PENDING = /* @__PURE__ */ Object.freeze({ ok: true, value: OuterPending } as const); + +/** + * `?` on a `Poll>`: the `Result` it is when ready, and `Ok(pending)` when + * `Pending`, where `pending` is the `Pending` of the `Poll` it continues with. + */ +export function pollBranch( + poll: Result | typeof Pending, + pending: P, +): Result { + if (poll !== Pending) return poll; + return (pending === Pending ? OK_PENDING : OK_OUTER_PENDING) as Result; +} + +/** + * `?` on a `Poll>>`, as `pollBranch`, whose `Ready(None)` continues as + * `Ok(none)`, where `none` is the `None` of the `Option`. + */ +export function pollOptionBranch( + poll: Result | undefined | typeof Pending, + pending: P, + none: N, +): Result { + if (poll === undefined) return none === undefined ? (OK as Result) : Ok(none); + return pollBranch(poll, pending); +} + /** `Waker`: what an executor hands a future to wake its task. */ export interface Waker extends Disposable { wake(): void; diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index 317ba8abb..bc1f4c186 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -690,6 +690,27 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { } ("Try", "branch") => { let value = self.operand(&args[0].node, span, out); + // A `Poll`'s `Pending` and `Ready(None)` are no `rt.Result`: they continue. + if let Some(poll) = targ(0) + && let (Some(Std::Poll), ty::Adt(_, poll_args)) = (self.b.t.std_of(poll), poll.kind()) + { + let inner = poll_args.type_at(0); + let ty::Adt(_, inner_args) = inner.kind() else { + unreachable!("`Try` for a `Poll` of a `Result` or an `Option` of one") + }; + let value = match self.b.t.std_of(inner) { + Some(Std::Option) => { + let ty::Adt(_, result_args) = inner_args.type_at(0).kind() else { + unreachable!("`Try` for a `Poll` of an `Option` of a `Result`") + }; + let ok = result_args.type_at(0); + let pending = self.b.t.pending(Ty::new_option(self.tcx, ok)); + Expr::rt("pollOptionBranch", vec![value, pending, self.b.t.none(ok)]) + } + _ => Expr::rt("pollBranch", vec![value, self.b.t.pending(inner_args.type_at(0))]), + }; + return done(value, false); + } // A `next`'s item or `rt.DONE` branches as itself. if let Some(local) = dest.as_local() && self.next_results.contains(&local) @@ -704,12 +725,15 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let to = targ(0)?; let residual = targ(1)?; let value = self.operand(&args[0].node, span, out); - // What a `Result` residual goes into: a `Result`, or one that is ready, which - // is the same value. + // What a `Result` residual goes into: a `Result`, or one that is ready, and some, + // which is the same value. let result = match (self.b.t.std_of(to), to.kind()) { (Some(Std::Result), _) => Some(to), (Some(Std::Poll), ty::Adt(_, poll_args)) => { - let inner = poll_args.type_at(0); + let mut inner = poll_args.type_at(0); + if let (Some(Std::Option), ty::Adt(_, option_args)) = (self.b.t.std_of(inner), inner.kind()) { + inner = option_args.type_at(0); + } (self.b.t.std_of(inner) == Some(Std::Result)).then_some(inner) } _ => None, diff --git a/ts/tests/task.ts b/ts/tests/task.ts index b2cde608a..a990e6b7e 100644 --- a/ts/tests/task.ts +++ b/ts/tests/task.ts @@ -384,7 +384,7 @@ export const cases: Uint8Array[] = [ // kio's API across crates: `Waiter::poll_future`, then a `Pending` of a `Task` polling // a `Poll>`, whose even value waits for the next, or for the close that drops // it; and cancelled while it waits. - script(["open", 0], ["watch", 0], ["run", 0], ["send", 3], ["run", 0], ["send", 4], ["run", 0], ["send", 5], ["run", 0], ["close", 0], ["run", 0]), + script(["open", 0], ["watch", 0], ["run", 0], ["send", 3], ["run", 0], ["send", 4], ["run", 0], ["send", 7], ["run", 0], ["close", 0], ["run", 0]), script(["open", 0], ["watch", 0], ["run", 0], ["send", 1], ["run", 0], ["send", 2], ["run", 0], ["close", 0], ["run", 0]), script(["open", 0], ["watch", 0], ["watch", 0], ["run", 0], ["send", 6], ["step", 0], ["send", 8], ["drop", 1], ["run", 0], ["drop", 0]), // Each scenario of values std holds that drop, panicking at each point or none. From 1162f00fb23cf0932dacfbb31e517949f303c209 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:50:18 -0700 Subject: [PATCH 6/8] wip: verbatim-safe messages Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/mir/call.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index bc1f4c186..b10820cbd 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -696,12 +696,12 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { { let inner = poll_args.type_at(0); let ty::Adt(_, inner_args) = inner.kind() else { - unreachable!("`Try` for a `Poll` of a `Result` or an `Option` of one") + unreachable!("Try for a Poll of a Result or an Option of one") }; let value = match self.b.t.std_of(inner) { Some(Std::Option) => { let ty::Adt(_, result_args) = inner_args.type_at(0).kind() else { - unreachable!("`Try` for a `Poll` of an `Option` of a `Result`") + unreachable!("Try for a Poll of an Option of a Result") }; let ok = result_args.type_at(0); let pending = self.b.t.pending(Ty::new_option(self.tcx, ok)); From 28e07e2205e6c667bd447a4160cc7c61730e54c2 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:53:58 -0700 Subject: [PATCH 7/8] wip: closures owning only stateless captures Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/task/expected/kio.Closed.ts | 6 + ts/fixtures/task/expected/kio.Consumer.ts | 17 ++ ts/fixtures/task/expected/kio.Pending.ts | 13 +- ts/fixtures/task/expected/kio.ts | 50 ++++++ ts/fixtures/task/expected/model.ts | 208 +++++++++++++--------- ts/fixtures/task/kio/src/lib.rs | 9 + ts/fixtures/task/src/model.rs | 3 +- ts/src/lower/mir/place.rs | 16 +- 8 files changed, 228 insertions(+), 94 deletions(-) diff --git a/ts/fixtures/task/expected/kio.Closed.ts b/ts/fixtures/task/expected/kio.Closed.ts index 3fdebbfcd..21d1d20d1 100644 --- a/ts/fixtures/task/expected/kio.Closed.ts +++ b/ts/fixtures/task/expected/kio.Closed.ts @@ -1,5 +1,11 @@ // Generated by rust2-ts. Do not edit. +import * as rt from "./rt.ts"; + +export function fmt$Debug(_self: Closed, f: rt.Formatter): rt.Result { + return rt.fmtWriteStr(f, "Closed"); +} + /** The error of a poll once the channel closed. */ export interface Closed { } diff --git a/ts/fixtures/task/expected/kio.Consumer.ts b/ts/fixtures/task/expected/kio.Consumer.ts index a9ea3eb22..e6d38e679 100644 --- a/ts/fixtures/task/expected/kio.Consumer.ts +++ b/ts/fixtures/task/expected/kio.Consumer.ts @@ -57,6 +57,23 @@ export function poll$held_closure0_Poll_u64(self: Consumer, waiter: Waiter.Waite return rt.Err({}); } +/** + * `f` of every value sent so far, once it is ready, or the error once the channel + * closed and `f` is not, registering `waiter` otherwise. + */ +export function poll$watch_closure0_closure1_usize(self: Consumer, waiter: Waiter.Waiter, f: (a0: rt.u64[]) => rt.Poll): rt.Poll> { + const state: rt.Mutex = self.state.value; + const _8: rt.Poll = f(state.value.queue); + if (_8 !== rt.Pending) { + return rt.Ok(_8); + } + if (!state.value.closed) { + Waiter.register(waiter, state.value.waiters); + return rt.Pending; + } + return rt.Err({}); +} + /** Ready once the channel closed, registering `waiter` until then. */ export function pollClosed(self: Consumer, waiter: Waiter.Waiter): rt.Poll { const state: rt.Mutex = self.state.value; diff --git a/ts/fixtures/task/expected/kio.Pending.ts b/ts/fixtures/task/expected/kio.Pending.ts index 2b6b46438..13e13532e 100644 --- a/ts/fixtures/task/expected/kio.Pending.ts +++ b/ts/fixtures/task/expected/kio.Pending.ts @@ -1,7 +1,8 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { poll$Task_boxed_closure0_closure3, poll$Task_boxed_closure0_closure5, poll$Task_closed_closure0_closure0, poll$Task_recv_closure0_closure0, poll$Task_watch_closure0_closure0 } from "./kio.ts"; +import { poll$Task_boxed_closure0_closure3, poll$Task_boxed_closure0_closure5, poll$Task_closed_closure0_closure0, poll$Task_recv_closure0_closure0, poll$Task_wait_closure0_closure0, poll$Task_watch_closure0_closure0 } from "./kio.ts"; +import type * as Closed from "./kio.Closed.ts"; import * as Park from "./kio.Park.ts"; import type * as Waiter from "./kio.Waiter.ts"; import * as Watch from "./model.Watch.ts"; @@ -51,6 +52,10 @@ export function new$recv_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll return new Pending({ inner, park: Park.default$Default() }); } +export function new$wait_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll>): Pending<(a0: Waiter.Waiter) => rt.Poll>> { + return new Pending({ inner, park: Park.default$Default() }); +} + export function new$watch_closure0_closure0(inner: (a0: Waiter.Waiter) => rt.Poll): Pending<(a0: Waiter.Waiter) => rt.Poll> { return new Pending({ inner, park: Park.default$Default() }); } @@ -95,6 +100,12 @@ export function poll$Future_recv_closure0_closure0(self: Pending<(a0: Waiter.Wai return poll$Task_recv_closure0_closure0(this_.inner, waiter); } +export function poll$Future_wait_closure0_closure0(self: Pending<(a0: Waiter.Waiter) => rt.Poll>>, cx: rt.Context): rt.Poll> { + const this_: Pending<(a0: Waiter.Waiter) => rt.Poll>> = self; + const waiter: Waiter.Waiter = Park.hold(this_.park, cx); + return poll$Task_wait_closure0_closure0(this_.inner, waiter); +} + export function poll$Future_watch_closure0_closure0(self: Pending<(a0: Waiter.Waiter) => rt.Poll>, cx: rt.Context): rt.Poll { const this_: Pending<(a0: Waiter.Waiter) => rt.Poll> = self; const waiter: Waiter.Waiter = Park.hold(this_.park, cx); diff --git a/ts/fixtures/task/expected/kio.ts b/ts/fixtures/task/expected/kio.ts index 01766f7d5..ed699f4c7 100644 --- a/ts/fixtures/task/expected/kio.ts +++ b/ts/fixtures/task/expected/kio.ts @@ -1,6 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; +import type * as Closed from "./kio.Closed.ts"; import * as Consumer from "./kio.Consumer.ts"; import * as Pending from "./kio.Pending.ts"; import type * as Waiter from "./kio.Waiter.ts"; @@ -21,6 +22,10 @@ export function poll$Task_recv_closure0_closure0(self: (a0: Waiter.Waiter) => rt return self(waiter); } +export function poll$Task_wait_closure0_closure0(self: (a0: Waiter.Waiter) => rt.Poll>, waiter: Waiter.Waiter): rt.Poll> { + return self(waiter); +} + export function poll$Task_watch_closure0_closure0(self: (a0: Waiter.Waiter) => rt.Poll, waiter: Waiter.Waiter): rt.Poll { return self(waiter); } @@ -45,11 +50,56 @@ export function wait$recv_closure0_closure0_Option_u64(poll: (a0: Waiter.Waiter) return Pending.new$recv_closure0_closure0(poll); } +/** A future from a poll function that receives a [`Waiter`]. */ +export function wait$wait_closure0_closure0_Result_usize_Closed(poll: (a0: Waiter.Waiter) => rt.Poll>): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll>> { + return Pending.new$wait_closure0_closure0(poll); +} + /** A future from a poll function that receives a [`Waiter`]. */ export function wait$watch_closure0_closure0_Option_u64(poll: (a0: Waiter.Waiter) => rt.Poll): Pending.Pending<(a0: Waiter.Waiter) => rt.Poll> { return Pending.new$watch_closure0_closure0(poll); } +export class waitFuture$watch_closure0_closure1_usize { + #state: number = 0; + #self: Consumer.Consumer; + #f: (a0: rt.u64[]) => rt.Poll; + #awaitee!: Pending.Pending<(a0: Waiter.Waiter) => rt.Poll>>; + + constructor(self: Consumer.Consumer, f: (a0: rt.u64[]) => rt.Poll) { + this.#self = self; + this.#f = f; + } + + [Symbol.dispose](): void { + if (this.#state !== 0) { + if (this.#state === 3) { + rt.drop(this.#awaitee); + } + } + } + + poll(_2: rt.Context): rt.Poll> { + if (this.#state === 0) { + const self: Consumer.Consumer = this.#self; + let captured = this.#f; + this.#awaitee = wait$wait_closure0_closure0_Result_usize_Closed((waiter: Waiter.Waiter): rt.Poll> => { + return Consumer.poll$watch_closure0_closure1_usize(self, waiter, captured); + }); + } else if (this.#state === 1) { + rt.asyncFnResumed(); + } + const _8: rt.Poll> = Pending.poll$Future_wait_closure0_closure0(this.#awaitee, _2); + if (_8 !== rt.Pending) { + rt.drop(this.#awaitee); + this.#state = 1; + return _8; + } + this.#state = 3; + return rt.Pending; + } +} + export class closedFuture { #state: number = 0; #self: Consumer.Consumer; diff --git a/ts/fixtures/task/expected/model.ts b/ts/fixtures/task/expected/model.ts index 9e28d9d89..c6e68d911 100644 --- a/ts/fixtures/task/expected/model.ts +++ b/ts/fixtures/task/expected/model.ts @@ -4,8 +4,8 @@ import * as rt from "./rt.ts"; import { copy$Tally, drop$ControlFlow_Guard_u32, drop$Result_u32_Guard } from "./lib.ts"; import { unwrapOr$Option_u64, map$Option_u64_Result_u64_u64_streamed_closure0_closure0 } from "./core/option.ts"; import { map$Poll_Option_u64_Option_Result_u64_u64_streamed_closure0, map$Poll_u64_Option_u64_Some, isReady$Poll_unit, isPending$Poll_Option_u64 } from "./core/task/poll.ts"; -import { wait$watch_closure0_closure0_Option_u64, closedFuture, recvFuture } from "./kio.ts"; -import type * as Closed from "./kio.Closed.ts"; +import { wait$watch_closure0_closure0_Option_u64, waitFuture$watch_closure0_closure1_usize, closedFuture, recvFuture } from "./kio.ts"; +import * as Closed from "./kio.Closed.ts"; import * as Consumer from "./kio.Consumer.ts"; import * as Pending from "./kio.Pending.ts"; import * as Waiter from "./kio.Waiter.ts"; @@ -200,6 +200,7 @@ export class watchFuture { #awaitee$2!: Pending.Pending; #watch!: Watch.Watch; #awaitee$3!: rt.Future; + #awaitee$4!: rt.Future>; #saved!: boolean; #saved$2!: boolean; @@ -212,76 +213,87 @@ export class watchFuture { let unwind: number = 0; try { b0: { - if (this.#state === 0) { - unwind = 38; - rt.drop(this.#rx); - unwind = 0; - rt.drop(this.#guard); - return; - } - if (this.#state === 3) { - unwind = 24; - rt.drop(this.#awaitee); - unwind = 26; - rt.drop(this.#pinned); - break b0; - } - if (this.#state !== 4) { + b1: { + if (this.#state === 0) { + unwind = 45; + rt.drop(this.#rx); + unwind = 0; + rt.drop(this.#guard); + return; + } + if (this.#state === 3) { + unwind = 31; + rt.drop(this.#awaitee); + unwind = 33; + rt.drop(this.#pinned); + break b0; + } + if (this.#state === 4) { + break b1; + } if (this.#state === 5) { - unwind = 20; + unwind = 26; rt.drop(this.#awaitee$3); - unwind = 22; - rt.drop(this.#watch); + } else if (this.#state === 6) { + unwind = 24; + rt.drop(this.#awaitee$4); } else { return; } + unwind = 29; + rt.drop(this.#watch); } if (this.#saved) { - unwind = 23; + unwind = 30; rt.drop(this.#pending); } this.#saved = false; } - unwind = 28; + unwind = 35; rt.drop(this.#guard); if (this.#saved$2) { - unwind = 29; + unwind = 36; rt.drop(this.#rx); } this.#saved$2 = false; } catch (e) { - b4: { - b5: { - b6: { - b7: { - b8: { - b9: { - if (unwind === 0) { - throw e; - } - if (unwind === 20) { - rt.drop(this.#watch); - break b9; - } - if (unwind === 22) { - break b9; - } - if (unwind === 23) { - break b8; - } - if (unwind === 24) { - rt.drop(this.#pinned); - } else if (unwind !== 26) { - if (unwind === 28) { - break b6; + b5: { + b6: { + b7: { + b8: { + b9: { + b10: { + b11: { + if (unwind === 0) { + throw e; + } + if (unwind === 24) { + break b11; + } + if (unwind === 26) { + break b11; } if (unwind === 29) { + break b10; + } + if (unwind === 30) { + break b9; + } + if (unwind === 31) { + rt.drop(this.#pinned); + } else if (unwind !== 33) { + if (unwind === 35) { + break b7; + } + if (unwind === 36) { + break b6; + } + rt.drop(this.#guard); break b5; } - rt.drop(this.#guard); - break b4; + break b8; } - break b7; + rt.drop(this.#watch); } } this.#saved = false; @@ -299,6 +311,7 @@ export class watchFuture { } poll(_2: rt.Context): rt.Poll { + let value!: rt.u64; let unwind: number = 0; try { let bb0: number = 0; @@ -306,12 +319,12 @@ export class watchFuture { switch (bb0) { case 0: if (this.#state === 0) { - unwind = 68; + unwind = 78; this.#saved$2 = false; this.#saved = false; this.#saved$2 = true; const _3: rt.Future = new recvFuture(this.#rx); - unwind = 67; + unwind = 77; this.#pinned = _3; this.#next = this.#pinned; this.#next$2 = this.#next; @@ -334,27 +347,31 @@ export class watchFuture { bb0 = 24; continue; } - bb0 = 44; + if (this.#state === 5) { + bb0 = 44; + continue; + } + bb0 = 52; continue; } case 6: - unwind = 66; + unwind = 76; const _12: rt.Poll = Pending.poll$Future_watch_closure0_closure0(this.#awaitee, _2); if (_12 !== rt.Pending) { - unwind = 67; + unwind = 77; this.#first = _12; rt.drop(this.#awaitee); - unwind = 68; + unwind = 78; rt.drop(this.#pinned); const arg = this.#first; const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtOption(arg, f, rt.u64Debug)]; const _24: string = rt.fmtFormat(["first ", args2[0]]); - unwind = 65; + unwind = 75; Guard.note(this.#guard, _24); - unwind = 64; + unwind = 74; this.#saved$2 = false; this.#pending = Pending.new$Watch(new Watch.Watch({ rx: this.#rx, index: 1 })); - unwind = 71; + unwind = 81; this.#saved = true; this.#total = unwrapOr$Option_u64(this.#first, 0); bb0 = 22; @@ -363,22 +380,22 @@ export class watchFuture { this.#state = 3; return rt.Pending; case 22: - unwind = 71; + unwind = 81; this.#awaitee$2 = this.#pending; bb0 = 24; continue; case 24: - unwind = 71; + unwind = 81; const _38: rt.Poll = Pending.poll$Future_Watch(this.#awaitee$2, _2); if (_38 !== rt.Pending) { const result2: rt.u64 | undefined = _38; if (result2 !== undefined) { - const value: rt.u64 = result2; + value = result2; const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.u64Display(value, f)]; const _52: string = rt.fmtFormat(["watched ", args4[0]]); - unwind = 63; + unwind = 73; Guard.note(this.#guard, _52); - unwind = 71; + unwind = 81; this.#total = rt.u64Add(this.#total, value); const _62: Watch.Watch = Pending.derefMut$DerefMut_Watch(this.#pending); _62.index = rt.usizeAdd(_62.index, 1); @@ -387,7 +404,7 @@ export class watchFuture { } this.#saved = false; this.#watch = Pending.intoInner$Watch(this.#pending); - unwind = 62; + unwind = 72; this.#awaitee$3 = new closedFuture(this.#watch.rx); bb0 = 44; continue; @@ -395,26 +412,41 @@ export class watchFuture { this.#state = 4; return rt.Pending; case 44: - unwind = 61; + unwind = 71; if (this.#awaitee$3.poll(_2) !== rt.Pending) { - unwind = 62; + unwind = 72; rt.drop(this.#awaitee$3); + this.#awaitee$4 = new waitFuture$watch_closure0_closure1_usize(this.#watch.rx, (queue: rt.u64[]): rt.Poll => { + return queue.length; + }); + bb0 = 52; + continue; + } + this.#state = 5; + return rt.Pending; + case 52: + unwind = 70; + const _82: rt.Poll> = this.#awaitee$4.poll(_2); + if (_82 !== rt.Pending) { + unwind = 72; + const sent: rt.Result = _82; + rt.drop(this.#awaitee$4); const arg2 = this.#watch.index; - const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(arg2, f)]; - const _81: string = rt.fmtFormat(["watched to ", args6[0]]); - unwind = 60; - Guard.note(this.#guard, _81); - unwind = 71; - const _93: rt.u64 = this.#total; + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(arg2, f), (f: rt.Formatter) => rt.fmtResult(sent, f, rt.usizeDebug, (value: Closed.Closed, f: rt.Formatter) => Closed.fmt$Debug(value, f))]; + const _94: string = rt.fmtFormat(["watched to ", args6[0], " of ", args6[1]]); + unwind = 69; + Guard.note(this.#guard, _94); + unwind = 81; + const _109: rt.u64 = this.#total; rt.drop(this.#watch); - unwind = 73; + unwind = 83; this.#saved = false; rt.drop(this.#guard); this.#saved$2 = false; this.#state = 1; - return _93; + return _109; } - this.#state = 5; + this.#state = 6; return rt.Pending; } } @@ -427,32 +459,36 @@ export class watchFuture { if (unwind === 0) { throw e; } - if (unwind === 60) { + if (unwind === 69) { + break b5; + } + if (unwind === 70) { + rt.drop(this.#awaitee$4); break b5; } - if (unwind === 61) { + if (unwind === 71) { rt.drop(this.#awaitee$3); break b5; } - if (unwind === 62) { + if (unwind === 72) { break b5; } - if (unwind === 63) { + if (unwind === 73) { break b4; } - if (unwind === 64) { + if (unwind === 74) { break b3; } - if (unwind === 65) { + if (unwind === 75) { break b2; } - if (unwind === 66) { + if (unwind === 76) { rt.drop(this.#awaitee); - } else if (unwind !== 67) { - if (unwind === 68) { + } else if (unwind !== 77) { + if (unwind === 78) { break b2; } - if (unwind === 71) { + if (unwind === 81) { break b4; } break b1; diff --git a/ts/fixtures/task/kio/src/lib.rs b/ts/fixtures/task/kio/src/lib.rs index 4573be792..a95ecf3c1 100644 --- a/ts/fixtures/task/kio/src/lib.rs +++ b/ts/fixtures/task/kio/src/lib.rs @@ -319,6 +319,15 @@ impl Consumer { Poll::Pending } + /// `f` of every value sent so far, once it is ready, or the error once the channel + /// closed and `f` is not, through a closure that owns `f`. + pub async fn wait(&self, mut f: F) -> Result + where + F: FnMut(&[u64]) -> Poll + Unpin, + { + wait(move |waiter| self.poll(waiter, &mut f)).await + } + /// Wait for the channel to close, through a closure that only borrows `self`. pub async fn closed(&self) { wait(move |waiter| self.poll_closed(waiter)).await diff --git a/ts/fixtures/task/src/model.rs b/ts/fixtures/task/src/model.rs index f0d485020..25686d5df 100644 --- a/ts/fixtures/task/src/model.rs +++ b/ts/fixtures/task/src/model.rs @@ -139,7 +139,8 @@ pub async fn watch(mut rx: Consumer, guard: Guard) -> u64 { } let watch = pending.into_inner(); watch.rx.closed().await; - guard.note(&format!("watched to {}", watch.index)); + let sent = watch.rx.wait(|queue| Poll::Ready(queue.len())).await; + guard.note(&format!("watched to {} of {sent:?}", watch.index)); total } diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index 3f055b25d..c7c2ec0c7 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -1242,17 +1242,21 @@ pub fn transparent<'tcx>(t: &crate::lower::Translator<'_, 'tcx>, ty: Ty<'tcx>) - ) } -/// Whether `ty` holds an `FnMut` closure by value that owns a capture: a copy of its arrow -/// function shares the variables it captured, which in Rust each copy has its own of. One -/// that only borrows them shared, which is `FnMut` where its bound asks for one, changes -/// none of them. +/// Whether `ty` holds an `FnMut` closure by value that owns a capture it could change: a +/// copy of its arrow function shares the variables it captured, which in Rust each copy +/// has its own of. One is `FnMut` where its bound asks for one, so a capture borrowed +/// shared, or a closure capturing nothing, which has no state, changes nothing. fn changes_captures(ty: Ty<'_>) -> bool { match ty.kind() { ty::Closure(_, args) => { let closure = args.as_closure(); closure.upvar_tys().iter().any(|t| { - let shared = matches!(t.kind(), ty::Ref(_, _, ty::Mutability::Not)); - closure.kind() == ty::ClosureKind::FnMut && !shared || changes_captures(t) + let fixed = match t.kind() { + ty::Ref(_, _, ty::Mutability::Not) | ty::FnDef(..) => true, + ty::Closure(_, args) => args.as_closure().upvar_tys().is_empty(), + _ => false, + }; + closure.kind() == ty::ClosureKind::FnMut && !fixed || changes_captures(t) }) } ty::Tuple(tys) => tys.iter().any(changes_captures), From 9d0ca3f21c5c831fa8e5f0b72814f9b9621cc87e Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:13:00 -0700 Subject: [PATCH 8/8] ts: tie closure copies to the captures their MIR writes A copied FnMut closure was allowed whenever its captures were shared references, but rebinding such a reference changes the copy's state. Read the closure's optimized MIR for written captures instead. Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.md | 20 +++++---- ts/fixtures/reject/expected.txt | 2 + ts/fixtures/reject/src/lib.rs | 14 ++++++ ts/fixtures/reject/ts.toml | 1 + ts/src/lower/mir/place.rs | 76 ++++++++++++++++++++++++--------- 5 files changed, 85 insertions(+), 28 deletions(-) diff --git a/ts/fixtures/reject/census.md b/ts/fixtures/reject/census.md index 4b4598ac4..5b376bd9c 100644 --- a/ts/fixtures/reject/census.md +++ b/ts/fixtures/reject/census.md @@ -1,7 +1,7 @@ # Census of `reject-fixture` -- Refusals: 75 -- Function instances: 87 +- Refusals: 77 +- Function instances: 88 - Generic functions no instance reached, lowered over their type parameters: 6 ## By construct @@ -11,6 +11,7 @@ | 11 | unsupported call to `_` | fixtures/reject/src/lib.rs:21:4 | | 4 | a variant of `_`, which is not supported: either is the same JS string | fixtures/reject/src/lib.rs:382:2 | | 4 | an assignment through a reference to `_` | fixtures/reject/src/lib.rs:101:2 | +| 3 | a copy of a closure that changes what it captured | fixtures/reject/src/lib.rs:257:14 | | 3 | replacing the value behind a reference to `_` | fixtures/reject/src/lib.rs:85:2 | | 3 | unsupported type `_` | fixtures/reject/src/lib.rs:7:14 | | 2 | `_` is not supported: its `None` and `Some` would both be `rt.None` | fixtures/reject/src/lib.rs:16:15 | @@ -39,7 +40,6 @@ | 1 | a cast from `_` to `_` | fixtures/reject/src/lib.rs:96:2 | | 1 | a coercion to `_` | fixtures/reject/src/lib.rs:341:2 | | 1 | a constant of type `_` | fixtures/reject/src/lib.rs:80:2 | -| 1 | a copy of a closure that changes what it captured | fixtures/reject/src/lib.rs:257:14 | | 1 | a future of `_`, which can be `rt.Pending`, as the future's own `Pending` is | fixtures/reject/src/lib.rs:110:34 | | 1 | a mutable borrow of a subslice of `_`, which JS would copy, having no views | fixtures/reject/src/lib.rs:226:13 | | 1 | a mutable reference into a `Result`'s payload, passed on or kept | fixtures/reject/src/lib.rs:413:12 | @@ -213,6 +213,14 @@ The type or module each refused type or path belongs to.
+
a copy of a closure that changes what it captured (3) + +- fixtures/reject/src/lib.rs:257:14: a copy of a closure that changes what it captured +- fixtures/reject/src/lib.rs:529:14: a copy of a closure that changes what it captured +- fixtures/reject/src/lib.rs:530:14: a copy of a closure that changes what it captured + +
+
replacing the value behind a reference to `_` (3) - fixtures/reject/src/lib.rs:85:2: replacing the value behind a reference to `Meters` @@ -394,12 +402,6 @@ The type or module each refused type or path belongs to.
-
a copy of a closure that changes what it captured (1) - -- fixtures/reject/src/lib.rs:257:14: a copy of a closure that changes what it captured - -
-
a future of `_`, which can be `rt.Pending`, as the future's own `Pending` is (1) - fixtures/reject/src/lib.rs:110:34: a future of `std::task::Poll`, which can be `rt.Pending`, as the future's own `Pending` is diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 5d5b2d107..ba25bab61 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -67,6 +67,8 @@ a coercion to `std::sync::Weak`, which is not supported yet at fixt a trait object of `[u8; 4]`, which is not supported yet at fixtures/reject/src/lib.rs:496:26 a coercion to `&Tail`, which is not supported yet at fixtures/reject/src/lib.rs:506:30 `Waiting>` is not supported: its field `poll` is `Poll`, typed `rt.Poll`, whose `Pending` would be `rt.OuterPending` as `Poll>` at fixtures/reject/src/lib.rs:517:2 +a copy of a closure that changes what it captured at fixtures/reject/src/lib.rs:529:14 +a copy of a closure that changes what it captured at fixtures/reject/src/lib.rs:530:14 overriding `Iterator::fold`, which std's adaptors call in place of `next` or beside it at fixtures/reject/src/lib.rs:139:2 an iterator of the crate implementing `DoubleEndedIterator`, which is not supported yet at fixtures/reject/src/lib.rs:159:1 overriding `Iterator::size_hint`, which std's adaptors call in place of `next` or beside it at fixtures/reject/src/lib.rs:193:2 diff --git a/ts/fixtures/reject/src/lib.rs b/ts/fixtures/reject/src/lib.rs index 3f474986a..185dce8f0 100644 --- a/ts/fixtures/reject/src/lib.rs +++ b/ts/fixtures/reject/src/lib.rs @@ -516,3 +516,17 @@ pub struct Waiting { pub fn generic_pending(poll: std::task::Poll>) -> bool { Waiting { poll }.poll.is_pending() } + +/// A copy of a closure that rebinds a shared reference it captured: each Rust copy has +/// its own, where JS would share one binding. +pub fn rebound_reference(a: &u32, b: &u32) -> (u32, u32) { + let mut r = a; + let c = move || { + let old = *r; + r = b; + old + }; + let mut x = c; + let mut y = c; + (x(), y()) +} diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index 06500136c..807374b51 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -60,5 +60,6 @@ roots = [ "tail_read", "missing", "generic_pending", + "rebound_reference", ] printable = ["Meters"] diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index c7c2ec0c7..546edadc7 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -5,9 +5,11 @@ //! stays put reads as that place, which is how a `&mut` to a number changes the number. use rustc_abi::{FieldIdx, VariantIdx}; +use rustc_data_structures::fx::FxHashSet; use rustc_middle::mir::interpret::{GlobalId, Scalar}; +use rustc_middle::mir::visit::{MutatingUseContext, PlaceContext, Visitor}; use rustc_middle::mir::{self, ConstValue, Local, Operand, Place, PlaceTy, ProjectionElem}; -use rustc_middle::ty::{self, Instance, Ty}; +use rustc_middle::ty::{self, Instance, Ty, TyCtxt}; use rustc_span::Span; use super::{Env, Fx, Pending, Slot}; @@ -548,7 +550,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let ty = place.ty(&self.mir.local_decls, self.tcx).ty; // Optimized MIR also copies what isn't `Copy`, where it is a move: copy // propagation does in a body without cleanups, as a dependency's are. - if changes_captures(ty) && self.b.t.is_copy(ty) { + if changes_captures(self.tcx, ty) && self.b.t.is_copy(ty) { return self.err(span, "a copy of a closure that changes what it captured"); } let value = self.place_expr(*place, span, out); @@ -1242,30 +1244,66 @@ pub fn transparent<'tcx>(t: &crate::lower::Translator<'_, 'tcx>, ty: Ty<'tcx>) - ) } -/// Whether `ty` holds an `FnMut` closure by value that owns a capture it could change: a -/// copy of its arrow function shares the variables it captured, which in Rust each copy -/// has its own of. One is `FnMut` where its bound asks for one, so a capture borrowed -/// shared, or a closure capturing nothing, which has no state, changes nothing. -fn changes_captures(ty: Ty<'_>) -> bool { +/// Whether `ty` holds an `FnMut` closure by value whose body writes a capture: a copy of +/// its arrow function shares the variables it captured, which in Rust each copy has its +/// own of. A closure is `FnMut` where its bound asks for one, even if it writes nothing, +/// and a capture that holds no state, a fn item or a closure capturing nothing, changes +/// nothing when written. +fn changes_captures<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> bool { match ty.kind() { - ty::Closure(_, args) => { + ty::Closure(def_id, args) => { let closure = args.as_closure(); - closure.upvar_tys().iter().any(|t| { - let fixed = match t.kind() { - ty::Ref(_, _, ty::Mutability::Not) | ty::FnDef(..) => true, - ty::Closure(_, args) => args.as_closure().upvar_tys().is_empty(), - _ => false, - }; - closure.kind() == ty::ClosureKind::FnMut && !fixed || changes_captures(t) - }) + let upvars = closure.upvar_tys(); + let writes = closure.kind() == ty::ClosureKind::FnMut + && (!tcx.is_mir_available(*def_id) + || written_captures(tcx.optimized_mir(*def_id)) + .into_iter() + .any(|field| upvars.get(field.index()).is_none_or(|t| !stateless(*t)))); + writes || upvars.iter().any(|t| changes_captures(tcx, t)) } - ty::Tuple(tys) => tys.iter().any(changes_captures), - ty::Array(t, _) => changes_captures(*t), - ty::Adt(_, args) => args.types().any(changes_captures), + ty::Tuple(tys) => tys.iter().any(|t| changes_captures(tcx, t)), + ty::Array(t, _) => changes_captures(tcx, *t), + ty::Adt(_, args) => args.types().any(|t| changes_captures(tcx, t)), _ => false, } } +/// Whether every value of `ty` is the same, so writing one changes nothing. +fn stateless(ty: Ty<'_>) -> bool { + match ty.kind() { + ty::FnDef(..) => true, + ty::Closure(_, args) => args.as_closure().upvar_tys().is_empty(), + ty::Tuple(tys) => tys.is_empty(), + _ => false, + } +} + +/// The captures a closure's body `mir` writes into, assigns, or borrows mutably, other than +/// to drop them. +fn written_captures(mir: &mir::Body<'_>) -> FxHashSet { + struct Written(FxHashSet); + impl<'tcx> Visitor<'tcx> for Written { + fn visit_place(&mut self, place: &Place<'tcx>, cx: PlaceContext, _: mir::Location) { + if place.local.as_u32() != 1 + || !cx.is_mutating_use() + || cx == PlaceContext::MutatingUse(MutatingUseContext::Drop) + { + return; + } + let projection = match place.projection.as_slice() { + [ProjectionElem::Deref, rest @ ..] => rest, + all => all, + }; + if let [ProjectionElem::Field(field, _), ..] = projection { + self.0.insert(*field); + } + } + } + let mut written = Written(FxHashSet::default()); + written.visit_body(mir); + written.0 +} + /// Whether the JS object of a `ty` is shared or private, so that copying fields into it /// would change other handles, or nothing. fn shared<'tcx>(fx: &Fx<'_, '_, '_, 'tcx>, ty: Ty<'tcx>) -> bool {