WIP star-flip: shim routes context waits; docs 22 records that D forces D5

NOT GREEN. resolveWait marks a context-parked waiter ready and arms a pump
instead of resolving a promise nobody awaits.

The doc records the measured correction: DoRun parks the MAIN stack every
frame in wxWasmYieldToBrowser, which doc 21 called safe-by-construction only
because dispatch also ran there. With the scheduler swapping contexts from the
tick, those interleave over one currData - overlapped-wake, the exact class
this work exists to remove. So the main loop must become a context (D5) and
the bridges (E) join the same flip: D5+D+C+B+E land together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
This commit is contained in:
Gergő Törcsvári 2026-08-07 01:29:56 +02:00
commit 41d49bdacc
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
4 changed files with 74 additions and 2 deletions

View file

@ -197,6 +197,39 @@ KiCad ever called `drain()`); at the flip it is fatal. Therefore:
The tick becomes `fiber_start(dispatch, …)` + `drain_all()` from a fresh JS task.
**MEASURED PLAN CORRECTION (2026-08-06): D forces D5, and E cannot wait.** The wiring
above is implemented on the sub-branch (dispatch context + `wxWasmYieldUntil` yielding
its owner + the shim routing `resolveWait` to `mark_ready` + libcontext's root adopting
the running context + `jump_fcontext` becoming `fiber_transfer`). wx compiles, the
sched-context battery and 41 of 48 wx/asyncify tests stay green — but the coroutine and
coroutine-nested harnesses now fail with
`overlapped-wake: restoring currData=… over null`, the exact class this work exists to
remove, and the trace names the cause:
```
fcs old=… new=… w=0 rf=wxWasmTopLevelTick
fcs old=… new=… ROOT w=0 rf=dynCall_vi
[wx-asyncify] overlapped-wake: restoring currData=… over null
```
`wxGUIEventLoop::DoRun`'s top-level loop parks the MAIN stack every frame in
`wxWasmYieldToBrowser` (doc 21's W2, classified "safe by construction … unless D5 is
taken"). That classification held only while dispatch also ran on the main stack. Once
the scheduler swaps contexts from the tick, the main stack's per-frame Asyncify park and
the scheduler's transitions interleave over one `currData`.
So the one-root constraint is stronger than first written: **the main stack must be the
scheduler and nothing else — which means the main loop itself has to become a context
(D5), not merely dispatch (D).** D5 is therefore no longer optional and no longer
"decide with Phase E telemetry"; it is part of the same flip. By the same argument, any
remaining in-place Asyncify park under a context (doc 21's K1K7 bridges and the T1T3
levers) can reproduce this, so **E belongs in the flip too**, or each bridge must be
proven never to run beneath a context first.
Revised flip contents: **D5 + D + C + B + E**, landed together. That is a bigger single
commit than doc 22 §5 planned, and it is the honest consequence of the measurement —
the alternative (landing D without D5) is the partial migration this document forbids.
### Phase E — bridges on contexts (1 wk)
Doc 21 §1's K1K7 and W4/W5 through one `wasm_await_promise`-style helper. After this **no

2
kicad

@ -1 +1 @@
Subproject commit 73dc150bd459983ece0825fddc0c857c506b5199
Subproject commit e052fb680d0fb446f33b919a8487d30b432e09ad

View file

@ -52,6 +52,24 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
// Deliver via a dedicated PLAIN export call (wxWasmMailboxTick), never
// from inside a pump's awaited ProcessEvents ccall — a fiber swap there
// sits on the JS-awaits-a-suspending-export boundary (#13302) and traps.
// Resume ready contexts from a FRESH task. Separate from the mailbox tick
// because it must run even when the mailbox is empty: a context wake is
// work the pump owns, not a queued message.
_armSchedPump: function () {
if (this._pumpArmed) return;
this._pumpArmed = true;
var self = this;
setTimeout(function () {
self._pumpArmed = false;
if (self.dead) return;
try {
if (Module["_wxWasmSchedPump"]) Module["_wxWasmSchedPump"]();
} catch (e) {
if (Module["_wx_dispatch_abandon"]) Module["_wx_dispatch_abandon"]();
throw e;
}
}, 0);
},
_armDeliveryTick: function () {
if (this._tickArmed) return;
this._tickArmed = true;
@ -187,6 +205,14 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
}
return entry.promise;
},
// doc 22 Phase C: this token's waiter parked a SCHEDULER CONTEXT instead of
// suspending its stack in place, so there is no promise anyone awaits —
// resolving one would strand the context forever. Marked from C++ at park
// time; resolveWait routes such tokens to the registry instead.
noteContextWait: function (token) {
var entry = this.waits.get(token);
if (entry) entry.contextParked = true;
},
resolveWait: function (token, result) {
var entry = this.waits.get(token);
if (!entry || entry.resolved) return false;
@ -198,6 +224,19 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
if (idx !== -1) stack.splice(idx, 1);
}
this.waits.delete(token);
if (entry.contextParked) {
// Mark ready only — never resume inline. The pump picks it up from a
// fresh task, which is doc 13 §1.4's deferred-wake law applied to
// contexts (a rewind inside this resolver's own turn is the whole
// class of bug the scheduler exists to remove).
try {
Module["_wxWasmSchedResolveContextWait"](token, result | 0);
} catch (e) {
console.warn("[wx-scheduler] context wait " + token + " resolve failed: " + e);
}
this._armSchedPump();
return true;
}
entry.resolve(result | 0);
return true;
},

@ -1 +1 @@
Subproject commit f360a1a7f66a2d0433bf8db0db4aa7939507689a
Subproject commit 7b9b1e9e855f05070b2c8df621975ae24b6023db