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
|
|
@ -24,6 +24,14 @@ type SchedulerShape = {
|
|||
mutatorQueue: unknown[];
|
||||
mutatorsDelivered: number;
|
||||
dead: boolean;
|
||||
terminal: boolean;
|
||||
canTouchNative(): boolean;
|
||||
runWaitCompletion(
|
||||
site: string,
|
||||
token: number,
|
||||
prepare: () => number,
|
||||
inertResult?: number,
|
||||
): boolean;
|
||||
shutdown(reason: string): void;
|
||||
enqueueAfter(fn: number, arg: number, ms: number): void;
|
||||
_openBusy(): boolean;
|
||||
|
|
@ -229,3 +237,121 @@ describe("N5: scheduler shim under flood", () => {
|
|||
await expect(S.waitPromise(99999), "unknown token resolves 0").resolves.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("E-8: runWaitCompletion admission gate for worker completions", () => {
|
||||
it("runs prepare immediately and resolves the wait with its result", async () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
const token = S.beginWait("occ");
|
||||
let ran = false;
|
||||
expect(
|
||||
S.runWaitCompletion("test completion", token, () => {
|
||||
ran = true;
|
||||
return 42;
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(ran, "prepare runs immediately, never queued").toBe(true);
|
||||
await expect(S.waitPromise(token)).resolves.toBe(42);
|
||||
});
|
||||
|
||||
it("drops a completion for a stale or already-resolved token, loudly", () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
const prepare = vi.fn(() => 1);
|
||||
expect(S.runWaitCompletion("late frame", 99999, prepare)).toBe(false);
|
||||
const token = S.beginWait("occ");
|
||||
S.resolveWait(token, 7);
|
||||
expect(S.runWaitCompletion("late frame", token, prepare)).toBe(false);
|
||||
expect(prepare, "stale completions never touch native").not.toHaveBeenCalled();
|
||||
expect(warn).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("a dead instance admits no native work and does not resolve the wait", () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
const token = S.beginWait("ngspice");
|
||||
S.shutdown("test");
|
||||
expect(S.canTouchNative()).toBe(false);
|
||||
const prepare = vi.fn(() => 1);
|
||||
expect(S.runWaitCompletion("post-shutdown", token, prepare)).toBe(false);
|
||||
expect(prepare).not.toHaveBeenCalled();
|
||||
expect(S.waitEarlyResolved(token), "wait deliberately not resolved").toBe(0);
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("a trap in prepare latches terminal and never resolves the wait", () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
const token = S.beginWait("occ");
|
||||
expect(
|
||||
S.runWaitCompletion("trapping completion", token, () => {
|
||||
throw new WebAssembly.RuntimeError("memory access out of bounds");
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(S.terminal).toBe(true);
|
||||
expect(S.canTouchNative()).toBe(false);
|
||||
// Resolving would resume the parked frame INSIDE the trapped module.
|
||||
expect(S.waitEarlyResolved(token)).toBe(0);
|
||||
// Every later completion is inert…
|
||||
const prepare = vi.fn(() => 1);
|
||||
const token2Before = S.beginWait("occ");
|
||||
expect(token2Before, "beginWait refuses on a terminal instance").toBe(0);
|
||||
expect(S.runWaitCompletion("after trap", token, prepare)).toBe(false);
|
||||
expect(prepare).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
err.mockRestore();
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("classifies cross-realm trap strings as terminal too", () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
try {
|
||||
const token = S.beginWait("occ");
|
||||
S.runWaitCompletion("cross-realm trap", token, () => {
|
||||
throw new Error("RuntimeError: unreachable");
|
||||
});
|
||||
expect(S.terminal).toBe(true);
|
||||
expect(S.waitEarlyResolved(token)).toBe(0);
|
||||
} finally {
|
||||
err.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("a plain JS bug fails the wait with inertResult instead of stranding it", async () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
try {
|
||||
const token = S.beginWait("ngspice");
|
||||
expect(
|
||||
S.runWaitCompletion(
|
||||
"buggy completion",
|
||||
token,
|
||||
() => {
|
||||
throw new TypeError("res.lines is not iterable");
|
||||
},
|
||||
1,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(S.terminal, "a JS bug is not a trap").toBe(false);
|
||||
await expect(S.waitPromise(token), "wait fails instead of stranding").resolves.toBe(1);
|
||||
} finally {
|
||||
err.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("beginWait refuses (token 0) on a dead instance", () => {
|
||||
const S = loadShim({ busy: () => false });
|
||||
S.shutdown("test");
|
||||
expect(S.beginWait("occ")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue