@polyengine/runtime 0.3.1-pre.g4fe5f4b → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/exec/executor.js +35 -4
- package/esm/intrinsics/context.js +18 -8
- package/esm/task/scheduler.js +70 -0
- package/package.json +1 -1
- package/types/intrinsics/context.d.ts +13 -3
- package/types/task/scheduler.d.ts +45 -0
package/esm/exec/executor.js
CHANGED
|
@@ -82,8 +82,26 @@ class Executor {
|
|
|
82
82
|
* another.
|
|
83
83
|
*/
|
|
84
84
|
store = new Store();
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Memoized `unsafe-intrinsic` core functions, by DECLARING COMPONENT
|
|
87
|
+
* INSTANCE and symbol.
|
|
88
|
+
*
|
|
89
|
+
* The instance is part of the key because `context.{get,set}` resolve their
|
|
90
|
+
* thread against it (`currentThreadForInstance`, task/scheduler.ts): the
|
|
91
|
+
* declaring instance is the one whose core frame is executing when the
|
|
92
|
+
* intrinsic is called, which is what keeps a JSPI continuation chunk's
|
|
93
|
+
* `context.set` out of a sibling task's slots. `null` keys the shared
|
|
94
|
+
* adapter/instance-less flavour (the plan records `instance: null` for FACT
|
|
95
|
+
* adapter modules).
|
|
96
|
+
*/
|
|
86
97
|
unsafeIntrinsics = new Map();
|
|
98
|
+
/**
|
|
99
|
+
* The component instance of the core module currently being instantiated
|
|
100
|
+
* (`instantiate-module`'s `instance` field; null for a FACT adapter). Read
|
|
101
|
+
* by `unsafeIntrinsic` while its import list is being resolved — the one
|
|
102
|
+
* place a core instance's owning component instance is stated by the plan.
|
|
103
|
+
*/
|
|
104
|
+
#declaringInstance = null;
|
|
87
105
|
/** The single in-flight FACT `prepare-call` state (intrinsics/fact_calls.ts). */
|
|
88
106
|
preparedCall = { current: null };
|
|
89
107
|
/**
|
|
@@ -353,6 +371,13 @@ class Executor {
|
|
|
353
371
|
// suspension point; every function it exports is therefore
|
|
354
372
|
// potentially-blocking, and everything else is not.
|
|
355
373
|
this.sawBlockingImport = false;
|
|
374
|
+
// Which component instance this core module belongs to — the plan
|
|
375
|
+
// states it here and nowhere else (`instance: null` = FACT adapter,
|
|
376
|
+
// contracts/plan-format.md). `unsafeIntrinsic` reads it while the
|
|
377
|
+
// import list below is resolved.
|
|
378
|
+
this.#declaringInstance = init.instance === null
|
|
379
|
+
? null
|
|
380
|
+
: this.componentInstance(init.instance);
|
|
356
381
|
// ISSUE #88: core wasm permits two imports with the same
|
|
357
382
|
// (module, field) pair (trusted wasmtime-environ 47.0.3 info.rs
|
|
358
383
|
// :438-445 gives one flat positional CoreDef per import slot, but
|
|
@@ -390,6 +415,10 @@ class Executor {
|
|
|
390
415
|
{})[imp.name] =
|
|
391
416
|
value;
|
|
392
417
|
});
|
|
418
|
+
// Scoped strictly to the import list above: a CoreDef resolved by
|
|
419
|
+
// any other initializer (extract-*, resource dtors) names no core
|
|
420
|
+
// module, so it must not inherit this one's instance.
|
|
421
|
+
this.#declaringInstance = null;
|
|
393
422
|
let instance;
|
|
394
423
|
try {
|
|
395
424
|
instance = await WebAssembly.instantiate(module, importObject);
|
|
@@ -670,10 +699,12 @@ class Executor {
|
|
|
670
699
|
return m;
|
|
671
700
|
}
|
|
672
701
|
unsafeIntrinsic(symbol) {
|
|
673
|
-
|
|
702
|
+
const inst = this.#declaringInstance;
|
|
703
|
+
const key = `${inst === null ? "-" : inst.index}\0${symbol}`;
|
|
704
|
+
let fn = this.unsafeIntrinsics.get(key);
|
|
674
705
|
if (fn === undefined) {
|
|
675
|
-
fn = createUnsafeIntrinsic(symbol);
|
|
676
|
-
this.unsafeIntrinsics.set(
|
|
706
|
+
fn = createUnsafeIntrinsic(symbol, inst);
|
|
707
|
+
this.unsafeIntrinsics.set(key, fn);
|
|
677
708
|
}
|
|
678
709
|
return fn;
|
|
679
710
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// memory access for wasmtime's own internals; they have no portable meaning
|
|
11
11
|
// in a JS host and are refused at instantiate time.
|
|
12
12
|
import { assert_, trapIf } from "../cabi/trap.js";
|
|
13
|
-
import {
|
|
13
|
+
import { currentThreadForInstance } from "../task/mod.js";
|
|
14
14
|
import { ambientDebug, dbgId } from "../task/scheduler.js";
|
|
15
15
|
import { UnsupportedFeatureError } from "./errors.js";
|
|
16
16
|
/**
|
|
@@ -28,8 +28,8 @@ export const NUM_CONTEXT_SLOTS = 2;
|
|
|
28
28
|
* in slot 0, which is why this intrinsic is the entry blocker for async
|
|
29
29
|
* guests.
|
|
30
30
|
*/
|
|
31
|
-
export function canonContextGet(i) {
|
|
32
|
-
const thread =
|
|
31
|
+
export function canonContextGet(i, inst) {
|
|
32
|
+
const thread = currentThreadForInstance(inst);
|
|
33
33
|
assert_(i < NUM_CONTEXT_SLOTS, `context.get slot ${i} out of range`);
|
|
34
34
|
const result = thread.storage[i];
|
|
35
35
|
assert_(result < 2 ** 32, "context.get value out of i32 range");
|
|
@@ -56,8 +56,8 @@ function trace(msg, thread) {
|
|
|
56
56
|
console.error(`[ctx] ${ctxThreadId(thread)} ${msg} storage=${JSON.stringify(thread.storage)} | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${a.claims.map(ctxThreadId).join(",")}]`);
|
|
57
57
|
}
|
|
58
58
|
/** definitions.py `canon_context_set` (line 2358). */
|
|
59
|
-
export function canonContextSet(i, v) {
|
|
60
|
-
const thread =
|
|
59
|
+
export function canonContextSet(i, v, inst) {
|
|
60
|
+
const thread = currentThreadForInstance(inst);
|
|
61
61
|
assert_(i < NUM_CONTEXT_SLOTS, `context.set slot ${i} out of range`);
|
|
62
62
|
if (CTX_TRACE)
|
|
63
63
|
trace(`set[${i}] = ${v >>> 0}`, thread);
|
|
@@ -70,7 +70,17 @@ export function canonContextSet(i, v) {
|
|
|
70
70
|
* unimplementable symbol fails instantiation rather than the first call
|
|
71
71
|
* (contracts/plan-format.md "Executor obligations").
|
|
72
72
|
*/
|
|
73
|
-
export function createUnsafeIntrinsic(symbol
|
|
73
|
+
export function createUnsafeIntrinsic(symbol,
|
|
74
|
+
/**
|
|
75
|
+
* The component instance whose core module declares this import — the
|
|
76
|
+
* instance whose frame is, by construction, the one executing when it is
|
|
77
|
+
* called. `undefined`/`null` (a FACT adapter module, which the plan records
|
|
78
|
+
* with `instance: null`) falls back to the unscoped ambient. See
|
|
79
|
+
* `currentThreadForInstance` (task/scheduler.ts) for why this discriminator
|
|
80
|
+
* is what makes a JSPI continuation chunk's `context.set` land in its own
|
|
81
|
+
* thread's slots.
|
|
82
|
+
*/
|
|
83
|
+
inst) {
|
|
74
84
|
const match = /^context-(get|set)-i32-(\d+)$/.exec(symbol);
|
|
75
85
|
if (match === null) {
|
|
76
86
|
throw new UnsupportedFeatureError("M2", `component imports the unsafe intrinsic '${symbol}', which has no ` +
|
|
@@ -83,8 +93,8 @@ export function createUnsafeIntrinsic(symbol) {
|
|
|
83
93
|
trapIf(slot >= NUM_CONTEXT_SLOTS, `unsafe intrinsic '${symbol}' addresses context slot ${slot}, but a ` +
|
|
84
94
|
`thread has ${NUM_CONTEXT_SLOTS}`);
|
|
85
95
|
if (match[1] === "get")
|
|
86
|
-
return () => canonContextGet(slot);
|
|
96
|
+
return () => canonContextGet(slot, inst);
|
|
87
97
|
return (v) => {
|
|
88
|
-
canonContextSet(slot, (v ?? 0) >>> 0);
|
|
98
|
+
canonContextSet(slot, (v ?? 0) >>> 0, inst);
|
|
89
99
|
};
|
|
90
100
|
}
|
package/esm/task/scheduler.js
CHANGED
|
@@ -567,6 +567,76 @@ export function currentThread() {
|
|
|
567
567
|
export function maybeCurrentThread() {
|
|
568
568
|
return resolveAmbient();
|
|
569
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* THE ambient, NARROWED BY THE INSTANCE WHOSE CORE FRAME IS EXECUTING.
|
|
572
|
+
*
|
|
573
|
+
* For a built-in whose declaration names a component instance, "who is
|
|
574
|
+
* running" is not an open question about the whole store: the call arrived
|
|
575
|
+
* from a core frame OF THAT INSTANCE, so the running activation is one of
|
|
576
|
+
* that instance's. This narrows `resolveAmbient` accordingly — same tiers,
|
|
577
|
+
* same order, candidates filtered — and falls back to the unscoped answer
|
|
578
|
+
* when the instance has no candidate at all (the instantiation-time shape,
|
|
579
|
+
* and any built-in reached before its instance has a task).
|
|
580
|
+
*
|
|
581
|
+
* WHY IT IS NEEDED (polyengine#24's residue; polyvisor#49 trap 1,
|
|
582
|
+
* `runtime/tests/context_attribution_test.ts`). A JSPI continuation chunk —
|
|
583
|
+
* the tail of a suspended activation, e.g. wit-bindgen's callback epilogue
|
|
584
|
+
* restoring its task pointer with `context.set` (rt/async_support.rs:592) —
|
|
585
|
+
* runs with an EMPTY `threadStack` and, unlike a hop, has no re-anchoring
|
|
586
|
+
* edge of its own. Tier 2 then answers the newest claim, which is whichever
|
|
587
|
+
* SIBLING activation suspended most recently. The attribution sentinels
|
|
588
|
+
* (jspi/bridge.ts) plant that claim one microtask ahead of the chunk, which
|
|
589
|
+
* is exact when the engine queues the resumption while the settle reaction
|
|
590
|
+
* returns (measured so in Deno's V8) — and NOT exact in Chromium, where a
|
|
591
|
+
* wider gap lets a sibling's sentinel land in between. Measured there 3/3:
|
|
592
|
+
* one task's epilogue wrote its state pointer into another task's slots, and
|
|
593
|
+
* the starved task's next callback entry hit `assert!(!state.is_null())`
|
|
594
|
+
* (async_support.rs:578) -> unreachable.
|
|
595
|
+
*
|
|
596
|
+
* Ordering discipline cannot fix that class — engine chunk boundaries are not
|
|
597
|
+
* observable, so every microtask-ordering scheme is a hope. Instance identity
|
|
598
|
+
* is not a hope: it is static (the declaration), and it is decisive because
|
|
599
|
+
* ONE INSTANCE CAN ONLY HAVE ONE ACTIVATION MID-FRAME AT A TIME — a callback
|
|
600
|
+
* invocation holds `inst.exclusiveThread` for its whole extent, suspensions
|
|
601
|
+
* included (definitions.py line 2187 / `runCallbackLoop`), and a sync or
|
|
602
|
+
* stackful-async lift holds the entry gate. Two activations that can race for
|
|
603
|
+
* an unbracketed read are therefore necessarily of different instances, which
|
|
604
|
+
* is exactly what this discriminates.
|
|
605
|
+
*
|
|
606
|
+
* SPEC BASIS. `canon_context_get`/`canon_context_set` (definitions.py 2348 /
|
|
607
|
+
* 2358) read `current_thread().storage`, and in the reference a built-in is
|
|
608
|
+
* only ever reached from inside the activation that called it — the identity
|
|
609
|
+
* is exact by construction, never inferred. This runtime has to reconstruct
|
|
610
|
+
* it; narrowing the reconstruction to the declaring instance moves it TOWARD
|
|
611
|
+
* the reference (it can only ever remove candidates the reference would never
|
|
612
|
+
* have named), never away.
|
|
613
|
+
*/
|
|
614
|
+
// deno-lint-ignore no-explicit-any
|
|
615
|
+
export function currentThreadForInstance(inst) {
|
|
616
|
+
const t = resolveAmbientForInstance(inst);
|
|
617
|
+
if (t !== undefined)
|
|
618
|
+
return t;
|
|
619
|
+
// No candidate of this instance: the unscoped ladder, including its
|
|
620
|
+
// `PendingCapability` for the instantiation-time shape.
|
|
621
|
+
return currentThread();
|
|
622
|
+
}
|
|
623
|
+
function resolveAmbientForInstance(inst) {
|
|
624
|
+
if (inst === null || inst === undefined)
|
|
625
|
+
return resolveAmbient();
|
|
626
|
+
const top = threadStack[threadStack.length - 1];
|
|
627
|
+
if (top !== undefined && instOf(top) === inst)
|
|
628
|
+
return top;
|
|
629
|
+
for (let i = activationClaims.length - 1; i >= 0; i--) {
|
|
630
|
+
const c = activationClaims[i];
|
|
631
|
+
if (instOf(c) === inst)
|
|
632
|
+
return c;
|
|
633
|
+
}
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
// deno-lint-ignore no-explicit-any
|
|
637
|
+
function instOf(t) {
|
|
638
|
+
return t?.task?.inst;
|
|
639
|
+
}
|
|
570
640
|
/** definitions.py `current_task()` (line 309). */
|
|
571
641
|
// deno-lint-ignore no-explicit-any
|
|
572
642
|
export function currentTask() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polyengine/runtime",
|
|
3
|
-
"version": "0.3.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A WebAssembly Component Model host for JavaScript engines: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, and embedder API.",
|
|
5
5
|
"homepage": "https://github.com/polymorph-components/polyengine#readme",
|
|
6
6
|
"repository": {
|
|
@@ -14,10 +14,10 @@ export declare const NUM_CONTEXT_SLOTS = 2;
|
|
|
14
14
|
* in slot 0, which is why this intrinsic is the entry blocker for async
|
|
15
15
|
* guests.
|
|
16
16
|
*/
|
|
17
|
-
export declare function canonContextGet(i: number): number;
|
|
17
|
+
export declare function canonContextGet(i: number, inst?: unknown): number;
|
|
18
18
|
export declare function ctxThreadId(t: unknown): string;
|
|
19
19
|
/** definitions.py `canon_context_set` (line 2358). */
|
|
20
|
-
export declare function canonContextSet(i: number, v: number): void;
|
|
20
|
+
export declare function canonContextSet(i: number, v: number, inst?: unknown): void;
|
|
21
21
|
/**
|
|
22
22
|
* Materialize one `unsafe-intrinsic` CoreDef as a core function.
|
|
23
23
|
*
|
|
@@ -25,4 +25,14 @@ export declare function canonContextSet(i: number, v: number): void;
|
|
|
25
25
|
* unimplementable symbol fails instantiation rather than the first call
|
|
26
26
|
* (contracts/plan-format.md "Executor obligations").
|
|
27
27
|
*/
|
|
28
|
-
export declare function createUnsafeIntrinsic(symbol: string
|
|
28
|
+
export declare function createUnsafeIntrinsic(symbol: string,
|
|
29
|
+
/**
|
|
30
|
+
* The component instance whose core module declares this import — the
|
|
31
|
+
* instance whose frame is, by construction, the one executing when it is
|
|
32
|
+
* called. `undefined`/`null` (a FACT adapter module, which the plan records
|
|
33
|
+
* with `instance: null`) falls back to the unscoped ambient. See
|
|
34
|
+
* `currentThreadForInstance` (task/scheduler.ts) for why this discriminator
|
|
35
|
+
* is what makes a JSPI continuation chunk's `context.set` land in its own
|
|
36
|
+
* thread's slots.
|
|
37
|
+
*/
|
|
38
|
+
inst?: unknown): CoreFn;
|
|
@@ -170,6 +170,51 @@ export declare function ambientResidue(): {
|
|
|
170
170
|
};
|
|
171
171
|
export declare function currentThread<T = CurrentThreadLike>(): T;
|
|
172
172
|
export declare function maybeCurrentThread(): CurrentThreadLike | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* THE ambient, NARROWED BY THE INSTANCE WHOSE CORE FRAME IS EXECUTING.
|
|
175
|
+
*
|
|
176
|
+
* For a built-in whose declaration names a component instance, "who is
|
|
177
|
+
* running" is not an open question about the whole store: the call arrived
|
|
178
|
+
* from a core frame OF THAT INSTANCE, so the running activation is one of
|
|
179
|
+
* that instance's. This narrows `resolveAmbient` accordingly — same tiers,
|
|
180
|
+
* same order, candidates filtered — and falls back to the unscoped answer
|
|
181
|
+
* when the instance has no candidate at all (the instantiation-time shape,
|
|
182
|
+
* and any built-in reached before its instance has a task).
|
|
183
|
+
*
|
|
184
|
+
* WHY IT IS NEEDED (polyengine#24's residue; polyvisor#49 trap 1,
|
|
185
|
+
* `runtime/tests/context_attribution_test.ts`). A JSPI continuation chunk —
|
|
186
|
+
* the tail of a suspended activation, e.g. wit-bindgen's callback epilogue
|
|
187
|
+
* restoring its task pointer with `context.set` (rt/async_support.rs:592) —
|
|
188
|
+
* runs with an EMPTY `threadStack` and, unlike a hop, has no re-anchoring
|
|
189
|
+
* edge of its own. Tier 2 then answers the newest claim, which is whichever
|
|
190
|
+
* SIBLING activation suspended most recently. The attribution sentinels
|
|
191
|
+
* (jspi/bridge.ts) plant that claim one microtask ahead of the chunk, which
|
|
192
|
+
* is exact when the engine queues the resumption while the settle reaction
|
|
193
|
+
* returns (measured so in Deno's V8) — and NOT exact in Chromium, where a
|
|
194
|
+
* wider gap lets a sibling's sentinel land in between. Measured there 3/3:
|
|
195
|
+
* one task's epilogue wrote its state pointer into another task's slots, and
|
|
196
|
+
* the starved task's next callback entry hit `assert!(!state.is_null())`
|
|
197
|
+
* (async_support.rs:578) -> unreachable.
|
|
198
|
+
*
|
|
199
|
+
* Ordering discipline cannot fix that class — engine chunk boundaries are not
|
|
200
|
+
* observable, so every microtask-ordering scheme is a hope. Instance identity
|
|
201
|
+
* is not a hope: it is static (the declaration), and it is decisive because
|
|
202
|
+
* ONE INSTANCE CAN ONLY HAVE ONE ACTIVATION MID-FRAME AT A TIME — a callback
|
|
203
|
+
* invocation holds `inst.exclusiveThread` for its whole extent, suspensions
|
|
204
|
+
* included (definitions.py line 2187 / `runCallbackLoop`), and a sync or
|
|
205
|
+
* stackful-async lift holds the entry gate. Two activations that can race for
|
|
206
|
+
* an unbracketed read are therefore necessarily of different instances, which
|
|
207
|
+
* is exactly what this discriminates.
|
|
208
|
+
*
|
|
209
|
+
* SPEC BASIS. `canon_context_get`/`canon_context_set` (definitions.py 2348 /
|
|
210
|
+
* 2358) read `current_thread().storage`, and in the reference a built-in is
|
|
211
|
+
* only ever reached from inside the activation that called it — the identity
|
|
212
|
+
* is exact by construction, never inferred. This runtime has to reconstruct
|
|
213
|
+
* it; narrowing the reconstruction to the declaring instance moves it TOWARD
|
|
214
|
+
* the reference (it can only ever remove candidates the reference would never
|
|
215
|
+
* have named), never away.
|
|
216
|
+
*/
|
|
217
|
+
export declare function currentThreadForInstance<T = CurrentThreadLike>(inst: unknown): T;
|
|
173
218
|
/** definitions.py `current_task()` (line 309). */
|
|
174
219
|
export declare function currentTask(): any;
|
|
175
220
|
/**
|