Repository navigation
feat(ts): translate kio's poll API across crates: Poll<Poll<T>>, ? on a Poll, and closure copies - #183
Conversation
Quest-Claim: b03e7e55f46ed499
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 09c41ff
One actionable correctness issue: the closure-copy relaxation also permits copying a closure that rebinds a captured shared reference (inline).
Overall direction: type-selected pending sentinels and dedicated Poll ? branching fit the existing erasure model without per-poll ready wrappers. The cross-crate fixture exercises the intended kio boundary; keep the closure exception tied to actual capture mutation rather than reference type alone.
Verification: static review of translator/runtime changes, generated snapshots, and fixture coverage; no tests or translation reproducer executed.
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 <noreply@anthropic.com>
|
@codex review (written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 9d0ca3f
Compared with my review of 09c41ff. The prior shared-reference closure-copy finding is fixed: capture-write analysis now detects rebinding, subfield mutation and mutable borrows, with conservative rejection when MIR is unavailable. The new rebound_reference regression expects refusals at both copies.
No new actionable bugs found in this update. Overall direction is sound: checking actual capture mutation preserves the intended kio exception without confusing an immutable referent with an immutable binding; the small analysis is justified over the former type-only rule.
Verification: focused static review of the fix, surrounding copy/closure lowering, and regression snapshots; tests were not run.
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Works on quest/m2/ts-async.md: the piece "kio's API, reached across crates", which #141 unblocked. This PR rewrites the quest to what remains.
Problem
Since #141, kio translates from source as moq-net's dependency. Its poll API still failed to translate, or translated wrong, wherever moq-net uses it:
Poll<Poll<T>>was refused. kio'sConsumer::poll(waiter, f)andConsumerWeak::polltake a closure that returns aPoll<R>. moq'strack.rspoll_grouppasses a closure whoseRis itself aPoll. Ready-of-pending and pending were the samert.Pending, so the translator refused the type.?on aPoll<Result<T, E>>was mistranslated silently. The translator branched on.okof the value?was applied to. Onrt.Pendingthat readundefined, so the code took theErrpath. moq-net does this about 10 times (poll_flush(cx)?,track.poll_recv_group(waiter)?), and every kioConsumer::pollresult can hit it.copyof a non-Copytype. Dependencies build without unwind cleanups, so copy propagation rewrites the move. One hit was wasm-bindgen.FnMutkind its bound asks for, even when it changes nothing. kio'swait(|waiter| self.poll_x(waiter))only borrows&selfshared.Consumer::waitowns a closure with no captures. Both were refused inPending::new.Consumer: moq-net, through kio's
Consumer::poll,ConsumerWeak::poll,Pending,wait, andConsumer::wait.Approach
Poll<T>, whereTcan already be pending, isT | rt.OuterPending, asOption<T>isT | rt.NonewhereTcan beundefined. The type alone decides it, so every function agrees.Erasedgainsouter_pending.Translator::pending(inner).Optionrefuses one;rt.Future'spollreturnsrt.Pendingas its own);Poll<T>field this instance makes pending.?on aPoll<Result<T, E>>branches throughrt.pollBranch(p, pending).Pendingcontinues as a shared frozenOkof thePoll<T>'s own pending, so it doesn't allocate.?on aPoll<Option<Result<T, E>>>branches throughrt.pollOptionBranch, which also continues onReady(None).from_residualinto aPoll<Option<Result<..>>>now translates too, since itsErris the same value. Before, it was refused.copyof a type that isn'tCopylowers as the move it is.FnMutclosure is allowed when its optimized MIR writes no capture, or writes only captures with no state (a fn item, a closure with no captures,()). A closure whose MIR isn't available stays refused.taskfixture's in-cratekiomodule is now its own crate,fixtures/task/kio(libkio), reached across crates as moq-net reaches kio. It gains kio's real shapes:Taskwith its blanket impl forFnMut,Pending<P>withDeref/DerefMut/into_inner,wait() -> impl Future,Waiter::poll_futureof?Sizedfutures,Consumer::poll,poll_closed,closed, andwait.Census at the pinned moq revision
Spans this PR removes, from
just ts censusbefore and after (moq-net, model, kio):59ae88e5)9d0ca3f2, merged with #147)net.tomlmodel.tomlkio.tomlPoll<Poll<Result<Option<group::Consumer>, Error>>>refusals (kio/src/weak.rs:284,:291;moq-net/src/model/track.rs:4429×2);pending.rs:25closure copy. In the model it came frombandwidth.rs:120,:136,group.rs:1691,track.rs:3352, andConsumer::closed; in moq-net, also fromsession.rs's callers ofConsumer::wait.rt/mod.rs:1096closure copy.scopeguard.rs:21, whose guard closures own an allocator and a layout and never write them. No closure-copy refusal remains in either.?fix removes no refusal: that code was mistranslated, not refused.kio.tomlhas no roots, so it lowers kio's generics over their own parameters, and none of these hinge on an instance.What kio still refuses in moq-net is outside this piece:
lock.rsPoisonError::into_innerlog.rs,sync.rsMaybeUninit,UnsafeCell, raw pointerstask.rsTasks, itsRawWakervtable andBox<dyn FnMut>tasksmoq/model.tomlalready bindskio::Taskswaiter.rsCell,thread_local!, andArc::weak_count(ts-shared);[AtomicU64; 8]::default,array::from_fn, and astaticatomiclib.rs,shared.rs<()>::default; aHashMapofOptionTests
task(differential, 20,000 random scripts plus named cases): a newwatchstep drives kio's API across crates.Waiter::poll_futureof a pinnedrecvas adyn Future, underkio::wait.Pendingof a crateTask, reached throughDerefMutbetween awaits, theninto_inner,Consumer::closed, andConsumer::wait.Taskreads through?onConsumer::poll, so aPoll<Poll<u64>>continues throughrt.OuterPending. An even value waits for the next one or for the close that drops it. It also goes through?on aPoll<Option<Result<..>>>.pollBranchreverted to pass-through, the differential reports 80 differences.reject:nested_pollis now three levels deep.pending_outputkeeps its refusal under the new message.generic_pendingrefuses a genericPoll<T>field made pending.rebound_referencerefuses copies of a closure that rebinds a captured&u32.just checkandjust ts testpass on the merged head.quest checkpasses.Impact
rt.OuterPending(and its type),rt.pollBranch, andrt.pollOptionBranch.Poll<T>whereTcan be pending is typedT | rt.OuterPending. Before, it was refused, so no breaking change and noMIGRATE.mdentry.?on aPoll<Result<..>>orPoll<Option<Result<..>>>now calls the runtime instead of passing the value through. This is a fix.README.md: thePollandControlFlowrows, the "Not yet" list, and thetaskfixture's description.Alternatives
{ ready: T }, whenTcan be pending: rejected. It allocates per poll and breaksPoll<T>'s erasure for callers. TheOptionprecedent is a second sentinel chosen by type.Pollinstead of the outer one: rejected. A&Poll<T>into the payload would then read a different value than the samePoll<T>elsewhere.Pendingof a future's ownpollbert.OuterPending: rejected.rt.spawn,rt.DynFuture, and callers' executors readrt.Pending, so such a future is refused instead.Choices made without asking
rt.OuterPending, afterrt.None's role. Alternatives:rt.ReadyPending,rt.Pending2.?-on-Pollfix is in this PR, though no census refusal counted it. It is a silent mistranslation of kio'sConsumer::pollresults, which moq-net reaches.Follow-ups
Waiter::poll_future::<()>"does not normalize" (kio/src/waiter.rs:160, moq-net census only). It comes fromutil.rs'sfuture_taskclosure, which is already refused as aBox<dyn FnMut>. The same call translates in the fixture, so it looks like an artifact of lowering the refused closure. Suggest a quest to find where thedyn Future's type argument turns into().array::from_fn,Defaultof[T; N]and(), astaticatomic (waiter.rs'sNEXT), and aHashMapof anOption(shared.rs). Suggest folding them into std-rest or a new quest.🤖 Generated with Claude Code
(written by Claude Opus 5.5)