@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,4 @@
1
+ // Plan executor + host boundary (docs/architecture.md §4.3).
2
+ export * from "./boundary.js";
3
+ export * from "./executor.js";
4
+ export * from "./host_streams.js";
@@ -0,0 +1,510 @@
1
+ // The 0.3 async canonical built-ins, as host trampolines
2
+ // (contracts/intrinsics.md §B "M2"): task.{return,cancel},
3
+ // backpressure.{set,inc,dec}, waitable-set.{new,wait,poll,drop},
4
+ // waitable.join, subtask.{drop,cancel} and thread.yield.
5
+ //
6
+ // Every one is a direct port of the correspondingly named `canon_*` function
7
+ // in definitions.py (cited per function), with one systematic substitution.
8
+ //
9
+ // ## `current_instance()` vs the trampoline's declared instance
10
+ //
11
+ // definitions.py reads `current_instance()` (line 312), defined as
12
+ // `current_task().inst` — it can, because in the reference a canonical
13
+ // built-in is only ever reached from inside a task. That is not true of a
14
+ // real component: wasmtime lets a core module's **start function** call
15
+ // instance-scoped built-ins (`waitable-set.new`, `backpressure.inc`, ...)
16
+ // during instantiation, before any task exists. The official suite exercises
17
+ // exactly this (e.g. `test/async/dont-block-start.wast`).
18
+ //
19
+ // wasmtime resolves it by naming the owning component instance *statically*
20
+ // in every trampoline declaration (`Trampoline::WaitableSetNew { instance }`
21
+ // and friends — the `instance` field the plan carries). So the built-ins
22
+ // below take their instance from the declaration, which is well-defined at
23
+ // instantiation time and identical to `current_instance()` whenever a task is
24
+ // running. Built-ins that genuinely need the *task* or *thread*
25
+ // (`task.return`, `task.cancel`, `subtask.cancel`, `thread.yield`) still read
26
+ // the current-thread stack: they are meaningless outside a task, and the
27
+ // reference's `trap_if`s are what report that.
28
+ //
29
+ // ## Blocking built-ins in a stackless world
30
+ //
31
+ // `waitable-set.wait`, `thread.yield` and the synchronous `subtask.cancel`
32
+ // all *block the calling wasm frame* in the reference. A callback-ABI guest
33
+ // is stackless: there is no suspendable wasm stack to park, so blocking here
34
+ // genuinely requires JSPI (docs/architecture.md §6, JSPI role 2) and these built-ins say
35
+ // so at the precise point, loudly, instead of faking a wait.
36
+ //
37
+ // They are not, however, unconditionally unavailable. Where the reference can
38
+ // complete *without* suspending — `waitable-set.wait` on a set that already
39
+ // has a pending event, `waitable-set.poll` always, `subtask.cancel` on a
40
+ // subtask that resolved eagerly — this module returns the answer directly.
41
+ // That is not a shortcut: definitions.py's `Thread.wait_until` (line 396) may
42
+ // legitimately return without blocking when `ready_func()` already holds, so
43
+ // taking that branch is a conforming schedule.
44
+ import { blockCurrentActivation } from "../jspi/mod.js";
45
+ import { assert_, trap, trapIf } from "../cabi/trap.js";
46
+ import { CoreValueIter, LiftLowerContext, liftFlatValues, MAX_FLAT_PARAMS, store as storeValue, } from "../cabi/mod.js";
47
+ import { valTypesEqual } from "../cabi/types.js";
48
+ import { currentTask, currentThread, EventCode, liftOptionsEqual, needsJspi, Subtask, Waitable, WaitableSet, } from "../task/mod.js";
49
+ import { cabiOptions, normalizeCoreValues } from "../exec/boundary.js";
50
+ import { traceCopy } from "./stream_builtins.js";
51
+ /**
52
+ * `BLOCKED` sentinel of `canon_subtask_cancel` (definitions.py line 2467).
53
+ */
54
+ export const BLOCKED = 0xffff_ffff;
55
+ // ---------------------------------------------------------------------------
56
+ // task.return / task.cancel
57
+ // ---------------------------------------------------------------------------
58
+ /** definitions.py `canon_task_return` (line 2384). */
59
+ export function createTaskReturn(decl, ctx) {
60
+ const opts = ctx.options(decl.options);
61
+ // plan v3: `resultType` is the interned `plan.types` entry; `results` is the
62
+ // raw wasmtime `TypeTupleIndex` (the FACT `task_return_type` key, consumed
63
+ // by the loader's dictionary). `null` is wire-legal for a task with no
64
+ // declared result type; today's producer always emits the empty tuple
65
+ // instead, so this degenerates to `[]` either way.
66
+ const resultTypes = decl.resultType === null
67
+ ? []
68
+ : ctx.resultTypes(decl.resultType);
69
+ return (...flatArgs) => {
70
+ const task = currentTask();
71
+ trapIf(!task.inst.mayLeave, "task.return: cannot leave component instance (may_leave violation)");
72
+ trapIf(!task.opts.async_, "task.return from a non-async task");
73
+ // `trap_if(result_type != task.ft.result)` (definitions.py:2388): the
74
+ // trampoline's interned result tuple must be the lifted function's result
75
+ // type. Compared structurally — the plan's type table interns by
76
+ // structure, so identity comparison would reject valid components.
77
+ //
78
+ // Plan v3 enables this for FACT cross-component tasks too: the callee
79
+ // task's declared result type is now resolvable from the raw
80
+ // `TypeTupleIndex` `prepare-call` carried (contracts/plan-format.md v3
81
+ // amendment 3, wired in fact_calls.ts). It remains skipped for the one
82
+ // case v3 does not answer — a callee the plan maps no `task.return`
83
+ // tuple for, where `ft.results` is a placeholder rather than a
84
+ // declaration (`factResultTypesKnown === false`); comparing against a
85
+ // placeholder would be a false rejection, not a check.
86
+ trapIf((!task.factPassthrough || task.factResultTypesKnown) &&
87
+ !valTypesEqual(resultTypes, task.ft.results), "task.return with a result type that is not the task's result type");
88
+ // `trap_if(not LiftOptions.equal(opts, task.opts))` (definitions.py:2389).
89
+ // The MEMORY half stays skipped for FACT tasks, and plan v3 does NOT
90
+ // change that: the relaxation was never about the type mapping. The
91
+ // task's memory is reconstructed from `prepare-call`'s `memory` field,
92
+ // which is the *adapter's* view of the lift options
93
+ // (`adapter.lift.options...memory`) and is `None` for callees whose own
94
+ // `task.return` options do name a memory — the 17-param async-lifted
95
+ // callees of `test/async/cross-abi-calls.wast` are exactly that shape.
96
+ // The information simply is not in the plan, at v3 as at v2; restoring
97
+ // the check needs `prepare-call`'s indices related to the callee's
98
+ // canonical options, which remains open contract friction.
99
+ //
100
+ // definitions.py `LiftOptions.equal` (line 643) compares string encoding
101
+ // *and* memory identity. Both halves are checked for a host-boundary task.
102
+ //
103
+ // For a FACT task the memory half is skipped, and the reason is specific
104
+ // rather than "we can't be bothered": the task's memory is reconstructed
105
+ // from `prepare-call`'s `memory` field, which carries the *adapter's* view
106
+ // of the lift options (`adapter.lift.options...memory`) and is `None`
107
+ // for callees whose own `task.return` options do name a memory —
108
+ // `test/async/cross-abi-calls.wast`'s 17-param async-lifted callees are
109
+ // exactly that shape. wasmtime tolerates the mismatch because its check is
110
+ // *one-sided*: `concurrent.rs:3344-3358` treats "the `task.return` site
111
+ // specifies no memory" as valid and only compares when it does, against a
112
+ // lift memory it holds first-hand. We hold ours second-hand, so applying
113
+ // either form of the memory comparison produces a false rejection.
114
+ //
115
+ // The string-encoding half IS checked on both paths: `prepare-call` passes
116
+ // the encoding directly, so that reconstruction is exact.
117
+ trapIf(!liftOptionsEqual({ stringEncoding: opts.stringEncoding, memory: opts.memory }, task.factPassthrough
118
+ ? { stringEncoding: task.opts.stringEncoding, memory: opts.memory }
119
+ : task.opts), "task.return with canonical options differing from the task's");
120
+ // Type-aware per-lane normalization: `normalizeFlat`'s blanket `>>> 0`
121
+ // silently truncated float lanes (a `task.return` of f64 -1.1 arrived at
122
+ // `[async-return]` as 4294967295). `normalizeCoreValues` consults the
123
+ // declared lane types, so only i32 lanes are coerced.
124
+ const flat = normalizeCoreValues(flatArgs, opts.coreType.params, "task.return arguments");
125
+ if (task.factPassthrough) {
126
+ // FACT cross-component call: the caller's `[async-return]` adapter
127
+ // function does the lift-and-lower itself, in wasm, so the host hands it
128
+ // the callee's flat results untouched. See `Task.factPassthrough`.
129
+ task.return_(flat);
130
+ return;
131
+ }
132
+ const cx = new LiftLowerContext(cabiOptions(opts), task.inst, task);
133
+ const vi = new CoreValueIter(flat);
134
+ const result = liftFlatValues(cx, MAX_FLAT_PARAMS, vi, task.ft.results);
135
+ task.return_(result);
136
+ };
137
+ }
138
+ /** definitions.py `canon_task_cancel` (line 2397). */
139
+ export function createTaskCancel() {
140
+ return () => {
141
+ const task = currentTask();
142
+ trapIf(!task.inst.mayLeave, "task.cancel: cannot leave component instance (may_leave violation)");
143
+ trapIf(!task.opts.async_, "task.cancel from a non-async task");
144
+ task.cancel();
145
+ };
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // backpressure
149
+ // ---------------------------------------------------------------------------
150
+ // `canon_backpressure_set` is not ported: wasmtime 47 emits no
151
+ // `BackpressureSet` trampoline (`component/info.rs` has only
152
+ // `BackpressureInc`/`BackpressureDec`), and the reference's own copy was
153
+ // unreachable dead code until upstream removed it (CM PR #690; see
154
+ // upstream-component-model-repo-findings.md CM-2, RESOLVED). The counter
155
+ // below is the live interface.
156
+ /** definitions.py `canon_backpressure_inc` (line 2368). */
157
+ export function createBackpressureInc(inst) {
158
+ return () => {
159
+ assert_(inst.backpressure >= 0 && inst.backpressure < 2 ** 16, "backpressure counter out of range");
160
+ inst.backpressure += 1;
161
+ trapIf(inst.backpressure === 2 ** 16, "backpressure counter overflow");
162
+ };
163
+ }
164
+ /** definitions.py `canon_backpressure_dec` (line 2375). */
165
+ export function createBackpressureDec(inst) {
166
+ return () => {
167
+ assert_(inst.backpressure >= 0 && inst.backpressure < 2 ** 16, "backpressure counter out of range");
168
+ inst.backpressure -= 1;
169
+ trapIf(inst.backpressure < 0, "backpressure counter underflow");
170
+ };
171
+ }
172
+ // ---------------------------------------------------------------------------
173
+ // waitable sets
174
+ // ---------------------------------------------------------------------------
175
+ /** definitions.py `canon_waitable_set_new` (line 2406). */
176
+ export function createWaitableSetNew(inst) {
177
+ return () => {
178
+ trapIf(!inst.mayLeave, "waitable-set.new: cannot leave component instance");
179
+ return inst.handles.add(new WaitableSet());
180
+ };
181
+ }
182
+ /**
183
+ * definitions.py `canon_waitable_set_wait` (line 2414).
184
+ *
185
+ * The reference blocks the calling thread until the set has an event. From a
186
+ * stackless (callback-ABI) guest there is no wasm stack to suspend, so this
187
+ * only succeeds when an event is *already* pending — which is the reference's
188
+ * own non-blocking branch of `Thread.wait_until`. Otherwise: `needsJspi`.
189
+ *
190
+ * A guest using the callback ABI is expected to return the `WAIT` callback
191
+ * code rather than call this built-in; hitting the JSPI path here means the
192
+ * component uses the stackful async ABI.
193
+ */
194
+ export function createWaitableSetWait(decl, ctx, inst, mode = "plain") {
195
+ const opts = ctx.options(decl.options);
196
+ // `cancellable` is a *canonical option*, not a trampoline field: wasmtime's
197
+ // `Trampoline::WaitableSetWait` carries only `{instance, options}`
198
+ // (wasmtime-environ 47.0.3 `component/info.rs:815`), while
199
+ // `CanonicalOptions.cancellable` (info.rs:540) is what the guest declared.
200
+ // It reaches definitions.py as `canon_waitable_set_wait`'s first parameter
201
+ // (line 2414).
202
+ const cancellable = opts.cancellable;
203
+ return (si, ptr) => {
204
+ trapIf(!inst.mayLeave, "waitable-set.wait: cannot leave component instance");
205
+ const wset = requireWaitableSet(inst, si ?? 0, "waitable-set.wait");
206
+ const task = currentTask();
207
+ let event;
208
+ if (task.deliverPendingCancel(cancellable)) {
209
+ event = [EventCode.TASK_CANCELLED, 0, 0];
210
+ }
211
+ else if (wset.hasPendingEvent()) {
212
+ // Non-blocking branch: definitions.py `Thread.wait_until` may return
213
+ // immediately when the condition already holds.
214
+ //
215
+ // This deliberately skips `wait_for_event_and`, and with it the
216
+ // `num_waiting += 1 / -= 1` bracket around the block
217
+ // (`WaitableSet.wait_for_event_and`, line 829). That is unobservable:
218
+ // `num_waiting` is read only by `WaitableSet.drop`
219
+ // (`trap_if(self.num_waiting > 0)`, line 852), and since we never yield
220
+ // between the increment and the decrement here, no other code could run
221
+ // to observe a non-zero value. Incrementing and immediately decrementing
222
+ // would be pure ceremony.
223
+ traceCopy(`waitable-set.wait si=${si} FAST (pending event)`);
224
+ event = wset.getPendingEvent();
225
+ }
226
+ else if (mode === "jspi") {
227
+ traceCopy(`waitable-set.wait si=${si} BLOCKS`);
228
+ // SITE 2 (lit). definitions.py `WaitableSet.wait_for_event_and`
229
+ // (line 829): block until the set has an event, then take it.
230
+ //
231
+ // The `num_waiting` bracket is real now. Skipping it was justified only
232
+ // while this path could not actually yield; a genuine block CAN be
233
+ // observed, because `WaitableSet.drop` traps on `num_waiting > 0`
234
+ // (line 852). Incremented before blocking and decremented in
235
+ // `onSettled`, which runs exactly once on EVERY terminal transition —
236
+ // normal resume, cancelled resume, produce-throw, and `abandon`
237
+ // (#106: decrementing in `produce` missed the abandon leg, leaving
238
+ // `numWaiting` elevated forever and a later `waitable-set.drop`
239
+ // trapping spuriously). The decrement is not idempotent, so it lives
240
+ // ONLY here, not in `produce` as well; nothing can observe the still-
241
+ // elevated count between `produce` and the hook — both run
242
+ // synchronously inside the settle, before any other code.
243
+ wset.numWaiting += 1;
244
+ return blockCurrentActivation({
245
+ store: inst.store,
246
+ task,
247
+ readyFunc: () => wset.hasPendingEvent(),
248
+ cancellable,
249
+ produce: (cancelled) => {
250
+ const ev = cancelled
251
+ ? [EventCode.TASK_CANCELLED, 0, 0]
252
+ : wset.getPendingEvent();
253
+ return unpackEvent(opts, inst, ptr ?? 0, ev);
254
+ },
255
+ onSettled: () => {
256
+ wset.numWaiting -= 1;
257
+ },
258
+ });
259
+ }
260
+ else {
261
+ needsJspi("waitable-set.wait with no pending event (the calling wasm frame " +
262
+ "must block; a callback-ABI guest should return the WAIT code " +
263
+ "instead)");
264
+ }
265
+ return unpackEvent(opts, inst, ptr ?? 0, event);
266
+ };
267
+ }
268
+ /** definitions.py `canon_waitable_set_poll` (line 2431). */
269
+ export function createWaitableSetPoll(decl, ctx, inst) {
270
+ const opts = ctx.options(decl.options);
271
+ /** See `createWaitableSetWait`: `cancellable` is an option, not a decl field. */
272
+ const cancellable = opts.cancellable;
273
+ return (si, ptr) => {
274
+ trapIf(!inst.mayLeave, "waitable-set.poll: cannot leave component instance");
275
+ const wset = requireWaitableSet(inst, si ?? 0, "waitable-set.poll");
276
+ const event = wset.poll(currentTask(), cancellable);
277
+ return unpackEvent(opts, inst, ptr ?? 0, event);
278
+ };
279
+ }
280
+ /** definitions.py `canon_waitable_set_drop` (line 2441). */
281
+ export function createWaitableSetDrop(inst) {
282
+ return (i) => {
283
+ trapIf(!inst.mayLeave, "waitable-set.drop: cannot leave component instance");
284
+ const wset = inst.handles.remove(i ?? 0);
285
+ trapIf(!(wset instanceof WaitableSet), "waitable-set.drop: handle is not a waitable set");
286
+ wset.drop();
287
+ };
288
+ }
289
+ /** definitions.py `canon_waitable_join` (line 2451). */
290
+ export function createWaitableJoin(inst) {
291
+ return (wi, si) => {
292
+ trapIf(!inst.mayLeave, "waitable.join: cannot leave component instance");
293
+ const w = inst.handles.get(wi ?? 0);
294
+ trapIf(!(w instanceof Waitable), "waitable.join: handle is not a waitable");
295
+ trapIf(w.hasSyncWaiter, "waitable.join on a waitable with a synchronous waiter");
296
+ if ((si ?? 0) === 0) {
297
+ w.join(null);
298
+ return;
299
+ }
300
+ const wset = requireWaitableSet(inst, si, "waitable.join");
301
+ w.join(wset);
302
+ };
303
+ }
304
+ // ---------------------------------------------------------------------------
305
+ // subtasks
306
+ // ---------------------------------------------------------------------------
307
+ /** definitions.py `canon_subtask_drop` (line 2494). */
308
+ export function createSubtaskDrop(inst) {
309
+ return (i) => {
310
+ trapIf(!inst.mayLeave, "subtask.drop: cannot leave component instance");
311
+ const s = inst.handles.remove(i ?? 0);
312
+ trapIf(!(s instanceof Subtask), "subtask.drop: handle is not a subtask");
313
+ s.drop();
314
+ };
315
+ }
316
+ /**
317
+ * definitions.py `canon_subtask_cancel` (line 2469).
318
+ *
319
+ * The synchronous form blocks (`subtask.wait_for_pending_event()`) when the
320
+ * callee does not resolve promptly; from a stackless guest that is JSPI
321
+ * territory. The async form returns `BLOCKED` instead of blocking, and is
322
+ * fully supported.
323
+ */
324
+ /**
325
+ * The tail shared by `subtask.cancel`'s blocking and non-blocking exits:
326
+ * take the delivered SUBTASK event, check it is the one we expect, and report
327
+ * the resolved state. Factored out so the blocking form can run it at RESUME
328
+ * time inside `produce`.
329
+ */
330
+ function finishSubtaskCancel(i, st) {
331
+ return () => {
332
+ const [code, index, payload] = st.getPendingEvent();
333
+ assert_(code === EventCode.SUBTASK && index === (i ?? 0) && payload === st.state, "unexpected event delivered by subtask.cancel");
334
+ assert_(st.resolveDelivered(), "subtask.cancel did not deliver the resolution");
335
+ return st.state;
336
+ };
337
+ }
338
+ export function createSubtaskCancel(decl, inst, mode = "plain") {
339
+ const async_ = decl.async === true;
340
+ return (i) => {
341
+ // The handle table is the **declared** instance's, not
342
+ // `current_thread().task.inst`. definitions.py `canon_subtask_cancel`
343
+ // (line 2469) uses the latter because the reference has no fused
344
+ // adapters, so the running task and the subtask's owner always coincide.
345
+ // With FACT they do not: `async-start-call` adds the subtask to
346
+ // `prepare-call`'s `caller_instance`, which for a nested component is a
347
+ // *different* instance from the one whose task is running — observed as
348
+ // caller=2 vs task.inst=3 in `big-interleaving-test.wast:1584`, where the
349
+ // lookup then failed with "table index out of range". wasmtime names the
350
+ // owner on the trampoline for exactly this reason
351
+ // (`Trampoline::SubtaskCancel { instance, .. }`), which is the same
352
+ // correction already applied to every other instance-scoped built-in —
353
+ // see this module's header.
354
+ trapIf(!inst.mayLeave, "subtask.cancel: cannot leave component instance");
355
+ const subtask = inst.handles.get(i ?? 0);
356
+ trapIf(!(subtask instanceof Subtask), "subtask.cancel: handle is not a subtask");
357
+ const st = subtask;
358
+ const finish = finishSubtaskCancel(i, st);
359
+ trapIf(st.resolveDelivered(), "subtask.cancel on a subtask whose resolution was already delivered");
360
+ trapIf(st.cancellationRequested, "subtask.cancel on a subtask that was already cancelled");
361
+ trapIf(st.inWaitableSet() && !async_, "synchronous subtask.cancel on a subtask that is in a waitable set");
362
+ if (st.resolved()) {
363
+ assert_(st.hasPendingEvent(), "resolved subtask without a pending event at cancellation");
364
+ }
365
+ else {
366
+ st.cancellationRequested = true;
367
+ assert_(st.onCancel !== null, "subtask.cancel on a subtask with no cancellation handler");
368
+ st.onCancel(inst);
369
+ if (!st.resolved()) {
370
+ if (!async_) {
371
+ if (mode === "jspi") {
372
+ // SITE 5 (lit): a sync `subtask.cancel` blocks until the callee
373
+ // actually resolves (definitions.py `canon_subtask_cancel`), then
374
+ // reports the resolved state through the same tail as the
375
+ // non-blocking path. Mirrors SITE 4 (stream_builtins.ts:305-323)
376
+ // and `Waitable.waitForPendingEvent` (definitions.py:786-790,
377
+ // reached from canon_subtask_cancel's `subtask.wait_for_pending_event()`
378
+ // call, :2484): `hasSyncWaiter` must
379
+ // be set for the duration so a concurrent `waitable.join` on
380
+ // this subtask traps (async_builtins.ts:362-365) instead of
381
+ // racing the SUBTASK event away from this resume (#87).
382
+ st.hasSyncWaiter = true;
383
+ return blockCurrentActivation({
384
+ store: inst.store,
385
+ task: currentTask(),
386
+ readyFunc: () => st.hasPendingEvent(),
387
+ cancellable: false,
388
+ produce: () => {
389
+ st.hasSyncWaiter = false;
390
+ return finish();
391
+ },
392
+ // #106: `abandon` never runs `produce`; without the backstop
393
+ // the flag stayed set forever and a later `waitable.join` on
394
+ // this subtask trapped spuriously. Idempotent, so the success
395
+ // path's clear-inside-`produce` ordering is untouched.
396
+ onSettled: () => {
397
+ st.hasSyncWaiter = false;
398
+ },
399
+ });
400
+ }
401
+ needsJspi("synchronous subtask.cancel whose callee did not resolve " +
402
+ "immediately (the calling wasm frame must block)");
403
+ }
404
+ // The ASYNC form answers "did the cancellation resolve the callee
405
+ // promptly?" — BLOCKED only when it genuinely did not
406
+ // (definitions.py line 2486). Under jspi "promptly" is invisible at
407
+ // this instant: `request_cancellation` delivered TASK_CANCELLED by
408
+ // settling the callee's suspension, and the engine runs the resumed
409
+ // activation (whose `task.cancel` resolves this subtask) on a LATER
410
+ // microtask (pin (j)). Deciding now reports BLOCKED for a callee the
411
+ // reference resolves synchronously (cancellable.wast tests 1-2). So:
412
+ // wait until the callee is DETERMINATE — resolved, finished, or
413
+ // re-parked on a scheduler condition — exactly `async-start-call`'s
414
+ // rule (fact_calls.ts). A callee with a pending (undeliverable)
415
+ // cancel sits parked non-cancellably, which is determinate, so the
416
+ // genuine BLOCKED answer is still immediate. Host-import subtasks
417
+ // carry no callee task: their onCancel is a no-op and their state
418
+ // cannot be mid-hop, so the pre-jspi immediate answer stands.
419
+ //
420
+ // NAMED DIVERGENCE (docs/architecture.md §6, #92): this park makes
421
+ // the async built-in non-atomic — other ready threads may run while
422
+ // it waits, a reordering within the reference's Store.tick freedom
423
+ // taken one built-in early.
424
+ if (mode === "jspi" && st.calleeTask !== null) {
425
+ const t = st.calleeTask;
426
+ const store = inst.store;
427
+ const determinate = () => st.resolved() ||
428
+ t.threads.every((th) => th.done()) ||
429
+ store.waiting.some((w) => w.task === st.calleeTask);
430
+ if (!determinate()) {
431
+ return blockCurrentActivation({
432
+ store: inst.store,
433
+ task: currentTask(),
434
+ readyFunc: determinate,
435
+ cancellable: false,
436
+ produce: () => (st.resolved() ? finish() : BLOCKED),
437
+ });
438
+ }
439
+ if (!st.resolved())
440
+ return BLOCKED;
441
+ return finish();
442
+ }
443
+ return BLOCKED;
444
+ }
445
+ }
446
+ return finish();
447
+ };
448
+ }
449
+ // ---------------------------------------------------------------------------
450
+ // thread.yield
451
+ // ---------------------------------------------------------------------------
452
+ /**
453
+ * definitions.py `canon_thread_yield` (line 2728).
454
+ *
455
+ * Yielding blocks the calling wasm frame until the scheduler comes back to
456
+ * it. A callback-ABI guest expresses the same intent by returning the `YIELD`
457
+ * callback code, which this runtime implements fully (exec/boundary.ts); the
458
+ * *built-in* form needs a suspendable stack.
459
+ */
460
+ export function createThreadYield(decl, mode = "plain") {
461
+ const cancellable = decl.cancellable === true;
462
+ return () => {
463
+ const thread = currentThread();
464
+ trapIf(!thread.task.inst.mayLeave, "thread.yield: cannot leave component instance");
465
+ // A pending cancellation is deliverable without suspending at all
466
+ // (definitions.py `Thread.yield_` -> `wait_until` -> `deliver_pending_cancel`).
467
+ if (thread.task.deliverPendingCancel(cancellable))
468
+ return 1;
469
+ if (mode === "jspi") {
470
+ // SITE 3 (lit). definitions.py `Thread.yield_` is
471
+ // `wait_until(lambda: True, cancellable)`: immediately ready, but it
472
+ // goes through the scheduler, so other threads get a turn first. A
473
+ // suspension point with an always-true `readyFunc` is exactly that --
474
+ // `Store.tick` will resume it, after whatever else is already ready.
475
+ return blockCurrentActivation({
476
+ store: thread.task.inst.store,
477
+ task: thread.task,
478
+ readyFunc: () => true,
479
+ cancellable,
480
+ produce: (cancelled) => (cancelled ? 1 : 0),
481
+ });
482
+ }
483
+ needsJspi("thread.yield (the calling wasm frame must block; a callback-ABI " +
484
+ "guest should return the YIELD code instead)");
485
+ };
486
+ }
487
+ // ---------------------------------------------------------------------------
488
+ // helpers
489
+ // ---------------------------------------------------------------------------
490
+ function requireWaitableSet(inst, si, what) {
491
+ const wset = inst.handles.get(si);
492
+ trapIf(!(wset instanceof WaitableSet), `${what}: handle ${si} is not a waitable set`);
493
+ return wset;
494
+ }
495
+ /**
496
+ * definitions.py `unpack_event` (line 2422): store the two payload words at
497
+ * `ptr` and return the event code.
498
+ */
499
+ function unpackEvent(opts, inst, ptr, e) {
500
+ const [event, p1, p2] = e;
501
+ const cx = new LiftLowerContext(cabiOptions(opts), inst, null);
502
+ storeValue(cx, p1, { kind: "u32" }, ptr);
503
+ storeValue(cx, p2, { kind: "u32" }, ptr + 4);
504
+ return event;
505
+ }
506
+ // Structural `ValType` equality (the C2-D bugfix) moved to cabi/types.ts
507
+ // (`valTypesEqual`) when the #18 tls smoke found its stream-element sibling;
508
+ // the contract note lives there now.
509
+ /** Unused-import guard: `trap` is re-exported for symmetry with cabi. */
510
+ void trap;
@@ -0,0 +1,90 @@
1
+ // `CoreDef::UnsafeIntrinsic` (plan v1 / contracts/plan-format.md v0.3):
2
+ // wasmtime compile-time builtins that a component's core modules import
3
+ // directly, bypassing the trampoline table.
4
+ //
5
+ // Of wasmtime-environ 47.0.3's 21 unsafe intrinsics
6
+ // (`component/intrinsic.rs`, `for_each_unsafe_intrinsic!`) only four have
7
+ // Component Model meaning: `context-{get,set}-i32-{0,1}`, the canonical
8
+ // `context.get` / `context.set` built-ins. The other seventeen
9
+ // (`*-native-load` / `*-native-store` / `store-data-address`) are raw host
10
+ // memory access for wasmtime's own internals; they have no portable meaning
11
+ // in a JS host and are refused at instantiate time.
12
+ import { assert_, trapIf } from "../cabi/trap.js";
13
+ import { currentThread } from "../task/mod.js";
14
+ import { ambientDebug, dbgId } from "../task/scheduler.js";
15
+ import { UnsupportedFeatureError } from "./errors.js";
16
+ /**
17
+ * Number of `i32` context slots per thread. definitions.py `Thread.storage`
18
+ * is initialised `[0,0]` (line 347) and `canon_context_{get,set}` assert
19
+ * `i < len(thread.storage)` — so exactly two, matching the intrinsic names
20
+ * `context-*-i32-0` and `context-*-i32-1`.
21
+ */
22
+ export const NUM_CONTEXT_SLOTS = 2;
23
+ /**
24
+ * definitions.py `canon_context_get` (line 2348).
25
+ *
26
+ * The storage is **per thread**, not per task: two threads of one task have
27
+ * independent context. wit-bindgen 0.60 keeps its async-executor task pointer
28
+ * in slot 0, which is why this intrinsic is the entry blocker for async
29
+ * guests.
30
+ */
31
+ export function canonContextGet(i) {
32
+ const thread = currentThread();
33
+ assert_(i < NUM_CONTEXT_SLOTS, `context.get slot ${i} out of range`);
34
+ const result = thread.storage[i];
35
+ assert_(result < 2 ** 32, "context.get value out of i32 range");
36
+ if (CTX_TRACE)
37
+ trace(`get[${i}] -> ${result}`, thread);
38
+ return result >>> 0;
39
+ }
40
+ // Standing probe (CE_CTX_TRACE=1): per-call context-slot traffic with the
41
+ // full ambient state — the instrument that isolated issue #24. Cheap and
42
+ // env-gated; keep.
43
+ const CTX_TRACE = (() => {
44
+ try {
45
+ return Deno.env.get("CE_CTX_TRACE") === "1";
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ })();
51
+ export function ctxThreadId(t) {
52
+ return dbgId(t);
53
+ }
54
+ function trace(msg, thread) {
55
+ const a = ambientDebug();
56
+ console.error(`[ctx] ${ctxThreadId(thread)} ${msg} storage=${JSON.stringify(thread.storage)} | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${a.claims.map(ctxThreadId).join(",")}] resuming=${a.resuming === null ? "-" : ctxThreadId(a.resuming)}`);
57
+ }
58
+ /** definitions.py `canon_context_set` (line 2358). */
59
+ export function canonContextSet(i, v) {
60
+ const thread = currentThread();
61
+ assert_(i < NUM_CONTEXT_SLOTS, `context.set slot ${i} out of range`);
62
+ if (CTX_TRACE)
63
+ trace(`set[${i}] = ${v >>> 0}`, thread);
64
+ thread.storage[i] = v >>> 0;
65
+ }
66
+ /**
67
+ * Materialize one `unsafe-intrinsic` CoreDef as a core function.
68
+ *
69
+ * Called during initializer resolution — i.e. at instantiate time — so an
70
+ * unimplementable symbol fails instantiation rather than the first call
71
+ * (contracts/plan-format.md "Executor obligations").
72
+ */
73
+ export function createUnsafeIntrinsic(symbol) {
74
+ const match = /^context-(get|set)-i32-(\d+)$/.exec(symbol);
75
+ if (match === null) {
76
+ throw new UnsupportedFeatureError("M2", `component imports the unsafe intrinsic '${symbol}', which has no ` +
77
+ `portable meaning in a JS host (only context.{get,set} do)`);
78
+ }
79
+ const slot = Number(match[2]);
80
+ // A slot outside the canonical range would be a wasmtime/plan inconsistency
81
+ // rather than a missing capability, but refusing it here is still the right
82
+ // shape: instantiate-time, never call-time.
83
+ trapIf(slot >= NUM_CONTEXT_SLOTS, `unsafe intrinsic '${symbol}' addresses context slot ${slot}, but a ` +
84
+ `thread has ${NUM_CONTEXT_SLOTS}`);
85
+ if (match[1] === "get")
86
+ return () => canonContextGet(slot);
87
+ return (v) => {
88
+ canonContextSet(slot, (v ?? 0) >>> 0);
89
+ };
90
+ }
@@ -0,0 +1,13 @@
1
+ // Shared failure type for trampolines/intrinsics scheduled after the current
2
+ // milestone. Split out of ./mod.ts so sibling intrinsic modules can raise it
3
+ // without importing the (much larger) trampoline dispatcher.
4
+ /** Instantiate-time failure for functionality scheduled after M0. */
5
+ export class UnsupportedFeatureError extends Error {
6
+ milestone;
7
+ constructor(milestone, what) {
8
+ super(`${what} — scheduled for ${milestone}, not implemented in the current ` +
9
+ `executor (contracts/intrinsics.md §B)`);
10
+ this.milestone = milestone;
11
+ this.name = "UnsupportedFeatureError";
12
+ }
13
+ }