Skip to content

feat(ts): translate Box::pin and dyn Future - #170

Merged
kixelated merged 5 commits into
mainfrom
quest/m2/ts-async
Oct 28, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m2/ts-async

Conversation

@kixelated

@kixelated kixelated commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The next piece of ts-async. Of what the quest listed, Box::pin and Pin<Box<dyn Future>> remove the most moq census refusals that can land now:

  • Box::pin / dyn Future: 50 refusals in moq-net (29 Box::pin, 12 dyn Future types, 6 polls of a Pin<Box<_>> or dyn Future, 3 Pin::as_mut).
  • kio's API across crates (Pending::poll 156, Waiter::poll_future 60, and more) is larger, but it waits on #141 (ts-model/crates).
  • Async closures and rt.spawn's shape refuse nothing: moq-net writes async closures only in #[cfg(test)] code, and spawns only in tests through moq_clock::sim::spawn.

Approach

  • A dyn Future<Output = T> is an rt.Future<T>, the interface every translated future already has: an object with poll(cx) and [Symbol.dispose]. Its object kind is a class, so a &mut dyn Future is the object, not an rt.Place.

  • Box::pin(x) is x, since a box's JS value is what it holds.

  • Unsizing to dyn Future goes only through references, boxes, and pins, whose JS value is what they point at. Any other pointer, such as Arc<dyn Future>, would lose the Arc around the future, so it is refused with its span.

  • An async body, poll_fn, future::pending, or another dyn Future stays as it is. Any other future, such as a struct of the crate's, becomes new rt.DynFuture(value, X.poll$Future, drop). The drop is passed only when the coercion owns the value.

  • A borrowed future in an owning box, as in Box::pin(&mut fut), becomes new rt.DynFuture(fut, rt.futurePoll). Dropping the box then leaves fut alone.

  • A future whose &mut would be an rt.Place (an enum JS copies) is refused with its span.

  • Future::poll on Pin<P>, Box<F>, or &mut F calls Future::poll on what it points at. On a dyn Future it calls the object's poll.

  • A dyn Future drops through its [Symbol.dispose].

  • A trait object in an instance suffix is named dyn_Future_u64, not dynstdfutureFutureOutputu64.

  • Fixed along the way: a move closure that reassigns a capture whose value is a constant (let mut total = 0; poll_fn(move |cx| { total += v; .. })) printed 0 = rt.u64Add(0, v). A constant capture the closure rebinds now gets its own typed let. No other snapshot changed.

  • Test: the task fixture's new futures::boxed, spawned as the harness step boxed, does four things:

    • It awaits a Box::pin(rx.recv()).
    • It awaits a Box::pin(&mut lend) of a guard-owning poll_fn, then polls lend again after the box drops.
    • It holds a Vec<Pin<Box<dyn Future>>> of an async block, kio's Pending (a crate struct, so an rt.DynFuture), and a poll_fn, each owning a guard.
    • It polls them through a Forward struct that delegates poll to its boxed poll_fn, as moq's session Driver does. That poll_fn uses poll_unsized<F: Future + ?Sized>, as kio's Waiter::poll_future does, and polls a pinned &mut dyn Future last.

    Scripts cover returning, cancelling with the queue still full, and a panic inside a boxed future. The differential ran 35,550 scripts with 0 differences, and the unwinding test passes. Two mutations each report 518 differences: skipping rt.DynFuture's drop, and letting a boxed &mut dispose what it borrows. The reject fixture covers Arc<dyn Future>.

Census at the pinned moq revision: moq-net goes from 8,147 to 8,097 refusals, with no new refusal. The two left that name dyn Future are calls to kio::Waiter::poll_future, which wait on #141. The model (2,919) and kio (317) are unchanged: the model binds its boxed-future holders.

