Draft: polyengine issue
Title: Async host import called from an export's initial activation is never delivered if its promise settles in the following microtask checkpoint
Summary
An async-lowered host import called during an async export's initial activation (before any earlier park), whose returned promise settles within the microtask checkpoint right after that activation returns WAIT (having already task.returned), never gets its subtask event delivered. The guest task parks forever. If the same promise settles one macrotask later, everything works.
Observed on rev 22b5d3d from the polyengine-dioxus host (Deno, linkedom, @deltic/runtime/embedder instantiate).
Repro shape
Guest (wit-bindgen 0.60, async-spawn + inter-task-wakeup, Rust): an async func export handle-event whose body
- synchronously runs app code that calls a sync host import (a resource constructor) and then
spawn_locals a task awaiting an async host import method on that resource (join: async func() -> result<string, error>);
- returns (task.return) with nothing else pending.
wit-bindgen's Tasks::poll_next then drains the spawned task, which issues the async import (BLOCKED subtask), and the initial activation returns CALLBACK_CODE_WAIT with the waitable set.
Host: join() is async join() { const r = await this.#outcome; ... } where #outcome is already settled (the script threw synchronously), so the returned promise settles after a couple of microtask hops.
Result matrix, varying how the host promise settles relative to the activation return:
| host-side settle timing |
guest resumed? |
| already-settled inner promise (2-3 microtask hops) |
no |
after await null in the script (a few more hops) |
no |
after setTimeout(r, 0) |
yes |
| same import issued from a later callback (after a stream-write park) |
yes |
Trace
POLYENGINE_DRIVE_TRACE=1 for the failing click:
[drive #21] drive branch=top ready=0 waiting=1{Thread[not-ready]} awaiting=1 hostCalls=1 ...
[drive #22] drive branch=->async(awaiting/pending) ...
[drive #23] driveAsync branch=top ready=0 waiting=1{Thread[not-ready]} awaiting=1 hostCalls=1 ...
[drive #24] driveAsync branch=top ready=2 waiting=2{Thread[READY],Thread[READY]} awaiting=0 hostCalls=1 ... done=true
[drive #25] driveAsync branch=EXIT-done ready=0 waiting=1{Thread[not-ready]} awaiting=1 hostCalls=1 ... done=true
After #25 no driver runs again for this store until unrelated activity, and every later drive shows awaiting=1 ... done=true — an activation promise that never gets serviced. Guest-side stderr confirms the spawned task issued the import and was never resumed (the waker registered on the joined future never fires; no EVENT_SUBTASK reaches the callback).
The hostCalls=1 that persists across the whole run is the mutation-stream retention arm (HostActivity), so at #25 hasRealHostCall is presumably false and the settlement pump (boundary.ts, "The settlement pump: liveness between export calls") does not arm. The export's own driveAsync exits because done() is true (the export has task.returned) while a callback activation is still in awaiting.
Hypothesis: the driver's exit criterion for export calls considers the call resolved and leaves a just-started (or about-to-settle) callback activation for the pump, but the pump only watches real host calls, and the host call in question has already settled and left pendingHostCalls. So neither the driver nor the pump owns the resumption.
Workaround in use
polyengine-dioxus host (host/src/eval.ts) defers recv/join settlement by one macrotask (await new Promise(r => setTimeout(r, 0))) before returning. Costs a macrotask per settlement; to be removed when this is fixed.
Minimal standalone repro
Not yet extracted; the above is reproducible with polyengine-dioxus PR #23's host/tests/eval_component_test.ts after removing the await macrotask() lines in host/src/eval.ts (deno task test then hangs on the click assertions until waitFor times out).
Draft: polyengine issue
Title: Async host import called from an export's initial activation is never delivered if its promise settles in the following microtask checkpoint
Summary
An async-lowered host import called during an async export's initial activation (before any earlier park), whose returned promise settles within the microtask checkpoint right after that activation returns
WAIT(having alreadytask.returned), never gets its subtask event delivered. The guest task parks forever. If the same promise settles one macrotask later, everything works.Observed on rev
22b5d3dfrom thepolyengine-dioxushost (Deno, linkedom,@deltic/runtime/embedderinstantiate).Repro shape
Guest (wit-bindgen 0.60,
async-spawn+inter-task-wakeup, Rust): anasync funcexporthandle-eventwhose bodyspawn_locals a task awaiting an async host import method on that resource (join: async func() -> result<string, error>);wit-bindgen's
Tasks::poll_nextthen drains the spawned task, which issues the async import (BLOCKED subtask), and the initial activation returnsCALLBACK_CODE_WAITwith the waitable set.Host:
join()isasync join() { const r = await this.#outcome; ... }where#outcomeis already settled (the script threw synchronously), so the returned promise settles after a couple of microtask hops.Result matrix, varying how the host promise settles relative to the activation return:
await nullin the script (a few more hops)setTimeout(r, 0)Trace
POLYENGINE_DRIVE_TRACE=1for the failing click:After #25 no driver runs again for this store until unrelated activity, and every later drive shows
awaiting=1 ... done=true— an activation promise that never gets serviced. Guest-side stderr confirms the spawned task issued the import and was never resumed (thewakerregistered on the joined future never fires; noEVENT_SUBTASKreaches the callback).The
hostCalls=1that persists across the whole run is the mutation-stream retention arm (HostActivity), so at #25hasRealHostCallis presumably false and the settlement pump (boundary.ts, "The settlement pump: liveness between export calls") does not arm. The export's owndriveAsyncexits becausedone()is true (the export hastask.returned) while a callback activation is still inawaiting.Hypothesis: the driver's exit criterion for export calls considers the call resolved and leaves a just-started (or about-to-settle) callback activation for the pump, but the pump only watches real host calls, and the host call in question has already settled and left
pendingHostCalls. So neither the driver nor the pump owns the resumption.Workaround in use
polyengine-dioxushost (host/src/eval.ts) defersrecv/joinsettlement by one macrotask (await new Promise(r => setTimeout(r, 0))) before returning. Costs a macrotask per settlement; to be removed when this is fixed.Minimal standalone repro
Not yet extracted; the above is reproducible with
polyengine-dioxusPR #23'shost/tests/eval_component_test.tsafter removing theawait macrotask()lines inhost/src/eval.ts(deno task testthen hangs on the click assertions untilwaitFortimes out).