@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,865 @@
1
+ // FACT cross-component call intrinsics: `prepare-call`, `sync-start-call` and
2
+ // `async-start-call` (contracts/intrinsics.md §A).
3
+ //
4
+ // These are how one component calls another's *async-lifted* export, or how an
5
+ // async-lowered import reaches any export. They have no direct analogue in
6
+ // definitions.py, because the reference has no fused adapters: there,
7
+ // `canon_lower` calls the callee's `FuncInst` directly and the host performs
8
+ // every copy. wasmtime instead compiles a FACT adapter that hoists the copying
9
+ // into wasm and asks the host to do only the task bookkeeping. The *semantics*
10
+ // are the reference's; only the division of labour differs.
11
+ //
12
+ // ===========================================================================
13
+ // THE PROTOCOL (wasmtime-environ 47.0.3)
14
+ // ===========================================================================
15
+ //
16
+ // Emission sites: `fact/trampoline.rs` — `call_prepare` (line 513),
17
+ // `compile_async_to_async_adapter` (474), `compile_sync_to_async_adapter`
18
+ // (607), `compile_async_to_sync_adapter` (643). Signatures: `fact.rs`
19
+ // `import_prepare_call` (584), `import_sync_start_call` (620),
20
+ // `import_async_start_call` (643), with `PREPARE_CALL_FIXED_PARAMS` at
21
+ // `fact.rs:47`.
22
+ //
23
+ // prepare-call(start: funcref, return: funcref,
24
+ // caller_instance: i32, callee_instance: i32,
25
+ // task_return_type: i32, callee_async: i32,
26
+ // string_encoding: i32, result_count_or_max_if_async: i32,
27
+ // ...caller's own flat params) -> ()
28
+ //
29
+ // sync-start-call (callee: funcref, lift_param_count: i32)
30
+ // -> the caller's flat results
31
+ // async-start-call(callee: funcref, param_count: i32,
32
+ // result_count: i32, flags: i32) -> i32 (packed subtask)
33
+ //
34
+ // **The `start` / `return` funcrefs are the reference's `on_start` /
35
+ // `on_resolve`.** That is the load-bearing finding, and their signatures
36
+ // (`fact/signature.rs`) say so exactly:
37
+ //
38
+ // `[async-start]` (async_start_signature, line 61)
39
+ // params = the *caller's* flattened params (what prepare-call stashed)
40
+ // results = the *callee's* flattened params (hand straight to the callee)
41
+ // i.e. "given the caller's arguments, produce the callee's" — `on_start`.
42
+ //
43
+ // `[async-return]` (async_return_signature, line 145)
44
+ // params = the *callee's* flattened results (+ a retptr when the caller
45
+ // is async-with-results, or when the caller's results spill)
46
+ // results = the *caller's* flattened results (empty if async/spilled)
47
+ // i.e. "given the callee's results, produce the caller's" — `on_resolve`.
48
+ //
49
+ // So the host never inspects a value: it calls `start` to get the callee's
50
+ // arguments, and calls `return` with whatever the callee produced. This is why
51
+ // a FACT task's payload is flat core values (`Task.factPassthrough`).
52
+ //
53
+ // Two details that fall out of the emission sites:
54
+ //
55
+ // * `prepare-call` must NOT run the callee. The callee may be exerting
56
+ // backpressure, and the whole point of splitting prepare from start is to
57
+ // let the host stash the parameters until it clears (fact.rs:580-583).
58
+ // The stashed state feeds `Task.enterImplicitThread`, which is exactly
59
+ // where the reference's backpressure gate lives.
60
+ // * Reentrance between *related* instances is resolved statically:
61
+ // `trampoline.rs:116-127` emits an unconditional
62
+ // `trap(Trap::CannotEnterComponent)` when the lower and lift instances are
63
+ // the same or are ancestors of one another. So the flat-instance-tree gap
64
+ // recorded in task/mod.ts is NOT load-bearing here — wasmtime has already
65
+ // decided those cases at translation time, and the remaining runtime check
66
+ // is the ordinary "is the callee instance currently executing" one, which
67
+ // a flat tree answers correctly.
68
+ import { assert_, trap } from "../cabi/trap.js";
69
+ import { MAX_FLAT_RESULTS } from "../cabi/mod.js";
70
+ import { NeedsJspi, currentTask, maybeCurrentTask, needsJspi, notifyInstancePoisoned, packSubtaskResult, PendingCapability, Subtask, SubtaskState, Task, Thread, withPoisonCause, } from "../task/mod.js";
71
+ import { blockCurrentActivation, enterWasm } from "../jspi/mod.js";
72
+ import { awaitCore, callCore, normalizeCoreValues, runCallbackLoop, } from "../exec/boundary.js";
73
+ import { traceCopy } from "./stream_builtins.js";
74
+ /** `PREPARE_ASYNC_NO_RESULT` (wasmtime-environ `component.rs:39`). */
75
+ const PREPARE_ASYNC_NO_RESULT = 0xffff_ffff;
76
+ /** `PREPARE_ASYNC_WITH_RESULT` (`component.rs:45`). */
77
+ const PREPARE_ASYNC_WITH_RESULT = 0xffff_fffe;
78
+ /** `START_FLAG_ASYNC_CALLEE` (`component.rs:52`). */
79
+ export const START_FLAG_ASYNC_CALLEE = 1;
80
+ /** Number of fixed leading parameters of `prepare-call` (`fact.rs:47`). */
81
+ const PREPARE_FIXED = 8;
82
+ /** definitions.py-shaped canonical options a FACT task must remember. */
83
+ function taskOptionsFor(prepared, callback, memory, calleeUsesAsyncAbi) {
84
+ return {
85
+ // NOTE the distinction definitions.py draws and this code initially got
86
+ // wrong: `Task.ft.async` is the *function type*'s asyncness (what
87
+ // `prepare-call` passes as `callee_async`), while `Task.opts.async_` is
88
+ // the *canonical options*' asyncness — and `canon_lift` branches on the
89
+ // latter (`if not opts.async_:` at line 2168). A function can be
90
+ // async-*typed* yet lifted with sync options, in which case the reference
91
+ // takes its plain synchronous path. Branching on the type instead sent
92
+ // those callees down the stackful path and reported a bogus JSPI
93
+ // requirement (`test/async/cross-abi-calls.wast`'s `async-calls-sync-*`).
94
+ async_: calleeUsesAsyncAbi,
95
+ callback: callback !== null,
96
+ stringEncoding: stringEncodingName(prepared.stringEncoding),
97
+ memory,
98
+ };
99
+ }
100
+ /**
101
+ * wasmtime's `StringEncoding` discriminant (`component/types.rs`), as passed
102
+ * through `prepare-call`.
103
+ */
104
+ function stringEncodingName(v) {
105
+ switch (v) {
106
+ case 0:
107
+ return "utf8";
108
+ case 1:
109
+ return "utf16";
110
+ case 2:
111
+ return "latin1+utf16";
112
+ default:
113
+ return "utf8";
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // prepare-call
118
+ // ---------------------------------------------------------------------------
119
+ export function createPrepareCall(decl, ctx) {
120
+ return (...args) => {
121
+ assert_(args.length >= PREPARE_FIXED, `prepare-call: expected at least ${PREPARE_FIXED} arguments`);
122
+ const [start, return_, callerI, calleeI, taskReturnType, calleeAsync, enc, rc_] = args;
123
+ assert_(typeof start === "function" && typeof return_ === "function", "prepare-call: start/return must be funcrefs");
124
+ assert_(ctx.prepared.current === null, "prepare-call with a preparation already outstanding");
125
+ // GAP (tracked): wasmtime performs a `check_blocking` here —
126
+ // if let (CallerInfo::Sync { .. }, true) = (&caller_info, callee_async) {
127
+ // store.0.check_blocking()?; // concurrent.rs:2802-2807
128
+ // }
129
+ // i.e. a *sync-lowered* caller reaching an *async-typed* callee must
130
+ // itself have been created by an async export, else it traps: only a task
131
+ // that is allowed to block may make a blocking call. We cannot evaluate it
132
+ // yet — it needs a "may this task block" bit on `Task`, which the reference
133
+ // models through its thread/task structure rather than a flag. Its absence
134
+ // means we accept some components wasmtime rejects; it never causes a
135
+ // wrong answer for an accepted one. `test/async/trap-if-block-and-sync.wast`
136
+ // is the file that exercises it, and that file is independently blocked on
137
+ // the wasmparser pin drift, so nothing observable depends on it today.
138
+ const params = args.slice(PREPARE_FIXED);
139
+ const rc = Number(rc_) >>> 0;
140
+ // wasmtime `ResultInfo` (concurrent.rs:2815-2836).
141
+ const lastParam = () => {
142
+ assert_(params.length > 0, "prepare-call: retptr missing");
143
+ return params[params.length - 1];
144
+ };
145
+ let resultInfo;
146
+ let asyncCallerWithResult = false;
147
+ if (rc === PREPARE_ASYNC_WITH_RESULT) {
148
+ resultInfo = { kind: "heap", retptr: lastParam() };
149
+ asyncCallerWithResult = true;
150
+ }
151
+ else if (rc === PREPARE_ASYNC_NO_RESULT) {
152
+ resultInfo = { kind: "stack" };
153
+ }
154
+ else if (rc > MAX_FLAT_RESULTS) {
155
+ // Sync caller whose results spilled: the adapter appended a retptr to
156
+ // its own parameters (`flatten_functype` lower/spill path).
157
+ resultInfo = { kind: "heap", retptr: lastParam() };
158
+ }
159
+ else {
160
+ resultInfo = { kind: "stack" };
161
+ }
162
+ ctx.prepared.current = {
163
+ start: start,
164
+ return_: return_,
165
+ callerInst: ctx.componentInstance(Number(callerI) >>> 0),
166
+ calleeInst: ctx.componentInstance(Number(calleeI) >>> 0),
167
+ taskReturnType: Number(taskReturnType) >>> 0,
168
+ calleeAsync: Number(calleeAsync) !== 0,
169
+ stringEncoding: Number(enc) >>> 0,
170
+ resultCountOrMax: rc,
171
+ params,
172
+ memory: decl.memory === null ? null : ctx.memoryToken(decl.memory),
173
+ resultInfo,
174
+ asyncCallerWithResult,
175
+ };
176
+ // Deliberately does not touch the callee: see the header. The callee may
177
+ // be under backpressure, and `*-start-call` is what runs it.
178
+ };
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // The shared callee activation
182
+ // ---------------------------------------------------------------------------
183
+ /**
184
+ * Build the `Task` for a prepared call and the generator body that runs the
185
+ * callee on it. Shared by both `*-start-call` forms; they differ only in how
186
+ * they *wait* for the result.
187
+ */
188
+ function mkCalleeTask(input) {
189
+ const { prepared, callee, callback, postReturn, ctx, calleeUsesAsyncAbi } = input;
190
+ // CONTRACT: default to `plain` when the context predates this field. Only
191
+ // `jspi` may wrap, and wrapping a non-wasm callee throws outright, so the
192
+ // conservative reading of an absent mode is "no suspension discipline".
193
+ const mode = input.mode ?? "plain";
194
+ // CONTRACT: default false -- a context that cannot answer the question gets
195
+ // the non-wrapping (plain-shaped) behaviour, which is the conservative one:
196
+ // it never forces asynchrony that the ABI forbids.
197
+ const canBlock = input.canBlock ?? false;
198
+ const memory = prepared.memory;
199
+ const inst = prepared.calleeInst;
200
+ // `ft` for the task: only `async` and `results` are consulted —
201
+ // `Task.needsExclusive` reads the former, `canon_task_return`'s result-type
202
+ // check reads the latter (which is why `prepare-call` carries
203
+ // `task_return_type` at all: fact.rs's comment on `PrepareCall.memory` says
204
+ // the same for the memory check).
205
+ // `task_return_type` arrives as wasmtime's *own* `TypeTupleIndex` — a
206
+ // runtime argument, not a plan field. Plan v3 (contracts/plan-format.md v3
207
+ // amendment 3) supplies the dictionary for it: every `task-return`
208
+ // trampoline decl carries that raw index alongside its interned
209
+ // `plan.types` entry, so the callee task CAN now carry its declared result
210
+ // types and `canon_task_return`'s `trap_if(result_type != task.ft.result)`
211
+ // applies to FACT tasks too (async_builtins.ts).
212
+ //
213
+ // `null` = the plan has no `task.return` trampoline for this tuple, i.e.
214
+ // the callee cannot call `task.return` at all (a sync-lifted callee reached
215
+ // through an async-to-sync adapter). Then the check has nothing to compare
216
+ // against and stays skipped — flagged by `factResultTypesKnown` rather than
217
+ // by an empty-results coincidence.
218
+ const declaredResults = ctx.resultTypesForTuple(prepared.taskReturnType);
219
+ const ft = {
220
+ params: [],
221
+ results: declaredResults ?? [],
222
+ async: prepared.calleeAsync,
223
+ };
224
+ const task = new Task(ft, taskOptionsFor(prepared, callback, memory, calleeUsesAsyncAbi), inst,
225
+ // on_start: the adapter's `[async-start]` turns the caller's flat params
226
+ // into the callee's flat params (fact/signature.rs:61).
227
+ //
228
+ // An async caller that has a result passes its retptr as the *last* flat
229
+ // parameter; `[async-start]` does not declare it, so it is chopped off
230
+ // here exactly as wasmtime does (concurrent.rs:2869-2876, "Async callers,
231
+ // if they have a result, use the last parameter as a return pointer so
232
+ // chop that off"). Sync callers forward everything directly.
233
+ () => {
234
+ // Open the FACT borrow window for the duration of the copy adapter:
235
+ // `[async-start]` is where argument resource transfers run, and it
236
+ // cannot block (see the WASM-ENTRY note below), so push/pop brackets a
237
+ // strictly synchronous window. Borrow bookkeeping lands on this
238
+ // (callee) task's `numBorrows` and the caller's lender scope — see
239
+ // intrinsics/mod.ts `FactStartScope`.
240
+ ctx.factStartScopes.push({ taskScope: task, lenders: input.lenderScope });
241
+ let calleeArgs;
242
+ try {
243
+ calleeArgs = callCore(prepared.start, prepared.asyncCallerWithResult
244
+ ? prepared.params.slice(0, -1)
245
+ : prepared.params);
246
+ }
247
+ finally {
248
+ ctx.factStartScopes.pop();
249
+ }
250
+ input.onStarted?.();
251
+ return calleeArgs;
252
+ },
253
+ // on_resolve: the adapter's `[async-return]` turns the callee's flat
254
+ // results into the caller's (fact/signature.rs:145).
255
+ (result) => {
256
+ if (result === null) {
257
+ // Cancelled before returning: there is nothing for `[async-return]`
258
+ // to copy. The subtask's CANCELLED_BEFORE_* state carries the news,
259
+ // so signal it rather than a normal empty result.
260
+ input.onCallerResults(null);
261
+ return;
262
+ }
263
+ // `[async-return]` takes the callee's flat results and, when the
264
+ // caller's results live in linear memory, the caller-supplied return
265
+ // pointer as a trailing argument (fact/signature.rs:166,178; appended by
266
+ // wasmtime at concurrent.rs:2916-2919). Omitting it made the adapter
267
+ // read `undefined` for that parameter, which coerces to 0 — every
268
+ // spilled result was written to linear-memory address 0.
269
+ const args = result;
270
+ const withRetptr = prepared.resultInfo.kind === "heap"
271
+ ? [...args, prepared.resultInfo.retptr]
272
+ : args;
273
+ input.onCallerResults(callCore(prepared.return_, withRetptr));
274
+ });
275
+ task.factPassthrough = true;
276
+ task.factResultTypesKnown = declaredResults !== null;
277
+ const body = function* (thread) {
278
+ if (!(yield* task.enterImplicitThread(thread)))
279
+ return;
280
+ const calleeArgs = task.start();
281
+ traceCopy(`mkCalleeTask callee canBlock=${canBlock} mode=${mode}`);
282
+ // WASM ENTRY (3 of 3 that can reach a blocking built-in).
283
+ //
284
+ // The other two — a lifted export's core function and a callback export —
285
+ // are entered through `awaitCore`, which establishes the
286
+ // activation-attached ambient. This one was not, and it is precisely the
287
+ // entry that owns a FACT sync-call bracket: `enter-sync-call` runs here
288
+ // under this task, and if the callee suspends, the engine resumes it later
289
+ // with no driver. Without the ambient travelling with the activation the
290
+ // matching `exit-sync-call` had no task in scope at all (traced in M2
291
+ // phase 3h as `ENTER-SYNC owner=K26` / `EXIT-SYNC owner=EXECUTOR`).
292
+ //
293
+ // Entries deliberately NOT wrapped: `realloc`, `post-return`, resource
294
+ // destructors and the `[async-start]`/`[async-return]` copy adapters.
295
+ // None of them may block — they cannot reach a canonical built-in that
296
+ // suspends — so the engine can never resume them, and wrapping would only
297
+ // cost an ALS frame on the hot copy path.
298
+ // The callee is its own activation and must get its own `promising`
299
+ // entry, not merely an ambient scope: otherwise it runs *inside* whatever
300
+ // `Suspending` trampoline invoked us, putting our JS frame between the
301
+ // caller's promising entry and any suspension the callee reaches --
302
+ // `SuspendError: trying to suspend JS frames` (jspi pin (b), mechanics.ts
303
+ // line 12). This is only coherent together with site 1 below blocking
304
+ // rather than raising `NeedsJspi`, since a promising callee resolves on a
305
+ // later turn by construction.
306
+ // NOTE (M2 stackful round): this wrap is RIGHT for a callee that blocks and
307
+ // Wrap ONLY a callee that can actually reach a suspension point.
308
+ //
309
+ // The wrap is required when the callee blocks: without its own `promising`
310
+ // entry it would suspend inside whatever `Suspending` trampoline invoked
311
+ // us, with our JS frame in between (`SuspendError: trying to suspend JS
312
+ // frames`, jspi pin (b)). But `enterWasm` returns a Promise
313
+ // unconditionally, so wrapping a callee that CANNOT block forces
314
+ // asynchrony the ABI forbids: an eagerly-completing callee must report its
315
+ // subtask RETURNED, and a wrapped one reports STARTED. That broke all six
316
+ // `async-calls-sync-*` cases of cross-abi-calls.wast.
317
+ //
318
+ // There is no per-CALL discriminator -- the same call site serves both --
319
+ // so the answer is per-callee, derived from whether the callee's core
320
+ // instance imports any blocking trampoline (`Executor.suspendableFuncs`).
321
+ const raw = yield* awaitCore(canBlock ? enterWasm(callee, mode) : callee, calleeArgs, thread);
322
+ if (!calleeUsesAsyncAbi) {
323
+ // Sync canonical options (definitions.py `canon_lift` line 2168, `if not
324
+ // opts.async_`): the callee returns its results directly and resolves
325
+ // before returning. Reached via `compile_async_to_sync_adapter`, which
326
+ // passes flags without `START_FLAG_ASYNC_CALLEE`.
327
+ task.return_(raw);
328
+ if (postReturn !== null) {
329
+ assert_(inst.mayLeave, "post-return with may_leave already false");
330
+ inst.mayLeave = false;
331
+ // NO local try/finally here, deliberately (#91, verified rather than
332
+ // assumed). definitions.py `canon_lift` (lines 2170-2174) has the
333
+ // same bare bracket: a trapping post-return skips `may_leave = True`
334
+ // and, since `Store.lift`'s `leave_to` is also skipped, leaves the
335
+ // instance poisoned — restoring `may_leave` locally would contradict
336
+ // both. What this runtime additionally needs, because it supports
337
+ // post-trap re-entry, is that no *live* instance is stranded with
338
+ // `may_leave === false`; exec/boundary.ts `unwind` covers exactly
339
+ // that: at the host boundary no lift or lower is in flight, so it
340
+ // asserts that resting state for every instance outside the poisoned
341
+ // entered set. This instance is either in that set (poisoned, left
342
+ // as the trap left it) or restored there.
343
+ callCore(postReturn, raw);
344
+ inst.mayLeave = true;
345
+ ctx.stats.postReturnsRun++;
346
+ }
347
+ task.exitImplicitThread(thread);
348
+ return;
349
+ }
350
+ if (callback === null) {
351
+ // Stackful async lift -- definitions.py `canon_lift` line 2178:
352
+ //
353
+ // if not opts.callback:
354
+ // [] = call_and_trap_on_throw(callee, flat_args)
355
+ // task.exit_implicit_thread()
356
+ // return
357
+ //
358
+ // That is the whole path. The callee runs to completion on its own
359
+ // stack, returning NO results and calling `task.return` itself; any
360
+ // blocking happened *inside* it, through the canonical built-ins. Which
361
+ // is exactly what the callee's own `promising` entry provides when it
362
+ // can block -- the `awaitCore` above parks the CALLEE's thread, not the
363
+ // caller's, so nothing here parks an async-lowered caller (the mistake
364
+ // the cross-abi differential caught).
365
+ normalizeCoreValues(raw, [], "stackful callee result");
366
+ task.exitImplicitThread(thread);
367
+ return;
368
+ }
369
+ const [packed] = normalizeCoreValues(raw, ["i32"], "callee result");
370
+ yield* runCallbackLoop({
371
+ name: "fact-callee",
372
+ task,
373
+ thread,
374
+ inst,
375
+ // The callback re-entry is the second of the three entries that can
376
+ // reach a blocking built-in (jspi/bridge.ts's invariant) and gets the
377
+ // same per-callee treatment as the initial entry above: a callee that
378
+ // parks (WAIT) and then, on a later callback activation, reaches a
379
+ // *synchronous* blocking built-in (wit-bindgen's `block_on` shape —
380
+ // e.g. a composed iroh endpoint signing a CertificateVerify via
381
+ // `waitable-set.wait` mid-handshake) suspends inside the plain
382
+ // callback frame otherwise: `SuspendError` (jspi pin (c)). Caught by
383
+ // the first composed consumer workload (wosh client), not by
384
+ // cross-abi-calls.wast, whose callees only ever block via WAIT codes.
385
+ callback: canBlock ? enterWasm(callback, mode) : callback,
386
+ packed,
387
+ stats: ctx.stats,
388
+ });
389
+ task.exitImplicitThread(thread);
390
+ };
391
+ return { task, body };
392
+ }
393
+ /** Take the outstanding preparation, or trap if the adapter skipped it. */
394
+ function takePrepared(ctx, what) {
395
+ const p = ctx.prepared.current;
396
+ assert_(p !== null, `${what} without a preceding prepare-call`);
397
+ ctx.prepared.current = null;
398
+ return p;
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // sync-start-call
402
+ // ---------------------------------------------------------------------------
403
+ /**
404
+ * A sync-lowered import calling an async-lifted export
405
+ * (`compile_sync_to_async_adapter`, trampoline.rs:607). The caller's wasm frame
406
+ * is blocked for the duration, so this must produce the results *now*.
407
+ */
408
+ export function createSyncStartCall(decl, ctx) {
409
+ return (callee, _liftParamCount) => {
410
+ const prepared = takePrepared(ctx, "sync-start-call");
411
+ assert_(typeof callee === "function", "sync-start-call: callee funcref");
412
+ const callback = decl.callback === null
413
+ ? null
414
+ : ctx.callback(decl.callback);
415
+ let callerResults = null;
416
+ // The caller's frame is blocked for the whole call, so resolution
417
+ // delivery = this intrinsic returning results: release lenders then
418
+ // (the sync analogue of `Subtask.deliver_resolve`, definitions.py 904).
419
+ // Inlined rather than reusing `SyncCallScope` to keep this module free
420
+ // of a value-level import cycle with intrinsics/mod.ts.
421
+ const lentHandles = [];
422
+ const lenderScope = {
423
+ addLender(h) {
424
+ h.numLends += 1;
425
+ lentHandles.push(h);
426
+ },
427
+ releaseLenders() {
428
+ for (const h of lentHandles)
429
+ h.numLends -= 1;
430
+ lentHandles.length = 0;
431
+ },
432
+ };
433
+ const { task, body } = mkCalleeTask({
434
+ prepared,
435
+ callee: callee,
436
+ callback,
437
+ postReturn: null,
438
+ ctx,
439
+ // `sync-start-call` exists only for "sync-lowered import to async-lifted
440
+ // export" (fact.rs:608), so the callee always uses the async ABI.
441
+ calleeUsesAsyncAbi: true,
442
+ mode: ctx.suspensionMode,
443
+ canBlock: ctx.calleeCanBlock?.(callee) ?? false,
444
+ onCallerResults: (r) => {
445
+ // sync-start-call's callee always uses the async ABI (comment above),
446
+ // but the *caller* side here is the sync `canon_lower` path: the
447
+ // reference's on_resolve(None) case is reached only when a
448
+ // cancellation was requested, and a sync-lowered subtask has no
449
+ // handle and hence no cancel channel — so `r` can never be null here.
450
+ assert_(r !== null, "sync-start-call: caller results missing");
451
+ callerResults = r;
452
+ },
453
+ lenderScope,
454
+ });
455
+ // Reference `Store.lift`: the reentrance gate, with the *caller* as the
456
+ // entering context (definitions.py `entering_set(caller)`).
457
+ // A poisoned callee's refusal names the original trap (polyengine#145).
458
+ if (!prepared.calleeInst.mayEnterFrom(prepared.callerInst)) {
459
+ trap(withPoisonCause(prepared.calleeInst, "cannot enter component instance"));
460
+ }
461
+ prepared.calleeInst.enterFrom(prepared.callerInst);
462
+ let ok = false;
463
+ try {
464
+ const thread = spawn(task, body);
465
+ thread.resume();
466
+ ok = true;
467
+ }
468
+ catch (e) {
469
+ // A trap leaves the instance poisoned: `leave_to` is not reached
470
+ // (definitions.py `Store.lift`, line 578). A *capability signal* does
471
+ // not — see the `isCapabilitySignal` note in exec/boundary.ts.
472
+ if (e instanceof NeedsJspi || e instanceof PendingCapability) {
473
+ prepared.calleeInst.leaveTo(prepared.callerInst);
474
+ }
475
+ else {
476
+ // Retire the poisoned CALLEE's stream/future ends (#66): this is a
477
+ // bracket-break site like `Store.tick`'s, and the trap unwinds to a
478
+ // hooked site that walks only the CALLER's chain — a composed
479
+ // component's callee would otherwise strand its host peers.
480
+ notifyInstancePoisoned(prepared.calleeInst, e);
481
+ }
482
+ // The lent handles are the CALLER's, and the caller is not poisoned by
483
+ // either exit (contracts/intrinsics.md v0.2 amendment 2: this runtime
484
+ // deliberately supports post-trap re-entry on the caller side, where
485
+ // the reference kills the whole store, so the sync-call scopes it
486
+ // skipped have to be unwound explicitly). Leaving `numLends` elevated
487
+ // would make every later `lift_own`/`resource.drop` of those handles
488
+ // trap "handle still lent out" (#91). Release is idempotent, and the
489
+ // success path below is unchanged.
490
+ lenderScope.releaseLenders();
491
+ throw e;
492
+ }
493
+ if (ok)
494
+ prepared.calleeInst.leaveTo(prepared.callerInst);
495
+ if (callerResults === null) {
496
+ // The callee did not resolve within its first activation. definitions.py
497
+ // `canon_lower`'s sync path blocks here — `thread.wait_until(
498
+ // subtask.resolved)` (line 2286) — suspending the *caller's* wasm frame
499
+ // while the scheduler runs other threads. That is JSPI role 2 (docs/architecture.md
500
+ // §6), and it is the first place a purely stackless runtime genuinely
501
+ // cannot proceed.
502
+ //
503
+ // Note this is NOT the sync driving loop of `canon_lift`: that loop
504
+ // drives the callee instance's own threads and is only correct when the
505
+ // callee can finish without anything from the caller. Here the caller is
506
+ // mid-frame and may be exactly what the callee is waiting for, so
507
+ // pumping the callee alone would spin rather than make progress.
508
+ // Note: the callee's thread stays parked in `store.waiting` when we bail
509
+ // here. That is deliberate — unwinding it would run callee cleanup the
510
+ // guest never asked for — but it does mean the store keeps a thread that
511
+ // will never be resumed. Harmless today (the enclosing host call is
512
+ // failing anyway, and the instance is not poisoned because no trap
513
+ // escaped a task), and it disappears once JSPI lets this path actually
514
+ // block instead of bailing.
515
+ if (ctx.suspensionMode === "jspi") {
516
+ // JSPI role 2 (docs/architecture.md §6): park the *caller's* wasm activation until
517
+ // the callee resolves, exactly as definitions.py `canon_lower`'s sync
518
+ // path does with `thread.wait_until(subtask.resolved)` (line 2286).
519
+ // The scheduler keeps ticking the callee meanwhile; when it produces
520
+ // results our `readyFunc` goes true and the engine resumes the caller.
521
+ //
522
+ // Not cancellable: a sync-lowered caller has no way to observe or
523
+ // request cancellation mid-call -- the reference's wait here carries
524
+ // no cancellation branch.
525
+ // LENDER RELEASE ON EVERY SETTLE PATH (#102).
526
+ //
527
+ // Enumeration of how this `SuspensionPoint` can reach a terminal
528
+ // state (jspi/bridge.ts `SuspensionPoint`), and whether `produce`
529
+ // runs on each:
530
+ //
531
+ // 1. `resume(false)` -> `produce` returns the packed result.
532
+ // RUNS. This is the success path; release stays INSIDE `produce`,
533
+ // before the results are shaped, so its ordering relative to the
534
+ // produced value is unchanged by this fix.
535
+ // 2. `resume(false)` -> `produce` throws (a trap computed at resume
536
+ // time). PARTIALLY RUNS. Release is `produce`'s first statement
537
+ // so it is already discharged here, but the `onSettled` backstop
538
+ // makes that independent of statement order.
539
+ // 3. `resume(true)` — a CANCELLED resume. Unreachable by
540
+ // construction: this park is `cancellable: false` and
541
+ // `SuspensionPoint.resume` asserts `cancellable || !cancelled`
542
+ // (#93). Note the assert fires BEFORE `#done` is set, so such a
543
+ // call leaves the point still parked and never settles it — a
544
+ // scheduler bug, not a guest-reachable exit; there is no
545
+ // non-poisoning continuation to release into.
546
+ // 4. `abandon(reason)` — store teardown / abandonment: fails the
547
+ // import's Promise WITHOUT calling `produce`. DOES NOT RUN. This
548
+ // is the #102 hole; `onSettled` covers it.
549
+ // 5. Never settled at all (the store is dropped while this point
550
+ // sits in `store.waiting`, e.g. the caller's whole host call was
551
+ // abandoned). No JS runs, so nothing can release; the lent
552
+ // handles die with the store, which is the reference's own
553
+ // outcome. Out of scope for amendment 2 (no non-poisoning exit).
554
+ // 6. Trap-poisoning of the parked instance: does not settle this
555
+ // point by itself — it reaches the guest either as (2) (a
556
+ // produce-time trap) or as (4) (teardown abandons the park), so
557
+ // it is covered by those two rows, not a third mechanism.
558
+ //
559
+ // `releaseLenders` is idempotent (#91), so the backstop is a no-op
560
+ // whenever `produce` already ran.
561
+ return blockCurrentActivation({
562
+ store: prepared.callerInst.store,
563
+ task: currentTask(),
564
+ readyFunc: () => callerResults !== null,
565
+ cancellable: false,
566
+ produce: () => {
567
+ lenderScope.releaseLenders();
568
+ return shapeResults(callerResults);
569
+ },
570
+ onSettled: () => lenderScope.releaseLenders(),
571
+ });
572
+ }
573
+ // A capability signal is expressly NON-poisoning (see above), so
574
+ // stranding the caller's lenders here is strictly worse than on the
575
+ // trap path: the caller is guaranteed to keep running (#91).
576
+ lenderScope.releaseLenders();
577
+ needsJspi("sync-start-call whose async-lifted callee did not resolve in its " +
578
+ "first activation (the caller's wasm frame must block)");
579
+ }
580
+ lenderScope.releaseLenders();
581
+ return shapeResults(callerResults);
582
+ };
583
+ }
584
+ /**
585
+ * Release a never-delivered subtask's lenders after a trap or capability bail
586
+ * broke the `[async-start-call]` bracket (#91).
587
+ *
588
+ * Now a thin alias of `Subtask.unwindLenders` — the same unwind serves the
589
+ * host-import parks (exec/boundary.ts, #106) — kept for the local name the
590
+ * `[async-start-call]` comments reference.
591
+ */
592
+ function unwindSubtaskLenders(subtask) {
593
+ subtask.unwindLenders();
594
+ }
595
+ /** The core-ABI shape of a returned results vector (0 / 1 / many). */
596
+ function shapeResults(out) {
597
+ if (out === null || out.length === 0)
598
+ return undefined;
599
+ if (out.length === 1)
600
+ return out[0];
601
+ return out;
602
+ }
603
+ // ---------------------------------------------------------------------------
604
+ // async-start-call
605
+ // ---------------------------------------------------------------------------
606
+ /**
607
+ * An async-lowered import calling any export (`compile_async_to_async_adapter`
608
+ * / `compile_async_to_sync_adapter`). Returns the packed subtask status the
609
+ * guest already knows how to interpret — the same
610
+ * `state | (subtaski << 4)` encoding `canon_lower` produces
611
+ * (definitions.py line 2308), so the caller's callback loop and waitable sets
612
+ * work unchanged.
613
+ */
614
+ export function createAsyncStartCall(decl, ctx) {
615
+ return (callee, _paramCount, _resultCount, flags) => {
616
+ const prepared = takePrepared(ctx, "async-start-call");
617
+ assert_(typeof callee === "function", "async-start-call: callee funcref");
618
+ const callback = decl.callback === null
619
+ ? null
620
+ : ctx.callback(decl.callback);
621
+ // The caller-side view of this call. Everything downstream — waitable
622
+ // sets, `subtask.drop`, the SUBTASK event — is the machinery already built
623
+ // for host-import subtasks in exec/boundary.ts.
624
+ // Starts STARTING and becomes STARTED only when `[async-start]` runs (see
625
+ // `onStarted`). A callee held at the backpressure gate is therefore
626
+ // reported as STARTING, and the STARTED transition delivers its own event
627
+ // if the guest has already been handed a subtask index.
628
+ const subtask = new Subtask();
629
+ let onProgress = () => { };
630
+ const { task, body } = mkCalleeTask({
631
+ prepared,
632
+ callee: callee,
633
+ callback,
634
+ postReturn: decl.postReturn === null
635
+ ? null
636
+ : ctx.callback(decl.postReturn),
637
+ ctx,
638
+ // `compile_async_to_async_adapter` sets START_FLAG_ASYNC_CALLEE;
639
+ // `compile_async_to_sync_adapter` passes 0 (trampoline.rs:508 and :764).
640
+ calleeUsesAsyncAbi: ((flags ?? 0) & START_FLAG_ASYNC_CALLEE) !== 0,
641
+ mode: ctx.suspensionMode,
642
+ canBlock: ctx.calleeCanBlock?.(callee) ?? false,
643
+ onStarted: () => {
644
+ if (subtask.state === SubtaskState.STARTING) {
645
+ subtask.state = SubtaskState.STARTED;
646
+ // `onProgress` is a no-op until the guest has a handle for this
647
+ // subtask, mirroring `canon_lower`'s `maybe_on_progress`
648
+ // (definitions.py line 2296): a call that starts before
649
+ // `async-start-call` returns reports STARTED in its packed result
650
+ // instead, with no event.
651
+ onProgress();
652
+ }
653
+ },
654
+ onCallerResults: (r) => {
655
+ // `[async-return]` already wrote the caller's results (through the
656
+ // retptr the caller supplied), so there is nothing to carry here: the
657
+ // guest learns of completion from the SUBTASK event.
658
+ if (!subtask.resolved()) {
659
+ subtask.resolve(r === null
660
+ // definitions.py `canon_lower`'s `on_resolve` (line 2267): a
661
+ // cancelled callee resolves CANCELLED_BEFORE_{STARTED,RETURNED}
662
+ // depending on how far it got.
663
+ ? (subtask.state === SubtaskState.STARTING
664
+ ? SubtaskState.CANCELLED_BEFORE_STARTED
665
+ : SubtaskState.CANCELLED_BEFORE_RETURNED)
666
+ : SubtaskState.RETURNED, []);
667
+ }
668
+ onProgress();
669
+ },
670
+ // Borrow lenders attach to the caller-side subtask, released by its
671
+ // `deliverResolve` (definitions.py `Subtask.deliver_resolve`, line 904).
672
+ lenderScope: subtask,
673
+ });
674
+ // Cross-component cancellation: `subtask.cancel` forwards to the callee
675
+ // task's `request_cancellation` (definitions.py line 519), which delivers
676
+ // TASK_CANCELLED to a cancellable block point — for a callback-ABI callee
677
+ // that is its WAIT/YIELD, so the guest observes the cancellation and calls
678
+ // `task.cancel`, resolving this subtask CANCELLED_BEFORE_RETURNED.
679
+ subtask.onCancel = (callerInst) => task.requestCancellation(callerInst);
680
+ subtask.calleeTask = task;
681
+ // A poisoned callee's refusal names the original trap (polyengine#145).
682
+ if (!prepared.calleeInst.mayEnterFrom(prepared.callerInst)) {
683
+ trap(withPoisonCause(prepared.calleeInst, "cannot enter component instance"));
684
+ }
685
+ prepared.calleeInst.enterFrom(prepared.callerInst);
686
+ let ok = false;
687
+ let thread;
688
+ try {
689
+ thread = spawn(task, body);
690
+ thread.resume();
691
+ ok = true;
692
+ }
693
+ catch (e) {
694
+ // See the sync form above and `isCapabilitySignal` in exec/boundary.ts.
695
+ if (e instanceof NeedsJspi || e instanceof PendingCapability) {
696
+ prepared.calleeInst.leaveTo(prepared.callerInst);
697
+ }
698
+ else {
699
+ // Bracket-break site — retire the poisoned callee's ends (#66),
700
+ // as in the sync form above.
701
+ notifyInstancePoisoned(prepared.calleeInst, e);
702
+ }
703
+ // The subtask never reached `report()`, so it has no handle in the
704
+ // caller's table and nothing will ever deliver its resolution — but it
705
+ // holds `num_lends` on the caller's handles. Resolve it as cancelled
706
+ // (the state the reference's `on_resolve(None)` would give a call that
707
+ // never started/returned) and deliver, which is what releases the
708
+ // lenders (definitions.py `Subtask.deliver_resolve`, line 902). See the
709
+ // sync form above for why the caller must not be left holding them.
710
+ unwindSubtaskLenders(subtask);
711
+ throw e;
712
+ }
713
+ if (ok)
714
+ prepared.calleeInst.leaveTo(prepared.callerInst);
715
+ const report = () => {
716
+ if (subtask.resolved()) {
717
+ // Eager completion: no handle, no event (definitions.py line 2293).
718
+ subtask.deliverResolve();
719
+ traceCopy(`async-start-call -> RETURNED (eager)`);
720
+ return SubtaskState.RETURNED;
721
+ }
722
+ const subtaski = prepared.callerInst.handles.add(subtask);
723
+ onProgress = () => subtask.setSubtaskPendingEvent(subtaski);
724
+ const packed = packSubtaskResult(subtask.state, subtaski);
725
+ traceCopy(`async-start-call -> state=${subtask.state} i=${subtaski} ` +
726
+ `packed=0x${packed.toString(16)}`);
727
+ return packed;
728
+ };
729
+ // NO WAIT FOR RESOLUTION HERE, deliberately. An async-lowered caller must
730
+ // not block on its callee's *completion* -- that is the entire point of
731
+ // async lowering: it takes a subtask handle and learns of completion
732
+ // through events. An earlier attempt (M2 "Fix 1") parked the caller here
733
+ // until the callee resolved. It made cross-abi-calls agree in both modes,
734
+ // and it broke the thing it had no business touching: the caller's
735
+ // activation was now suspended, so the sync-lowered parked caller of
736
+ // `handshake_test.ts` was never resumed and the run hung. Correct-looking,
737
+ // semantically wrong.
738
+ //
739
+ // What jspi mode DOES need is a wait for **determinacy** (jspi pin (j),
740
+ // `fastpath_hop_test.ts`): the engine defers a promising callee's
741
+ // continuation to a microtask at EVERY Suspending call -- even one whose
742
+ // value was available synchronously -- so a callee the reference would
743
+ // run to completion inside this call (`canon_lift` drives the thread to
744
+ // its first real block point before `canon_lower` returns) is still
745
+ // mid-hop when `report()` runs. Reporting then is reporting a state the
746
+ // reference can never observe: STARTED for a call that eagerly RETURNED
747
+ // (big-interleaving's `call-import` scripts), or a missed synchronous
748
+ // cancellation (its `subtask-cancel` scripts).
749
+ //
750
+ // "Determinate" is exactly one of:
751
+ // * the subtask resolved (task.return ran mid-activation), or
752
+ // * the callee's thread finished (results flow through the body), or
753
+ // * the callee genuinely parked on a scheduler condition -- its
754
+ // SuspensionPoint (or its body's own wait) sits in `store.waiting`.
755
+ // A genuinely-blocking callee reaches its first real block point without
756
+ // anything from the caller, so unlike Fix 1 this wait cannot deadlock:
757
+ // it is the reference's atomic run-to-first-block, reconstructed across
758
+ // the engine's microtask hops.
759
+ //
760
+ // THE DEFERRED ENTRY DECISION (issue #43; wasmtime's model — source
761
+ // walkthrough distilled on the issue, exam kit archived at
762
+ // 4f3351f:exams/wasmtime-exclusivity/). The determinacy wait above is
763
+ // also where the initial *status* is decided, so it is where the
764
+ // deferral lives.
765
+ //
766
+ // In wasmtime a guest->guest call queues the callee's `StartImplicit`
767
+ // and the caller suspends until the first subtask status event
768
+ // (concurrent.rs :3040-3160); the executor first drains the work queued
769
+ // ahead of it, so a ready gate holder runs to invocation exit and
770
+ // releases `do_not_enter` BEFORE the new call's readiness is evaluated
771
+ // (:1497-1522). polyengine's callee thread is likewise already spawned and
772
+ // parked at `enter_implicit_thread`'s gate wait at this point; what
773
+ // changes here is only WHEN the caller reads `subtask.state`.
774
+ //
775
+ // Order-robust formulation (issue #43; a non-normative scheduler
776
+ // policy — entry-status timing is not normative — chosen over wasmtime's
777
+ // FIFO-dependent one so the seeded-shuffle reruns stay green): while the
778
+ // callee is still parked at the entry gate, the caller waits until the
779
+ // callee instance's runnable work is exhausted
780
+ // (`Store.hasRunnableWork`). Then:
781
+ // * the holder was ready -> it ran to `exit_implicit_thread`, released
782
+ // the gate, the callee entered: `subtask.state` is STARTED (or the
783
+ // callee already RETURNED) -- test/async/sync-streams.wast:145;
784
+ // * the holder was NOT ready (parked mid-frame on an un-rendezvous'd
785
+ // operation), or the holder IS this caller (a nested lower, excluded
786
+ // from the scan): quiescence is immediate and the caller reports
787
+ // STARTING -- hold semantics, observably.
788
+ //
789
+ // Backpressure-queue admission is untouched: the callee registered in
790
+ // `num_waiting_to_enter` synchronously at `thread.resume()` above,
791
+ // before any draining, so the deterministic-profile ordering pins
792
+ // (async-calls-sync.wast) see the same admission order as before.
793
+ //
794
+ // PLAIN MODE IS DELIBERATELY UNTOUCHED, and provably needs no drain: a
795
+ // needs-exclusive task holds `exclusiveThread` only across a core
796
+ // invocation (the callback loop releases it across every wait), and
797
+ // without JSPI a wasm frame cannot park mid-invocation at all. So in
798
+ // plain mode the gate, when held, is held by the *currently running*
799
+ // activation -- the one obstacle a drain can never remove. Zero cost for
800
+ // sync-only components, and no suspendability reclassification:
801
+ // `async-start-call` was already `Suspending`-wrapped for the
802
+ // determinacy park (exec/executor.ts, "async-start-call is wrapped").
803
+ if (ctx.suspensionMode === "jspi") {
804
+ const store = prepared.callerInst.store;
805
+ const calleeInst = prepared.calleeInst;
806
+ // The caller's task: excluded from the drain scan (it is the asker).
807
+ // `maybeCurrentTask` rather than `currentTask` because a host-driven
808
+ // entry can reach here with no ambient task at all.
809
+ const callerTask = maybeCurrentTask();
810
+ // STARTING + parked == parked at the entry gate: `[async-start]` runs
811
+ // immediately after `enter_implicit_thread` succeeds, so any callee
812
+ // that got past the gate has already left STARTING.
813
+ const gatedAtEntry = () => subtask.state === SubtaskState.STARTING &&
814
+ !subtask.resolved() &&
815
+ !thread.done() &&
816
+ thread.waiting();
817
+ const determinate = () => gatedAtEntry()
818
+ ? !store.hasRunnableWork(calleeInst, callerTask)
819
+ : subtask.resolved() ||
820
+ thread.done() ||
821
+ store.waiting.some((w) => w.task === task);
822
+ if (!determinate()) {
823
+ // Same settle-path enumeration as the sync form above (#102). Here
824
+ // the lender scope is the `Subtask` itself, discharged by
825
+ // `deliverResolve`, and `report()` is what eventually delivers it —
826
+ // either eagerly (the resolved branch) or, for a live subtask, via
827
+ // the handle it hands the guest. So the backstop must fire ONLY when
828
+ // `report()` did not complete: on the success path the subtask is
829
+ // typically still live and in the caller's table, and unwinding it
830
+ // there would cancel a perfectly good call.
831
+ //
832
+ // `report()` not completing means the guest never received the
833
+ // subtask index (it either threw before `handles.add`, or after it
834
+ // with the index lost), so nothing will ever deliver this subtask's
835
+ // resolution — exactly the state `unwindSubtaskLenders` exists for
836
+ // (contracts/intrinsics.md v0.2 amendment 2).
837
+ let produced = false;
838
+ return blockCurrentActivation({
839
+ store: prepared.callerInst.store,
840
+ task: currentTask(),
841
+ readyFunc: determinate,
842
+ cancellable: false,
843
+ produce: () => {
844
+ const r = report();
845
+ produced = true;
846
+ return r;
847
+ },
848
+ onSettled: () => {
849
+ if (!produced)
850
+ unwindSubtaskLenders(subtask);
851
+ },
852
+ });
853
+ }
854
+ }
855
+ return report();
856
+ };
857
+ }
858
+ /** Create a thread whose body needs a reference to the thread itself. */
859
+ function spawn(task, body) {
860
+ let thread;
861
+ thread = new Thread(task, (function* () {
862
+ yield* body(thread);
863
+ })());
864
+ return thread;
865
+ }