Choices made without asking:

  • The runtime name rt.DynFuture, shaped like rt.PollFn.
  • A dyn Future of a type whose &mut is an rt.Place is refused rather than given a place of its own. moq boxes only async bodies and structs.
  • Arc<dyn Future> and other non-transparent pointers are refused. moq-net makes none.
  • The capture fix rides along because the fixture needs it, and it is the same closure-capture path feat(ts): translate std's future helpers and wakers from Arc<impl Wake> #166 touched.
  • futures::boxed is the task harness step boxed, after helpers.

Impact

  • Translator output: Box::pin, dyn Future (boxed, pinned, or borrowed), and polling them now translate instead of being refused.
  • rt.ts gains DynFuture and futurePoll.
  • Breaking: an instance over a trait object is renamed, as in show$dynstdfmtDebug to show$dyn_Debug. Entry added to ts/MIGRATE.md.

Alternatives

  • Make every crate future a class with a poll method, so no wrapper is needed: that changes every impl Future's output from a module function to a method, against how every other trait impl translates.
  • Leave boxed futures to ts-dyn: dyn Future needs no vtable beyond rt.Future, which every future already implements, so it lands here. ts-dyn now says it builds on this.

Follow-ups

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 3 commits October 28, 2026 04:48
Quest-Claim: ebf43987b63391ba
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Async in the TypeScript translator feat(ts): translate Box::pin and dyn Future Oct 28, 2026
@kixelated
kixelated marked this pull request as ready for review October 28, 2026 12:09
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T12:28:30.743655Z fa59bd8 New commits
🔒 Security Review ✅ Completed 2026-10-28T12:31:22.229025Z fa59bd8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f306167725

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ts/src/lower/task.rs Resolved

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: f30616772534543d859ad1c0b6a6b22180572447.

One additional P2 finding below: the new coercion path also accepts non-transparent owning wrappers, but supplies their outer value to the inner future adapter. I also verified the existing borrowed-future disposal finding; it remains applicable and is not duplicated inline.

Direction: reusing rt.Future and a small adapter is simpler than changing every trait implementation into a method. Keep that approach, but preserve ownership boundaries and explicitly refuse wrapper coercions that it cannot represent. The return/cancel/panic fixture coverage is useful; add non-owning erasure and non-transparent-wrapper cases.

Verification: static inspection of lowering, runtime disposal, fixture and generated output only. I did not run the translator, differential tests, or type checks.

(Written by OpenAI)

Comment thread ts/src/lower/mir/rvalue.rs Resolved
kixelated and others added 2 commits October 28, 2026 05:25
… Future behind an Arc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ts/fixtures/task/src/harness.rs
#	ts/tests/task.ts

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: fa59bd8f720d3dd59b99087a5945ea3f1da3d339, against my previous f30616772534543d859ad1c0b6a6b22180572447 review, separating the merged harness-naming base changes.

Both earlier findings are addressed in the inspected code:

  • Non-transparent Arc coercion: coercion now walks only matching references/Box/Pin and refuses Arc with a source span; the reject fixture covers it.
  • Borrowed-future disposal: a borrowed runtime future behind an owning erased box gets a disposer-free adapter. The new fixture and generated output keep the original poll_fn alive for its subsequent poll and eventual drop.

No new actionable bugs found in this update. Direction is sound: restricting unsupported wrapper shapes and reusing the existing adapter keeps the fix focused, without a broader trait-dispatch redesign.

Verification: static inspection of changed lowering/runtime, fixtures, generated output and base integration only; I did not run translation, type checks or differential tests.

(Written by OpenAI)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging. Codex reviewed head fa59bd8 with no findings (👍), CI passes, and quest check --base origin/main passes with ts-async rewritten to its remainder. Both review findings (borrowed-future disposal, non-transparent Arc coercion) were fixed in fa59bd8 and their threads resolved. Main has since gained only #144 (ffi), which does not conflict.

Follow-ups from the PR body, for /quest-plan: Vec::remove under ts-model/std-rest, and FutureExt::now_or_never under ts-model/stock.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit fcd9295 into main Oct 28, 2026
1 check passed
@kixelated
kixelated deleted the quest/m2/ts-async branch October 28, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant