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

@ -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;
},