findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate
E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped
execution owner; under JSPI a fresh non-suspending JS→wasm entry while
another activation is suspended is structurally safe on its own stack, so
the admission boundary for worker completions is liveness + trap state, not
execution ownership):
- jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`),
canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError +
cross-realm string classification), and runWaitCompletion(site, token,
prepare, inertResult): prepare runs immediately and owns ALL native work;
stale tokens and dead/terminal instances drop loudly without resolving
(resolving would resume the parked frame inside the damaged module); a
trap latches terminal; a plain JS bug resolves inertResult so the wait
fails instead of stranding. beginWait refuses (token 0) when dead/terminal.
- all four delayed completion sites route their native work through the
gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion'
(oce_plugin_stub — the MEMFS cache write moves inside the gate too),
'ngspice request completion' and 'ngspice vector completion'
(sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare,
inertResult 1 = transport error). Every wxWasmBeginWait caller in the
stubs bails on token <= 0.
- deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion,
executionBarrier, the byte-credit native-entry FIFO — completions are
one-shot per wait token and stream volume is bounded at the E-6 transport
credit window. Cross-refs logged for group M (M-2/M-6/M-8).
E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8
gate): js_ngspice_install_events binds the handler to the EXACT installing
module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity),
re-installation replaces a foreign module's handler, a superseded handler
disarms itself, native entry goes through installingModule._malloc/
._pcbjam_ngspice_event (never lexical Module), each dispatch checks
canTouchNative() (loud drop on a dead/terminal module), and a trap on the
per-line entry latches the terminal gate.
Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/
js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export
decode-fault recovery (real onmessageerror transition via failDecode, J-4),
ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7
applied-generation receipt (codex's executionBarrier await replaced with a
pendingWaits('ngspice') drain poll — the JSPI-line equivalent).
Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0b8e7186d4
commit
c14e76651c
9 changed files with 763 additions and 149 deletions
|
|
@ -202,6 +202,13 @@
|
|||
earlyWaitResolves: 0,
|
||||
|
||||
beginWait: function (kind) {
|
||||
if (this.dead || this.terminal) {
|
||||
// Refuse to mint a wait an unhealthy instance can never satisfy.
|
||||
// Callers treat token 0 as "not started" (the C++ bridges bail);
|
||||
// a stray waitPromise(0) settles immediately and warns.
|
||||
this._note("beginWaitRefused", kind, 0);
|
||||
return 0;
|
||||
}
|
||||
var token = ++this.waitSeq;
|
||||
var entry = { kind: kind, resolved: false, resolve: null, promise: null };
|
||||
entry.promise = new Promise(function (resolve) { entry.resolve = resolve; });
|
||||
|
|
@ -274,6 +281,76 @@
|
|||
},
|
||||
|
||||
dead: false,
|
||||
// --- E-8: admission gate for delayed worker/MEMFS completions -----------
|
||||
// `terminal` means the wasm instance TRAPPED (WebAssembly.RuntimeError or
|
||||
// its cross-realm string equivalent): the heap may be mid-mutation, so no
|
||||
// further native work (malloc / heap stores / FS writes) may run and no
|
||||
// parked frame may be resumed into it. Distinct from `dead` (orderly
|
||||
// shutdown). One-way.
|
||||
terminal: false,
|
||||
canTouchNative: function () { return !this.dead && !this.terminal; },
|
||||
_terminalizeNativeTrap: function (site, e) {
|
||||
var isTrap = (typeof WebAssembly !== "undefined"
|
||||
&& WebAssembly.RuntimeError
|
||||
&& e instanceof WebAssembly.RuntimeError)
|
||||
|| /unreachable|memory access out of bounds|index out of bounds|null function or function signature mismatch|Aborted\(/i
|
||||
.test(String((e && e.message) || e));
|
||||
if (!isTrap) return false;
|
||||
if (!this.terminal) {
|
||||
this.terminal = true;
|
||||
this._note("terminal", site, 0);
|
||||
console.error("[wx-scheduler] native trap in " + site
|
||||
+ " — instance is terminal; all further native completions are inert: "
|
||||
+ e);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// The one admission boundary for delayed completions that both touch
|
||||
// native state and wake a parked waiter (the four worker/MEMFS completion
|
||||
// sites: OCC export, OCC model, ngspice request, ngspice vector).
|
||||
// `prepare` runs IMMEDIATELY, never queued — it owns the parked waiter's
|
||||
// output pointers, and queuing it behind anything can deadlock the very
|
||||
// frame this completion wakes. Disposition (every drop is loud, never
|
||||
// silent):
|
||||
// stale/unknown token -> drop + warn (late frame from a retired
|
||||
// worker generation)
|
||||
// dead or terminal instance -> drop + warn, DO NOT resolve — resolving
|
||||
// resumes the suspended frame INSIDE the
|
||||
// damaged module
|
||||
// prepare() traps -> latch terminal, DO NOT resolve
|
||||
// prepare() throws plain JS -> resolve inertResult (fail the wait
|
||||
// rather than strand its parked frame in
|
||||
// a healthy instance)
|
||||
runWaitCompletion: function (site, token, prepare, inertResult) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry || entry.resolved) {
|
||||
console.warn("[wx-scheduler] " + site + ": completion for stale wait "
|
||||
+ token + " dropped");
|
||||
this._note("staleCompletion", site, token);
|
||||
return false;
|
||||
}
|
||||
if (!this.canTouchNative()) {
|
||||
console.warn("[wx-scheduler] " + site + ": completion dropped ("
|
||||
+ (this.terminal ? "terminal" : "dead") + " instance)");
|
||||
this._note("inertCompletion", site, token);
|
||||
return false;
|
||||
}
|
||||
var result;
|
||||
try {
|
||||
result = prepare();
|
||||
} catch (e) {
|
||||
if (this._terminalizeNativeTrap(site, e)) {
|
||||
this._note("completionTrap", site, token);
|
||||
return false;
|
||||
}
|
||||
console.error("[wx-scheduler] " + site + ": completion failed: " + e);
|
||||
this._note("completionError", site, token);
|
||||
this.resolveWait(token, inertResult == null ? 0 : inertResult | 0);
|
||||
return false;
|
||||
}
|
||||
this.resolveWait(token, result | 0);
|
||||
return true;
|
||||
},
|
||||
shutdown: function (why) {
|
||||
this.dead = true;
|
||||
// S6 teardown contract: queued-but-
|
||||
|
|
|
|||
Loading…
Reference in a new issue