@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.
@@ -82,8 +82,26 @@ class Executor {
82
82
  * another.
83
83
  */
84
84
  store = new Store();
85
- /** Memoized `unsafe-intrinsic` core functions, by symbol. */
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
- let fn = this.unsafeIntrinsics.get(symbol);
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(symbol, fn);
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 { currentThread } from "../task/mod.js";
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 = currentThread();
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 = currentThread();
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
  }
@@ -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-pre.g4fe5f4b",
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): CoreFn;
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
  /**