@polyengine/runtime 0.1.0-pre.g633468a

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.
Files changed (126) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +23 -0
  3. package/esm/cabi/async_values.js +162 -0
  4. package/esm/cabi/bulk_lists.js +198 -0
  5. package/esm/cabi/context.js +42 -0
  6. package/esm/cabi/flatten.js +145 -0
  7. package/esm/cabi/float.js +67 -0
  8. package/esm/cabi/handles.js +253 -0
  9. package/esm/cabi/layout.js +149 -0
  10. package/esm/cabi/lift.js +196 -0
  11. package/esm/cabi/load.js +146 -0
  12. package/esm/cabi/lower.js +141 -0
  13. package/esm/cabi/memory.js +182 -0
  14. package/esm/cabi/mod.js +22 -0
  15. package/esm/cabi/store.js +186 -0
  16. package/esm/cabi/strings.js +336 -0
  17. package/esm/cabi/trap.js +38 -0
  18. package/esm/cabi/types.js +264 -0
  19. package/esm/cabi/values.js +64 -0
  20. package/esm/cache/core.js +156 -0
  21. package/esm/cache/dir.js +170 -0
  22. package/esm/cache/mod.js +4 -0
  23. package/esm/cache/web.js +136 -0
  24. package/esm/digest/digest.js +332 -0
  25. package/esm/digest/mod.js +3 -0
  26. package/esm/digest/verify.js +129 -0
  27. package/esm/embedder/casing.js +56 -0
  28. package/esm/embedder/copy.js +42 -0
  29. package/esm/embedder/errors.js +26 -0
  30. package/esm/embedder/imports.js +63 -0
  31. package/esm/embedder/instantiate.js +978 -0
  32. package/esm/embedder/mod.js +40 -0
  33. package/esm/embedder/resources.js +406 -0
  34. package/esm/embedder/streams.js +770 -0
  35. package/esm/embedder/values.js +450 -0
  36. package/esm/embedder/version.js +273 -0
  37. package/esm/exec/boundary.js +1902 -0
  38. package/esm/exec/executor.js +1004 -0
  39. package/esm/exec/host_streams.js +818 -0
  40. package/esm/exec/mod.js +4 -0
  41. package/esm/intrinsics/async_builtins.js +510 -0
  42. package/esm/intrinsics/context.js +90 -0
  43. package/esm/intrinsics/errors.js +13 -0
  44. package/esm/intrinsics/fact_calls.js +865 -0
  45. package/esm/intrinsics/mod.js +564 -0
  46. package/esm/intrinsics/stream_builtins.js +578 -0
  47. package/esm/intrinsics/transcode.js +443 -0
  48. package/esm/jspi/bridge.js +579 -0
  49. package/esm/jspi/mechanics.js +89 -0
  50. package/esm/jspi/mod.js +5 -0
  51. package/esm/jspi/suspending.js +15 -0
  52. package/esm/jspi/types.js +29 -0
  53. package/esm/package.json +3 -0
  54. package/esm/plan/format.js +5 -0
  55. package/esm/plan/loader.js +657 -0
  56. package/esm/plan/mod.js +3 -0
  57. package/esm/shim/mod.js +2 -0
  58. package/esm/shim/translator.js +94 -0
  59. package/esm/task/mod.js +483 -0
  60. package/esm/task/scheduler.js +1028 -0
  61. package/esm/task/streams.js +786 -0
  62. package/esm/task/subtask.js +135 -0
  63. package/esm/task/thread.js +255 -0
  64. package/esm/task/waitable.js +144 -0
  65. package/package.json +91 -0
  66. package/types/cabi/async_values.d.ts +35 -0
  67. package/types/cabi/bulk_lists.d.ts +18 -0
  68. package/types/cabi/context.d.ts +59 -0
  69. package/types/cabi/flatten.d.ts +14 -0
  70. package/types/cabi/float.d.ts +14 -0
  71. package/types/cabi/handles.d.ts +70 -0
  72. package/types/cabi/layout.d.ts +13 -0
  73. package/types/cabi/lift.d.ts +25 -0
  74. package/types/cabi/load.d.ts +14 -0
  75. package/types/cabi/lower.d.ts +10 -0
  76. package/types/cabi/memory.d.ts +58 -0
  77. package/types/cabi/mod.d.ts +15 -0
  78. package/types/cabi/store.d.ts +12 -0
  79. package/types/cabi/strings.d.ts +23 -0
  80. package/types/cabi/trap.d.ts +11 -0
  81. package/types/cabi/types.d.ts +206 -0
  82. package/types/cabi/values.d.ts +5 -0
  83. package/types/cache/core.d.ts +97 -0
  84. package/types/cache/dir.d.ts +6 -0
  85. package/types/cache/mod.d.ts +3 -0
  86. package/types/cache/web.d.ts +10 -0
  87. package/types/digest/digest.d.ts +17 -0
  88. package/types/digest/mod.d.ts +2 -0
  89. package/types/digest/verify.d.ts +48 -0
  90. package/types/embedder/casing.d.ts +40 -0
  91. package/types/embedder/copy.d.ts +24 -0
  92. package/types/embedder/errors.d.ts +11 -0
  93. package/types/embedder/imports.d.ts +47 -0
  94. package/types/embedder/instantiate.d.ts +88 -0
  95. package/types/embedder/mod.d.ts +11 -0
  96. package/types/embedder/resources.d.ts +158 -0
  97. package/types/embedder/streams.d.ts +202 -0
  98. package/types/embedder/values.d.ts +70 -0
  99. package/types/embedder/version.d.ts +85 -0
  100. package/types/exec/boundary.d.ts +360 -0
  101. package/types/exec/executor.d.ts +125 -0
  102. package/types/exec/host_streams.d.ts +165 -0
  103. package/types/exec/mod.d.ts +3 -0
  104. package/types/intrinsics/async_builtins.d.ts +69 -0
  105. package/types/intrinsics/context.d.ts +28 -0
  106. package/types/intrinsics/errors.d.ts +5 -0
  107. package/types/intrinsics/fact_calls.d.ts +120 -0
  108. package/types/intrinsics/mod.d.ts +187 -0
  109. package/types/intrinsics/stream_builtins.d.ts +113 -0
  110. package/types/intrinsics/transcode.d.ts +21 -0
  111. package/types/jspi/bridge.d.ts +227 -0
  112. package/types/jspi/mechanics.d.ts +50 -0
  113. package/types/jspi/mod.d.ts +3 -0
  114. package/types/jspi/suspending.d.ts +1 -0
  115. package/types/jspi/types.d.ts +26 -0
  116. package/types/plan/format.d.ts +369 -0
  117. package/types/plan/loader.d.ts +113 -0
  118. package/types/plan/mod.d.ts +2 -0
  119. package/types/shim/mod.d.ts +1 -0
  120. package/types/shim/translator.d.ts +55 -0
  121. package/types/task/mod.d.ts +257 -0
  122. package/types/task/scheduler.d.ts +421 -0
  123. package/types/task/streams.d.ts +370 -0
  124. package/types/task/subtask.d.ts +96 -0
  125. package/types/task/thread.d.ts +73 -0
  126. package/types/task/waitable.d.ts +67 -0
@@ -0,0 +1,1004 @@
1
+ // Plan executor (docs/architecture.md §4.3 item 1): compile sliced core modules and FACT
2
+ // adapters, run the plan's `initializers` strictly in order, wire the
3
+ // component's typed export surface through the task model.
4
+ //
5
+ // Executor obligations implemented per contracts/plan-format.md:
6
+ // - formatVersion validation (via plan loader), fail fast
7
+ // - strict initializer order; semantics per wasmtime GlobalInitializer
8
+ // - instantiate-time (not call-time) failure for unsupported trampolines /
9
+ // ops (milestone-aware, contracts/intrinsics.md)
10
+ // - component hash verification against plan.component
11
+ import { Trap } from "../cabi/trap.js";
12
+ import { ComponentInstanceState, Store } from "../task/mod.js";
13
+ import { anySuspendingImport, assertModeConsistent, chooseMode, isSuspending, planNeedsSuspension, suspendingImport, trampolineCanBlock, trampolineNeedsSuspension, } from "../jspi/mod.js";
14
+ import { loadPlan, PlanError, resourceIndexOfDefined, } from "../plan/loader.js";
15
+ import { CONSTRUCTOR_SYNC_ENTRY, createDtorEntry, createLiftedFunction, createLoweredImport, LiveMemory, newStats, } from "./boundary.js";
16
+ import { createTrampoline, createUnsafeIntrinsic, TranscodeMemory, } from "../intrinsics/mod.js";
17
+ /**
18
+ * Standing probe (CE_COPY_TRACE): log which import made a core instance
19
+ * suspendable — the first question to ask whenever a FACT callee is
20
+ * promising-wrapped that should not be. Read once, permission-safe.
21
+ */
22
+ const SUSPENDABLE_TRACE = (() => {
23
+ try {
24
+ return Deno.env.get("CE_COPY_TRACE") === "1";
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ })();
30
+ /**
31
+ * Identity token for a resource type **defined by the host** and imported by
32
+ * a component (plan `importedResources`). One object per resource type;
33
+ * object identity is the type identity, exactly as for guest-defined
34
+ * resources whose identity is the per-instantiation `ResourceTypeInfo`.
35
+ */
36
+ export class HostResourceType {
37
+ options;
38
+ constructor(options = {}) {
39
+ this.options = options;
40
+ }
41
+ }
42
+ /** Convenience constructor for {@link HostResourceType}. */
43
+ export function hostResourceType(options) {
44
+ return new HostResourceType(options ?? {});
45
+ }
46
+ const moduleCache = new WeakMap();
47
+ export async function instantiateComponent(input) {
48
+ // Re-load per instantiation: resource identity tokens must be fresh per
49
+ // component instance (descriptor-ir.md open item on ResourceTypeInfo).
50
+ // Re-load unless the caller already did (see `InstantiateInput.loadedPlan`).
51
+ const loaded = input.loadedPlan ?? loadPlan(input.plan);
52
+ if (loaded.wire !== input.plan) {
53
+ throw new PlanError("instantiateComponent: `loadedPlan` was converted from a different " +
54
+ "plan document than `plan`");
55
+ }
56
+ const executor = new Executor(loaded, input);
57
+ await executor.verifyComponent();
58
+ await executor.compileModules();
59
+ executor.bindImportedResources();
60
+ await executor.runInitializers();
61
+ return executor.finish();
62
+ }
63
+ class Executor {
64
+ wire;
65
+ loaded;
66
+ componentBytes;
67
+ adapterBytes;
68
+ hostImports;
69
+ verifyHash;
70
+ /** See `InstantiateInput.jspi` and jspi/bridge.ts's invariant. */
71
+ suspensionMode;
72
+ stats = newStats();
73
+ modules = [];
74
+ instances = [];
75
+ componentInstances = new Map();
76
+ /**
77
+ * One scheduler `Store` for the whole component, shared by every component
78
+ * instance in it — matching definitions.py, where a linked graph of
79
+ * `ComponentInstance`s shares the `Store` that owns the waiting-thread list
80
+ * (`ComponentInstance.__init__` takes `store`). A per-instance store would
81
+ * make a thread blocked in one instance invisible to a driving loop in
82
+ * another.
83
+ */
84
+ store = new Store();
85
+ /** Memoized `unsafe-intrinsic` core functions, by symbol. */
86
+ unsafeIntrinsics = new Map();
87
+ /** The single in-flight FACT `prepare-call` state (intrinsics/fact_calls.ts). */
88
+ preparedCall = { current: null };
89
+ /**
90
+ * One `LiveMemory` per `RuntimeMemoryIndex`, memoized.
91
+ *
92
+ * definitions.py's `LiftOptions.equal` (line 643) compares memories by
93
+ * *identity* (`lhs.memory is rhs.memory`), and `canon_task_return` requires
94
+ * the options at the `task.return` site to equal the lifted export's. A
95
+ * fresh wrapper per `resolveOptions` call would make that comparison fail
96
+ * for every component that actually uses a memory — it only ever passed
97
+ * before because the async fixtures in play had `memory: null` on both
98
+ * sides. Memoizing restores wasmtime's semantics, where the comparison is
99
+ * on `RuntimeMemoryIndex`.
100
+ */
101
+ liveMemories = new Map();
102
+ /** Set by the entry/import wrapping sites; checked in `finish`. */
103
+ wrappedEntries = false;
104
+ wrappedImports = false;
105
+ /** Record that an entry / import wrapping site ran under the current mode. */
106
+ noteEntry() {
107
+ if (this.suspensionMode === "jspi")
108
+ this.wrappedEntries = true;
109
+ return this.suspensionMode;
110
+ }
111
+ noteImport() {
112
+ if (this.suspensionMode === "jspi")
113
+ this.wrappedImports = true;
114
+ return this.suspensionMode;
115
+ }
116
+ taskMayBlock = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
117
+ // extract-* landing zones (index spaces per plan-format.md).
118
+ memories = [];
119
+ reallocs = [];
120
+ postReturns = [];
121
+ callbacks = [];
122
+ tables = [];
123
+ /** LoweredIndex -> RuntimeImportIndex (from lower-import initializers). */
124
+ lowerings = new Map();
125
+ trampolineCache = new Map();
126
+ /**
127
+ * ResourceIndex -> the host token bound to it (imported resource types).
128
+ * Surfaced on the component handle for embedder introspection.
129
+ */
130
+ hostResourceTypes = new Map();
131
+ /** In-flight sync cross-component calls (see intrinsics `SyncCallScope`). */
132
+ syncCallStack = [];
133
+ /** In-flight FACT `[async-start]` borrow windows (intrinsics `FactStartScope`). */
134
+ factStartScopes = [];
135
+ /**
136
+ * Core functions exported by a core instance that imports at least one
137
+ * genuinely-blocking trampoline (`trampolineNeedsSuspension`, per
138
+ * DECLARATION — the async form of a copy/cancel built-in never blocks and
139
+ * does not mark) or a function from an already-marked instance. FACT
140
+ * consults this to decide whether a callee needs its own `promising`
141
+ * entry; wrapping one that cannot block forces asynchrony the ABI forbids
142
+ * (an eagerly-completing callee must report RETURNED, not STARTED).
143
+ *
144
+ * Instance granularity is still an over-approximation — a module exporting
145
+ * both a blocking and a non-blocking function marks both — but with two
146
+ * mitigations it no longer produces wrong answers on the official corpus:
147
+ *
148
+ * * per-declaration classification keeps async-form-only importers (and
149
+ * the FACT `[adapter-callee]*` pass-through wrappers reached through
150
+ * them) out of the set entirely;
151
+ * * a needlessly-wrapped callee no longer changes observable state:
152
+ * `async-start-call` parks the caller until the callee is determinate
153
+ * (fact_calls.ts), reconstructing the reference's synchronous
154
+ * run-to-first-block across the engine's microtask hops (jspi pin (j)).
155
+ *
156
+ * Per-FUNCTION reachability (a call-graph pass in the translator, where
157
+ * wasmparser already is) would still shrink the set — as a wrapping-cost
158
+ * optimization now, not a correctness need.
159
+ */
160
+ suspendableFuncs = new WeakSet();
161
+ /** Scratch: set by `importValue` while one module's imports are resolved. */
162
+ sawBlockingImport = false;
163
+ /** LoweredIndex-es whose host functions carry the `suspending()` brand —
164
+ * populated by `buildLoweredImport`, read by `importValue` (A1). */
165
+ suspendableLowerings = new Set();
166
+ /** Host trap held across a FACT exception barrier (see `HostTrapState`). */
167
+ trapState = { pending: undefined };
168
+ /** Export path -> why it has no runtime surface (see `buildExport`). */
169
+ omittedExports = new Map();
170
+ constructor(loaded, input) {
171
+ this.loaded = loaded;
172
+ this.wire = loaded.wire;
173
+ this.componentBytes = input.componentBytes;
174
+ this.adapterBytes = input.adapters ?? new Map();
175
+ this.hostImports = input.imports ?? {};
176
+ this.verifyHash = input.verifyHash ?? true;
177
+ // AUTO-DETECTION IS ON (M2 exit). `chooseMode` picks jspi when the
178
+ // embedder opts in OR when the plan needs suspension: a stackful async
179
+ // lift, or a genuinely blocking built-in — classified per DECLARATION
180
+ // (`trampolineNeedsSuspension`; the async form of a copy/cancel built-in
181
+ // never blocks and is not evidence). An explicit `jspi: false` still
182
+ // forces plain, and a sync-only component never detects as needing
183
+ // suspension, so the M1 synchronous API is untouched (pinned by
184
+ // bridge_test "plain mode: lifted exports still return values" and the
185
+ // planNeedsSuspension(hello) === false pin beside it).
186
+ //
187
+ // The detection-on failure inventory that kept this off is CLOSED — all
188
+ // suspension sites lit, zero failures over the full corpus. What
189
+ // protects each closed class:
190
+ // * STARTED-vs-RETURNED / eager-callee wrapping (big-interleaving's
191
+ // expect-codes, cross-abi's six): per-declaration classification +
192
+ // `async-start-call`'s determinacy park — pinned by
193
+ // tests/jspi/cross_abi_differential_test.ts (KNOWN_DIVERGENT is
194
+ // EMPTY and asserted empty) and fastpath_hop_test.ts (pin (j): the
195
+ // Suspending fast path still defers the continuation);
196
+ // * park/resume of a sync-lowered caller: handshake_test.ts pins;
197
+ // * stall-vs-trap verdicts (incl. the YIELD-spin starvation and the
198
+ // stale-race guard in exec/boundary.ts): deadlock_test.ts pins;
199
+ // * trap poisoning through rejections (`Thread.resumeWith` bracket)
200
+ // and start-function suspension mapping: the conformance suite's
201
+ // builtin-trap-poisons-instance / dont-block-start files, green
202
+ // under detection.
203
+ this.suspensionMode = chooseMode(input.jspi,
204
+ // Auto-detection evidence, two independent sources: the PLAN (a
205
+ // stackful async lift or a blocking built-in — per-declaration), and
206
+ // the IMPORTS RECORD (a `suspending()`-marked host function: the
207
+ // embedder's declared intent to park a sync-lowered frame, which no
208
+ // plan field can express — embedder-api.md amendment A1).
209
+ planNeedsSuspension(loaded.wire) || anySuspendingImport(this.hostImports));
210
+ }
211
+ async verifyComponent() {
212
+ const { sha256, len } = this.wire.component;
213
+ if (this.componentBytes.length !== len) {
214
+ throw new PlanError(`component byte length ${this.componentBytes.length} != plan's ${len}`);
215
+ }
216
+ if (!this.verifyHash)
217
+ return;
218
+ const digest = await crypto.subtle.digest("SHA-256",
219
+ // Pass an ArrayBuffer copy: subtle.digest rejects SharedArrayBuffer
220
+ // views and non-aligned oddities.
221
+ this.componentBytes.slice().buffer);
222
+ const hex = [...new Uint8Array(digest)]
223
+ .map((b) => b.toString(16).padStart(2, "0"))
224
+ .join("");
225
+ if (hex !== sha256) {
226
+ throw new PlanError(`component sha256 mismatch: plan has ${sha256}, bytes are ${hex}`);
227
+ }
228
+ }
229
+ async compileModules() {
230
+ // Reuse compiled modules across instantiations of the same plan.
231
+ // Fresh-instance-per-case suite runs re-instantiate one component
232
+ // thousands of times, and recompiling costs real time per instantiation
233
+ // even when V8's byte-keyed module dedup hits (it still re-hashes every
234
+ // byte — ~7 ms for a 14 MB component). `WebAssembly.Module` is immutable
235
+ // and freely instantiable many times, so reuse cannot change semantics
236
+ // PROVIDED the compile inputs are the same objects: a hit requires the
237
+ // plan (WeakMap key), the component bytes, and every adapter buffer to
238
+ // be identical by reference. In-place *content* mutation of a reused
239
+ // buffer is caught before this runs by `verifyComponent`'s sha256 check
240
+ // whenever `verifyHash` is on (the default); a caller who disables that
241
+ // and mutates reused buffers gets stale modules — the same caller error
242
+ // as mutating them mid-compile today.
243
+ const cached = moduleCache.get(this.wire);
244
+ if (cached !== undefined &&
245
+ cached.componentBytes === this.componentBytes &&
246
+ this.wire.modules.every((m, i) => m.kind === "embedded" ||
247
+ cached.adapterRefs[i] === this.adapterBytes.get(m.file))) {
248
+ this.modules.push(...await cached.modules);
249
+ return;
250
+ }
251
+ const compiled = Promise.all(this.wire.modules.map((m, i) => {
252
+ if (m.kind === "embedded") {
253
+ // Defense-in-depth (polyengine#187): the loader now refuses
254
+ // negative/non-integer `offset`/`len` at load time (loader.ts
255
+ // `validateModule`), but a negative offset silently slices the
256
+ // *wrong* bytes from the tail of `componentBytes`
257
+ // (`Uint8Array.slice` treats negative indices as relative to the
258
+ // end) rather than tripping the old upper-bound-only check —
259
+ // belt-and-braces here in case a `LoadedPlan` ever reaches this
260
+ // path without going through `loadPlan`.
261
+ if (!Number.isInteger(m.offset) || m.offset < 0 ||
262
+ !Number.isInteger(m.len) || m.len < 0) {
263
+ throw new PlanError(`module ${i}: offset/len must be non-negative integers, got ` +
264
+ `offset=${m.offset}, len=${m.len}`);
265
+ }
266
+ const end = m.offset + m.len;
267
+ if (end > this.componentBytes.length) {
268
+ throw new PlanError(`module ${i}: byte range ${m.offset}..${end} exceeds component ` +
269
+ `size ${this.componentBytes.length}`);
270
+ }
271
+ return WebAssembly.compile(this.componentBytes.slice(m.offset, end).buffer);
272
+ }
273
+ const bytes = this.adapterBytes.get(m.file);
274
+ if (!bytes) {
275
+ throw new PlanError(`adapter artifact ${m.file} not provided`);
276
+ }
277
+ if (bytes.length !== m.len) {
278
+ throw new PlanError(`adapter ${m.file}: expected ${m.len} bytes, got ${bytes.length}`);
279
+ }
280
+ return WebAssembly.compile(bytes.slice().buffer);
281
+ }));
282
+ const entry = {
283
+ componentBytes: this.componentBytes,
284
+ adapterRefs: this.wire.modules.map((m) => m.kind === "embedded" ? undefined : this.adapterBytes.get(m.file)),
285
+ modules: compiled,
286
+ };
287
+ moduleCache.set(this.wire, entry);
288
+ // A failed compile must not poison the plan's cache slot.
289
+ void compiled.catch(() => {
290
+ if (moduleCache.get(this.wire) === entry)
291
+ moduleCache.delete(this.wire);
292
+ });
293
+ this.modules.push(...await compiled);
294
+ }
295
+ /**
296
+ * Bind every imported resource type to the `HostResourceType` the embedder
297
+ * supplied at the corresponding import path, before any initializer runs.
298
+ *
299
+ * Identity: all resource *tables* whose `resource` is this imported
300
+ * ResourceIndex share the host token's dtor. `impl` stays null — an
301
+ * imported resource is implemented by the host, not by any component
302
+ * instance in this component, which is what the reference's
303
+ * `ResourceType.impl` means (definitions.py `class ResourceType`).
304
+ */
305
+ bindImportedResources() {
306
+ const imported = this.wire.importedResources ?? [];
307
+ imported.forEach((ir, resourceIndex) => {
308
+ const imp = this.wire.imports[ir.import];
309
+ const label = importLabel(imp.name, imp.path);
310
+ const value = this.lookupHostImport(imp.name, imp.path, label);
311
+ if (!(value instanceof HostResourceType)) {
312
+ throw new PlanError(`host import '${label}' must be a HostResourceType (the component ` +
313
+ `imports a resource type); got ${describe(value)}`);
314
+ }
315
+ const dtor = value.options.dtor;
316
+ this.wire.resourceTables.forEach((table, tableIndex) => {
317
+ if (table.kind !== "concrete" || table.resource !== resourceIndex) {
318
+ return;
319
+ }
320
+ // A type-only import may have no concrete table at all; that is fine,
321
+ // there is simply no runtime state to bind.
322
+ const token = this.loaded.resourceTokens[tableIndex];
323
+ token.impl = null;
324
+ token.dtor = dtor === undefined ? null : (rep) => dtor(rep);
325
+ });
326
+ this.hostResourceTypes.set(resourceIndex, value);
327
+ });
328
+ }
329
+ async runInitializers() {
330
+ for (const init of this.wire.initializers) {
331
+ switch (init.op) {
332
+ case "instantiate-module": {
333
+ const module = this.modules[init.module];
334
+ if (module === undefined) {
335
+ throw new PlanError(`instantiate-module: no module ${init.module}`);
336
+ }
337
+ const declared = WebAssembly.Module.imports(module);
338
+ if (declared.length !== init.args.length) {
339
+ throw new PlanError(`module ${init.module}: ${declared.length} imports but ` +
340
+ `${init.args.length} args in plan`);
341
+ }
342
+ const importObject = {};
343
+ // Per-CORE-INSTANCE suspendability. `planNeedsSuspension` answers
344
+ // the question for a whole component; FACT needs it for the specific
345
+ // callee it is about to invoke, because that is what decides whether
346
+ // the callee must be `promising`-wrapped (see `mkCalleeTask`).
347
+ //
348
+ // The trampoline declarations cannot answer it: `sync-start-call`
349
+ // and `async-start-call` carry no `instance` field (verified against
350
+ // real plans). What CAN answer it is right here -- the import list
351
+ // of the module being instantiated. A core instance whose imports
352
+ // include a blocking trampoline is one whose code can reach a
353
+ // suspension point; every function it exports is therefore
354
+ // potentially-blocking, and everything else is not.
355
+ this.sawBlockingImport = false;
356
+ // ISSUE #88: core wasm permits two imports with the same
357
+ // (module, field) pair (trusted wasmtime-environ 47.0.3 info.rs
358
+ // :438-445 gives one flat positional CoreDef per import slot, but
359
+ // WebAssembly.Module.imports(module) and the JS import object are
360
+ // both keyed by (module, field) name, not by slot). If two slots
361
+ // share a name and resolve to different values, the second object
362
+ // write silently wins and BOTH slots receive the last value — the
363
+ // JS API cannot express per-slot values for duplicate names. Detect
364
+ // this here and fail loudly rather than wire the wrong function in
365
+ // silently; identical values are safe (the API cannot distinguish
366
+ // the slots in that case, so nothing is actually lost).
367
+ const seenAt = new Map();
368
+ declared.forEach((imp, i) => {
369
+ const before = this.sawBlockingImport;
370
+ const value = this.importValue(init.args[i]);
371
+ // Standing probe (CE_COPY_TRACE): which import made this core
372
+ // instance suspendable — the first question to ask whenever a
373
+ // FACT callee is promising-wrapped that should not be.
374
+ if (!before && this.sawBlockingImport && SUSPENDABLE_TRACE) {
375
+ console.error(`[suspendable] module ${init.module}: import ` +
376
+ `${imp.module}.${imp.name} (${JSON.stringify(init.args[i])})`);
377
+ }
378
+ const key = `${imp.module}\0${imp.name}`;
379
+ const prior = seenAt.get(key);
380
+ if (prior !== undefined && prior.value !== value) {
381
+ throw new PlanError(`module ${init.module}: duplicate import ` +
382
+ `${JSON.stringify(imp.module)}.${JSON.stringify(imp.name)} ` +
383
+ `at arg indices ${prior.index} and ${i} resolve to ` +
384
+ `different values (plan args[${prior.index}]=` +
385
+ `${JSON.stringify(init.args[prior.index])}, args[${i}]=` +
386
+ `${JSON.stringify(init.args[i])})`);
387
+ }
388
+ seenAt.set(key, { index: i, value });
389
+ (importObject[imp.module] ??=
390
+ {})[imp.name] =
391
+ value;
392
+ });
393
+ let instance;
394
+ try {
395
+ instance = await WebAssembly.instantiate(module, importObject);
396
+ }
397
+ catch (e) {
398
+ // A SuspendError out of instantiation is a START FUNCTION trying
399
+ // to suspend: instantiation is never a `promising` activation, so
400
+ // ANY suspension-capable call from a start function trips jspi
401
+ // pin (c) ("a Suspending import called outside a promising
402
+ // activation traps unconditionally"). That is precisely the
403
+ // condition the Component Model traps on — a start function is an
404
+ // implicitly synchronous context that may not block — and
405
+ // wasmtime words it as below (`test/async/dont-block-start.wast`
406
+ // asserts the text twice). Deliberately conservative in the same
407
+ // direction as the engine: a wait whose event is already pending
408
+ // still suspends under jspi (pin (j)) and so still traps here,
409
+ // where wasmtime might have completed it; the reference has no
410
+ // model for instantiation-time built-ins at all
411
+ // (`current_thread()` presumes a running task).
412
+ if (e?.constructor?.name ===
413
+ "SuspendError") {
414
+ throw new Trap("cannot block a synchronous task before returning");
415
+ }
416
+ throw e;
417
+ }
418
+ if (this.sawBlockingImport) {
419
+ for (const exported of Object.values(instance.exports)) {
420
+ if (typeof exported === "function") {
421
+ this.suspendableFuncs.add(exported);
422
+ }
423
+ }
424
+ }
425
+ this.instances.push(instance);
426
+ break;
427
+ }
428
+ case "lower-import": {
429
+ // Associates LoweredIndex -> RuntimeImportIndex; the callable side
430
+ // materializes when a lower-import trampoline referencing it is
431
+ // resolved.
432
+ this.lowerings.set(init.index, init.import);
433
+ break;
434
+ }
435
+ case "extract-memory": {
436
+ const value = this.resolveCoreExport(init.export);
437
+ if (!(value instanceof WebAssembly.Memory)) {
438
+ throw new PlanError(`extract-memory ${init.index}: resolved to non-memory`);
439
+ }
440
+ this.memories[init.index] = value;
441
+ break;
442
+ }
443
+ case "extract-realloc": {
444
+ this.reallocs[init.index] = this.resolveFunction(init.def, `extract-realloc ${init.index}`);
445
+ break;
446
+ }
447
+ case "extract-callback": {
448
+ this.callbacks[init.index] = this.resolveFunction(init.def, `extract-callback ${init.index}`);
449
+ break;
450
+ }
451
+ case "extract-post-return": {
452
+ this.postReturns[init.index] = this.resolveFunction(init.def, `extract-post-return ${init.index}`);
453
+ break;
454
+ }
455
+ case "extract-table": {
456
+ const value = this.resolveCoreExport(init.export);
457
+ if (!(value instanceof WebAssembly.Table)) {
458
+ throw new PlanError(`extract-table ${init.index}: resolved to non-table`);
459
+ }
460
+ this.tables[init.index] = value;
461
+ break;
462
+ }
463
+ case "resource": {
464
+ // Wire the dtor + implementing instance into every concrete
465
+ // resource-table token for this defined resource
466
+ // (tolerate-if-unreferenced for M0; plan-format.md open item).
467
+ const dtor = init.dtor === null
468
+ ? null
469
+ : this.resolveFunction(init.dtor, `resource ${init.index} dtor`);
470
+ const inst = this.componentInstance(init.instance);
471
+ // `init.index` is a DefinedResourceIndex; resource *tables* key off
472
+ // the component-wide ResourceIndex, which counts imported resources
473
+ // first (plan-format.md v0.1 amendment #2 / v0.2
474
+ // `importedResources`; wasmtime `Component::resource_index`).
475
+ const resourceIndex = resourceIndexOfDefined(this.loaded, init.index);
476
+ this.wire.resourceTables.forEach((table, tableIndex) => {
477
+ if (table.kind === "concrete" && table.resource === resourceIndex) {
478
+ const token = this.loaded.resourceTokens[tableIndex];
479
+ token.impl = inst;
480
+ token.dtor = dtor === null ? null : (rep) => {
481
+ dtor(rep);
482
+ };
483
+ // #85/#160: the host-initiated-drop entry. A host-initiated
484
+ // drop is a full canonical LIFT of the dtor (definitions.py
485
+ // `canon_resource_drop`, line 2319), so it is built here with
486
+ // the same harness every lifted export uses — that is what
487
+ // gives the dtor's activation a real Task/Thread, and what
488
+ // releases the impl instance's entry bracket at the first park
489
+ // instead of holding it across the whole activation (#160).
490
+ //
491
+ // The `promising` entry wrapping (docs §7: in jspi mode a dtor
492
+ // may legally reach a `Suspending` import) is applied INSIDE
493
+ // `createLiftedFunction` per `suspensionMode`, and only when
494
+ // the dtor is suspension-capable (`suspendableFuncs`: its core
495
+ // instance imports a blocking trampoline). A non-suspendable
496
+ // dtor cannot legally suspend, so the plain entry is exact for
497
+ // it and avoids `promising`'s unconditional microtask hop
498
+ // (jspi pin (j)). The hop no longer risks a drop-then-call
499
+ // trap either way — the bracket is released before the drive,
500
+ // and the hop-quiescence entry gate covers the sequence — but
501
+ // the plain path stays the cheaper and more deterministic one.
502
+ //
503
+ // `WebAssembly.promising` rejects non-wasm callables (a dtor
504
+ // CoreDef can resolve to a JS trampoline) with a TypeError;
505
+ // fall back to the plain entry, where `awaitCore` still parks
506
+ // on a returned Promise. Deliberately does NOT set
507
+ // `wrappedEntries`: `finish()`'s invariant inventories the two
508
+ // primary wrapping sites; this is an auxiliary entry.
509
+ const suspendable = dtor !== null &&
510
+ this.suspensionMode === "jspi" &&
511
+ this.suspendableFuncs.has(dtor);
512
+ const mkEntry = (mode) => createDtorEntry({
513
+ name: `[dtor] resource ${init.index}`,
514
+ dtor,
515
+ instance: inst,
516
+ suspensionMode: mode,
517
+ stats: this.stats,
518
+ trapState: this.trapState,
519
+ syncCallStack: this.syncCallStack,
520
+ allInstances: () => this.componentInstances.values(),
521
+ });
522
+ try {
523
+ token.dtorHost = mkEntry(suspendable ? "jspi" : "plain");
524
+ }
525
+ catch {
526
+ token.dtorHost = mkEntry("plain");
527
+ }
528
+ }
529
+ });
530
+ break;
531
+ }
532
+ default: {
533
+ const exhaustive = init;
534
+ throw new PlanError(`unsupported initializer op ${exhaustive.op}`);
535
+ }
536
+ }
537
+ }
538
+ }
539
+ finish() {
540
+ const exports = {};
541
+ for (const exp of this.wire.exports) {
542
+ const built = this.buildExport(exp, exp.name);
543
+ if (built.kind === "value")
544
+ exports[exp.name] = built.value;
545
+ }
546
+ // Structural check of jspi/bridge.ts's invariant, run once both wrapping
547
+ // sites have had their chance: entries are wrapped while building exports
548
+ // (just above) and imports while running `instantiate-module`. Neither
549
+ // flag can be set by accident — only the wrapping helpers set them.
550
+ assertModeConsistent(this.suspensionMode, this.wrappedEntries, this.wrappedImports);
551
+ const componentInstances = [];
552
+ for (const [i, state] of this.componentInstances) {
553
+ componentInstances[i] = state;
554
+ }
555
+ return {
556
+ exports,
557
+ stats: this.stats,
558
+ componentInstances,
559
+ coreInstances: this.instances,
560
+ suspendableFuncs: this.suspendableFuncs,
561
+ taskMayBlock: this.taskMayBlock,
562
+ hostResourceTypes: this.hostResourceTypes,
563
+ omittedExports: this.omittedExports,
564
+ loadedPlan: this.loaded,
565
+ };
566
+ }
567
+ // -- export surface -------------------------------------------------------
568
+ /**
569
+ * Materialize one plan export.
570
+ *
571
+ * The result is an explicit discriminated union rather than
572
+ * `unknown | undefined`: an earlier `if (built !== undefined)` filter meant
573
+ * *any* path that happened to yield `undefined` removed the export from the
574
+ * component's surface with no diagnostic anywhere. Only `type` exports are
575
+ * legitimately absent from the runtime surface, and they say so with a
576
+ * reason that is recorded on the handle (`omittedExports`); everything else
577
+ * either produces a value or throws.
578
+ */
579
+ buildExport(exp, path) {
580
+ switch (exp.kind) {
581
+ case "lifted-func": {
582
+ const ft = this.funcType(exp.type, `export '${path}'`);
583
+ const core = this.resolveFunction(exp.coreDef, `export '${path}'`);
584
+ const opts = this.resolveOptions(exp.options);
585
+ const value = createLiftedFunction({
586
+ name: path,
587
+ ft,
588
+ opts,
589
+ core,
590
+ stats: this.stats,
591
+ suspensionMode: this.noteEntry(),
592
+ trapState: this.trapState,
593
+ syncCallStack: this.syncCallStack,
594
+ allInstances: () => this.componentInstances.values(),
595
+ });
596
+ // Constructor exports additionally carry a plain-entered variant
597
+ // (see CONSTRUCTOR_SYNC_ENTRY): in jspi mode the promising-wrapped
598
+ // entry above necessarily returns a Promise, which a JS class
599
+ // constructor cannot await. Deliberately NOT noteEntry()-recorded —
600
+ // this is the one documented exception to the bridge invariant
601
+ // (entries wrapped iff imports wrapped), safe because a
602
+ // synchronously-completing activation never reaches the Suspending
603
+ // seam.
604
+ if (this.suspensionMode === "jspi" &&
605
+ exp.name.startsWith("[constructor]")) {
606
+ value[CONSTRUCTOR_SYNC_ENTRY] = createLiftedFunction({
607
+ name: `${path} (sync entry)`,
608
+ ft,
609
+ opts,
610
+ core,
611
+ stats: this.stats,
612
+ suspensionMode: "plain",
613
+ trapState: this.trapState,
614
+ syncCallStack: this.syncCallStack,
615
+ allInstances: () => this.componentInstances.values(),
616
+ });
617
+ }
618
+ return { kind: "value", value };
619
+ }
620
+ case "instance": {
621
+ const nested = {};
622
+ for (const sub of exp.exports) {
623
+ const built = this.buildExport(sub, `${path}/${sub.name}`);
624
+ if (built.kind === "value")
625
+ nested[sub.name] = built.value;
626
+ }
627
+ return { kind: "value", value: nested };
628
+ }
629
+ case "type":
630
+ // Informational (plan-format.md): an exported *type* has no callable
631
+ // runtime surface. Recorded, not silently dropped.
632
+ this.omittedExports.set(path, "type export: no runtime surface (plan-format.md)");
633
+ return {
634
+ kind: "omitted",
635
+ reason: "type export: no runtime surface",
636
+ };
637
+ case "module": {
638
+ // plan-format.md v4 amendment 2: an exported embedded core module
639
+ // surfaces as the already-compiled `WebAssembly.Module` — the same
640
+ // compilation `instantiate-module` initializers use.
641
+ const module = this.modules[exp.module];
642
+ if (module === undefined) {
643
+ throw new PlanError(`export '${path}': no module ${exp.module} in the static ` +
644
+ `module space (${this.modules.length} modules)`);
645
+ }
646
+ return { kind: "value", value: module };
647
+ }
648
+ default: {
649
+ const exhaustive = exp;
650
+ throw new PlanError(`unsupported export kind ${exhaustive.kind}`);
651
+ }
652
+ }
653
+ }
654
+ // -- resolution -----------------------------------------------------------
655
+ componentInstance(index) {
656
+ let state = this.componentInstances.get(index);
657
+ if (state === undefined) {
658
+ state = new ComponentInstanceState(index, this.store);
659
+ this.componentInstances.set(index, state);
660
+ }
661
+ return state;
662
+ }
663
+ /** Memoized `LiveMemory` for a `RuntimeMemoryIndex` (see `liveMemories`). */
664
+ liveMemory(index) {
665
+ let m = this.liveMemories.get(index);
666
+ if (m === undefined) {
667
+ m = new LiveMemory(() => this.memories[index], `memory ${index}`);
668
+ this.liveMemories.set(index, m);
669
+ }
670
+ return m;
671
+ }
672
+ unsafeIntrinsic(symbol) {
673
+ let fn = this.unsafeIntrinsics.get(symbol);
674
+ if (fn === undefined) {
675
+ fn = createUnsafeIntrinsic(symbol);
676
+ this.unsafeIntrinsics.set(symbol, fn);
677
+ }
678
+ return fn;
679
+ }
680
+ /**
681
+ * Resolve a core-instantiation argument.
682
+ *
683
+ * Identical to `resolveCoreDef` except that in jspi mode a *blocking-capable*
684
+ * trampoline is handed to wasm as a `WebAssembly.Suspending`, so that
685
+ * returning a Promise from it suspends the calling activation instead of
686
+ * trapping. Only this path wraps: the same trampoline resolved anywhere the
687
+ * host will *call* it from JS (extract-callback, post-return, realloc) must
688
+ * stay an ordinary function.
689
+ */
690
+ importValue(def) {
691
+ const value = this.resolveCoreDef(def);
692
+ // Suspendability is TRANSITIVE. FACT does not put blocking trampolines in
693
+ // the guest's own module: it generates an adapter module that imports
694
+ // them, and the guest imports the adapter's exported function. So a core
695
+ // instance is suspendable if it imports a blocking trampoline OR imports a
696
+ // function from an already-suspendable instance. Missing this closure is
697
+ // what made `async-calls-sync`'s sync-lifted middle look non-blocking and
698
+ // broke the handshake pins.
699
+ if (typeof value === "function" &&
700
+ this.suspendableFuncs.has(value)) {
701
+ this.sawBlockingImport = true;
702
+ }
703
+ if (this.suspensionMode !== "jspi" || def.kind !== "trampoline" ||
704
+ typeof value !== "function") {
705
+ return value;
706
+ }
707
+ const decl = this.wire.trampolines[def.index];
708
+ // Per-DECLARATION blocking classification (jspi/bridge.ts): the async
709
+ // form of a copy/cancel built-in never blocks, so importing one neither
710
+ // needs a `Suspending` wrap nor marks the importer suspendable. The
711
+ // kind-only version of this test pulled every async-form consumer into
712
+ // `suspendableFuncs`, promising-wrapping FACT callees that complete
713
+ // eagerly — the STARTED-vs-RETURNED and missed-synchronous-cancellation
714
+ // divergences big-interleaving-test.wast asserts against.
715
+ if (decl === undefined)
716
+ return value;
717
+ const optionsAsync = (i) => this.wire.canonicalOptions[i]?.async === true;
718
+ const d = decl;
719
+ // Host lowers (A1): `trampolineCanBlock` classifies DECLARATIONS and a
720
+ // `lower-import` declaration says nothing about the host's intent — the
721
+ // evidence is the `suspending()` brand on the host function, recorded by
722
+ // `buildLoweredImport` into `suspendableLowerings` (which resolving this
723
+ // very def just populated, one frame down). A marked lower is a genuine
724
+ // blocker: it marks the importer (transitive suspendability →
725
+ // promising-wrapped entries, satisfying jspi pin (c)) and gets the
726
+ // Suspending wrap so a returned Promise parks the frame instead of
727
+ // tripping the boundary's guard.
728
+ if (d.kind === "lower-import") {
729
+ const lowered = d.lowered;
730
+ if (!this.suspendableLowerings.has(lowered))
731
+ return value;
732
+ this.sawBlockingImport = true;
733
+ this.noteImport();
734
+ return suspendingImport(value, "jspi");
735
+ }
736
+ if (!trampolineCanBlock(d, optionsAsync))
737
+ return value;
738
+ // `async-start-call` is wrapped (its jspi-only determinacy park must be
739
+ // able to suspend the caller) but does NOT mark the importer: see
740
+ // `trampolineCanBlock` in jspi/bridge.ts for why marking on it is wrong.
741
+ if (trampolineNeedsSuspension(d, optionsAsync)) {
742
+ this.sawBlockingImport = true;
743
+ }
744
+ this.noteImport();
745
+ return suspendingImport(value, "jspi");
746
+ }
747
+ resolveCoreDef(def) {
748
+ switch (def.kind) {
749
+ case "export": {
750
+ return this.resolveCoreExport({
751
+ instance: def.instance,
752
+ item: def.item,
753
+ });
754
+ }
755
+ case "instance-flags":
756
+ return this.componentInstance(def.instance).flags;
757
+ case "trampoline":
758
+ return this.trampoline(def.index);
759
+ case "unsafe-intrinsic":
760
+ // plan v1: wasmtime compile-time builtins imported directly by a core
761
+ // module. `context.{get,set}` become host functions over the *current
762
+ // thread's* context slots (definitions.py `Thread.storage`); every
763
+ // other symbol fails here, at instantiate time.
764
+ return this.unsafeIntrinsic(def.intrinsic);
765
+ case "task-may-block":
766
+ return this.taskMayBlock;
767
+ default: {
768
+ const exhaustive = def;
769
+ throw new PlanError(`unsupported CoreDef kind ${exhaustive.kind}`);
770
+ }
771
+ }
772
+ }
773
+ resolveCoreExport(ref) {
774
+ const instance = this.instances[ref.instance];
775
+ if (instance === undefined) {
776
+ throw new PlanError(`core export ref: runtime instance ${ref.instance} not created yet`);
777
+ }
778
+ const value = instance.exports[ref.item.name];
779
+ if (value === undefined) {
780
+ throw new PlanError(`core instance ${ref.instance} has no export '${ref.item.name}'`);
781
+ }
782
+ return value;
783
+ }
784
+ resolveFunction(def, what) {
785
+ const value = this.resolveCoreDef(def);
786
+ if (typeof value !== "function") {
787
+ throw new PlanError(`${what}: resolved to non-function`);
788
+ }
789
+ return value;
790
+ }
791
+ trampoline(index) {
792
+ const cached = this.trampolineCache.get(index);
793
+ if (cached !== undefined)
794
+ return cached;
795
+ const decl = this.wire.trampolines[index];
796
+ if (decl === undefined) {
797
+ throw new PlanError(`no trampoline ${index} in plan`);
798
+ }
799
+ const fn = createTrampoline(decl, {
800
+ componentInstance: (i) => this.componentInstance(i),
801
+ resourceToken: (i) => {
802
+ const token = this.loaded.resourceTokens[i];
803
+ if (token === undefined) {
804
+ throw new PlanError(`no resource table ${i} in plan`);
805
+ }
806
+ return token;
807
+ },
808
+ runtimeMemory: (i) => new TranscodeMemory(() => this.memories[i], `runtime memory ${i}`),
809
+ resourceTableInstance: (i) => {
810
+ const table = this.wire.resourceTables[i];
811
+ if (table === undefined) {
812
+ throw new PlanError(`no resource table ${i} in plan`);
813
+ }
814
+ if (table.kind !== "concrete") {
815
+ throw new PlanError(`resource table ${i} is abstract (type-only) and has no runtime ` +
816
+ `handle table`);
817
+ }
818
+ return this.componentInstance(table.instance);
819
+ },
820
+ options: (i) => this.resolveOptions(i),
821
+ resultTypes: (i) => this.resultTypes(i),
822
+ callback: (i) => {
823
+ const fn = this.callbacks[i];
824
+ if (fn === undefined) {
825
+ throw new PlanError(`callback ${i} accessed before its extract-callback initializer ran`);
826
+ }
827
+ return fn;
828
+ },
829
+ memoryToken: (i) => this.liveMemory(i),
830
+ streamElem: (i) => {
831
+ if (i >= this.loaded.streamElems.length) {
832
+ throw new PlanError(`stream table ${i} is not in the plan's streamTables (plan v2)`);
833
+ }
834
+ return this.loaded.streamElems[i];
835
+ },
836
+ streamTableInstance: (i) => {
837
+ const instance = this.loaded.streamTableInstances[i];
838
+ if (instance === undefined) {
839
+ throw new PlanError(`stream table ${i} is not in the plan's streamTables (plan v2)`);
840
+ }
841
+ return this.componentInstance(instance);
842
+ },
843
+ futureTableInstance: (i) => {
844
+ const instance = this.loaded.futureTableInstances[i];
845
+ if (instance === undefined) {
846
+ throw new PlanError(`future table ${i} is not in the plan's futureTables (plan v2)`);
847
+ }
848
+ return this.componentInstance(instance);
849
+ },
850
+ errorContextTableInstance: (i) => {
851
+ // plan v3: the error-context tables' own index space
852
+ // (`TypeComponentLocalErrorContextTableIndex`). Loud on absence — the
853
+ // predecessor of this accessor borrowed the *resource*-table mapping
854
+ // and could answer with a different instance's table (polyengine#89).
855
+ const instance = this.loaded.errorContextTableInstances[i];
856
+ if (instance === undefined) {
857
+ throw new PlanError(`error-context table ${i} is not in the plan's ` +
858
+ `errorContextTables (plan v3)`);
859
+ }
860
+ return this.componentInstance(instance);
861
+ },
862
+ resultTypesForTuple: (i) => {
863
+ const t = this.loaded.resultTupleTypes.get(i);
864
+ return t === undefined ? null : this.resultTypes(t);
865
+ },
866
+ futureElem: (i) => {
867
+ if (i >= this.loaded.futureElems.length) {
868
+ throw new PlanError(`future table ${i} is not in the plan's futureTables (plan v2)`);
869
+ }
870
+ return this.loaded.futureElems[i];
871
+ },
872
+ prepared: this.preparedCall,
873
+ suspensionMode: this.suspensionMode,
874
+ calleeCanBlock: (fn) => this.suspendableFuncs.has(fn),
875
+ syncCallStack: this.syncCallStack,
876
+ factStartScopes: this.factStartScopes,
877
+ trapState: this.trapState,
878
+ loweredImport: (d) => this.buildLoweredImport(d),
879
+ stats: this.stats,
880
+ });
881
+ this.trampolineCache.set(index, fn);
882
+ return fn;
883
+ }
884
+ buildLoweredImport(decl) {
885
+ const importIndex = this.lowerings.get(decl.lowered);
886
+ if (importIndex === undefined) {
887
+ throw new PlanError(`lower-import trampoline: lowering ${decl.lowered} was never ` +
888
+ `initialized (initializer order violation)`);
889
+ }
890
+ const imp = this.wire.imports[importIndex];
891
+ if (imp === undefined) {
892
+ throw new PlanError(`no import ${importIndex} in plan`);
893
+ }
894
+ const label = importLabel(imp.name, imp.path);
895
+ const value = this.lookupHostImport(imp.name, imp.path, label);
896
+ if (typeof value !== "function") {
897
+ throw new PlanError(`host import '${label}' missing or not a function (got ` +
898
+ `${describe(value)})`);
899
+ }
900
+ const ft = this.funcType(decl.type, `import '${label}'`);
901
+ const opts = this.resolveOptions(decl.options);
902
+ const suspendable = isSuspending(value);
903
+ // The Suspending-wrap decision is taken in `importValue`, which sees the
904
+ // trampoline only AFTER `createTrampoline`'s trap-recording wrapper has
905
+ // replaced this function's identity — a brand on the CoreFn would die
906
+ // there (measured: the returned Promise coerced to 0 through the
907
+ // unwrapped import). Record the decision as executor state instead,
908
+ // keyed by LoweredIndex; `importValue` runs later on the same call
909
+ // stack, so the set is populated by construction when it reads.
910
+ if (suspendable)
911
+ this.suspendableLowerings.add(decl.lowered);
912
+ return createLoweredImport({
913
+ name: label,
914
+ ft,
915
+ opts,
916
+ hostFn: value,
917
+ stats: this.stats,
918
+ mode: this.suspensionMode,
919
+ suspendable,
920
+ });
921
+ }
922
+ /**
923
+ * Resolve one plan import against the host-provided import record: index by
924
+ * the component's exact import string, then walk `path` (instance imports —
925
+ * plan-format.md v0.1 amendment #4).
926
+ */
927
+ lookupHostImport(name, path, label) {
928
+ if (!(name in this.hostImports)) {
929
+ throw new PlanError(`host import '${label}' not provided (no key '${name}' in imports)`);
930
+ }
931
+ let value = this.hostImports[name];
932
+ const walked = [];
933
+ for (const segment of path) {
934
+ if (value === null || typeof value !== "object") {
935
+ throw new PlanError(`host import '${label}': '${[name, ...walked].join("/")}' is ${describe(value)}, expected an object to read ` +
936
+ `'${segment}' from`);
937
+ }
938
+ value = value[segment];
939
+ walked.push(segment);
940
+ }
941
+ return value;
942
+ }
943
+ /**
944
+ * Element types of an interned *results tuple* — the `results` field of a
945
+ * `task-return` trampoline (the shim interns a lifted function's result
946
+ * list as a single tuple type, `intern_results_tuple`).
947
+ */
948
+ resultTypes(index) {
949
+ const entry = this.loaded.types[index];
950
+ if (entry === undefined) {
951
+ throw new PlanError(`task-return results: no type ${index}`);
952
+ }
953
+ if (entry.kind !== "value" || entry.type.kind !== "tuple") {
954
+ throw new PlanError(`task-return results: type ${index} is not a tuple type`);
955
+ }
956
+ return entry.type.elements;
957
+ }
958
+ funcType(index, what) {
959
+ const entry = this.loaded.types[index];
960
+ if (entry === undefined)
961
+ throw new PlanError(`${what}: no type ${index}`);
962
+ if (entry.kind !== "func") {
963
+ throw new PlanError(`${what}: type ${index} is not a function type`);
964
+ }
965
+ return entry.funcType;
966
+ }
967
+ resolveOptions(index) {
968
+ const wire = this.wire.canonicalOptions[index];
969
+ if (wire === undefined) {
970
+ throw new PlanError(`no canonicalOptions ${index} in plan`);
971
+ }
972
+ const memoryIndex = wire.memory;
973
+ return {
974
+ stringEncoding: wire.stringEncoding,
975
+ memory: memoryIndex === null ? null : this.liveMemory(memoryIndex),
976
+ realloc: wire.realloc === null
977
+ ? null
978
+ : () => this.reallocs[wire.realloc],
979
+ postReturn: wire.postReturn === null
980
+ ? null
981
+ : () => this.postReturns[wire.postReturn],
982
+ callback: wire.callback === null
983
+ ? null
984
+ : () => this.callbacks[wire.callback],
985
+ async: wire.async,
986
+ cancellable: wire.cancellable,
987
+ coreType: wire.coreType,
988
+ instance: this.componentInstance(wire.instance),
989
+ };
990
+ }
991
+ }
992
+ /** `name` plus instance path, for diagnostics: `"ns:pkg/iface"."f"`. */
993
+ function importLabel(name, path) {
994
+ return path.length === 0 ? name : `${name}/${path.join("/")}`;
995
+ }
996
+ function describe(v) {
997
+ if (v === null)
998
+ return "null";
999
+ if (v === undefined)
1000
+ return "undefined";
1001
+ if (typeof v === "object")
1002
+ return `a ${v.constructor?.name ?? "object"}`;
1003
+ return `a ${typeof v}`;
1004
+ }