@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,786 @@
1
+ // Streams, futures and error-context: the async *value* types
2
+ // (definitions.py `### Stream State`, `### Future State`, `class ErrorContext`).
3
+ //
4
+ // ===========================================================================
5
+ // THE RENDEZVOUS, AND HOW IT MAPS ONTO OUR TASK CORE
6
+ // ===========================================================================
7
+ //
8
+ // A stream is not a buffer. `SharedStreamImpl` (definitions.py line 997) holds
9
+ // at most **one pending side** — a reader waiting for data, or a writer
10
+ // waiting for a reader — and a copy happens only when the second side arrives.
11
+ // That is the whole model:
12
+ //
13
+ // * first side to call `read`/`write` finds `pending_buffer == None` and
14
+ // parks itself via `set_pending(...)`;
15
+ // * second side finds a pending buffer, copies `min(remain, remain)`
16
+ // elements directly between the two guests' linear memories, and notifies
17
+ // *both* sides;
18
+ // * either side may be partially satisfied — that is not an error, it is the
19
+ // normal case, and the progress count is what the guest is told.
20
+ //
21
+ // Nothing here needs a scheduler: the copy is synchronous inside whichever
22
+ // call arrives second. What the scheduler provides is the *waiting*: a parked
23
+ // side has `CopyState.COPYING` and its `CopyEnd` (a `Waitable`) carries the
24
+ // pending event that wakes the guest.
25
+ //
26
+ // So the mapping to our task core is small and mechanical:
27
+ //
28
+ // reference here
29
+ // ------------------------------------------------------------------
30
+ // CopyEnd(Waitable) CopyEnd extends our Waitable — the same
31
+ // base the SUBTASK path already uses, so
32
+ // waitable sets, `waitable.join` and the
33
+ // callback loop's WAIT code all work unchanged
34
+ // set_pending_event(thunk) the same thunk indirection: the event payload
35
+ // (progress, result) is computed at *delivery*
36
+ // time, exactly as `Subtask.setSubtaskPendingEvent`
37
+ // already does. This is why the phase-1 decision
38
+ // to keep events as thunks rather than values
39
+ // pays off here with no generalization at all.
40
+ // STREAM_READ / STREAM_WRITE EventCode values, already defined
41
+ // FUTURE_READ / FUTURE_WRITE
42
+ //
43
+ // The one genuinely new thing is `Buffer`: a cursor over a guest's linear
44
+ // memory that can be *partially* consumed, which is what makes partial copies
45
+ // expressible.
46
+ import { defineBrand, ERROR_CONTEXT } from "@polyengine/protocol";
47
+ import { assert_, Trap, trapIf } from "../cabi/trap.js";
48
+ import { loadListFromValidRange } from "../cabi/load.js";
49
+ import { storeListIntoValidRange } from "../cabi/store.js";
50
+ import { alignment, alignTo, elemSize } from "../cabi/layout.js";
51
+ import { despecialize, valTypeEqual } from "../cabi/types.js";
52
+ import { Waitable } from "./waitable.js";
53
+ import { isInstancePoisoned, setOnInstancePoisoned } from "./scheduler.js";
54
+ /** Structural element-type equality (`null` = the zero-width payload).
55
+ * Delegates to `valTypeEqual`: naive `JSON.stringify` comparison throws on
56
+ * resource-bearing element types (cabi/types.ts `valTypeEqual` contract
57
+ * note; found by the #18 polymorph-tls smoke). */
58
+ export function sameElemType(a, b) {
59
+ if (a === null || b === null)
60
+ return a === b;
61
+ return valTypeEqual(a, b);
62
+ }
63
+ /** definitions.py `Buffer.MAX_LENGTH`. */
64
+ export const BUFFER_MAX_LENGTH = 2 ** 28 - 1;
65
+ /** definitions.py `CopyResult` (line 977). */
66
+ export var CopyResult;
67
+ (function (CopyResult) {
68
+ CopyResult[CopyResult["COMPLETED"] = 0] = "COMPLETED";
69
+ CopyResult[CopyResult["DROPPED"] = 1] = "DROPPED";
70
+ CopyResult[CopyResult["CANCELLED"] = 2] = "CANCELLED";
71
+ })(CopyResult || (CopyResult = {}));
72
+ /** definitions.py `CopyState` (line 1075). */
73
+ export var CopyState;
74
+ (function (CopyState) {
75
+ CopyState[CopyState["IDLE"] = 1] = "IDLE";
76
+ CopyState[CopyState["COPYING"] = 2] = "COPYING";
77
+ CopyState[CopyState["CANCELLING_COPY"] = 3] = "CANCELLING_COPY";
78
+ CopyState[CopyState["DONE"] = 4] = "DONE";
79
+ })(CopyState || (CopyState = {}));
80
+ // ---------------------------------------------------------------------------
81
+ // Buffers (definitions.py `class BufferGuestImpl`, line 930)
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * A cursor over `length` elements of type `t` at `ptr` in one guest's memory.
85
+ * `t === null` is the zero-width element type (`stream` with no payload),
86
+ * where only the *count* is meaningful.
87
+ */
88
+ export class GuestBuffer {
89
+ t;
90
+ cx;
91
+ ptr;
92
+ length;
93
+ progress = 0;
94
+ constructor(t, cx, ptr, length) {
95
+ this.t = t;
96
+ this.cx = cx;
97
+ this.ptr = ptr;
98
+ this.length = length;
99
+ trapIf(length > BUFFER_MAX_LENGTH, "buffer length exceeds MAX_LENGTH");
100
+ if (t !== null && length > 0) {
101
+ const mem = cx.opts.memory;
102
+ assert_(mem !== null, "buffer requires a memory");
103
+ const ptrType = mem.ptrType();
104
+ trapIf(ptr !== alignTo(ptr, alignment(t, ptrType)), "unaligned buffer pointer");
105
+ trapIf(ptr + length * elemSize(t, ptrType) > mem.length, "buffer out of bounds");
106
+ }
107
+ }
108
+ remain() {
109
+ return this.length - this.progress;
110
+ }
111
+ isZeroLength() {
112
+ return this.length === 0;
113
+ }
114
+ /** definitions.py `ReadableBufferGuestImpl.read`. */
115
+ read(n) {
116
+ assert_(n <= this.remain(), "buffer read beyond remaining");
117
+ let vs;
118
+ if (this.t !== null) {
119
+ vs = loadListFromValidRange(this.cx, this.ptr, n, this.t);
120
+ this.ptr += n * elemSize(this.t, this.cx.opts.memory.ptrType());
121
+ }
122
+ else {
123
+ vs = new Array(n).fill(null);
124
+ }
125
+ this.progress += n;
126
+ return vs;
127
+ }
128
+ /** definitions.py `WritableBufferGuestImpl.write`. */
129
+ write(vs) {
130
+ assert_(vs.length <= this.remain(), "buffer write beyond remaining");
131
+ if (this.t !== null) {
132
+ storeListIntoValidRange(this.cx, vs, this.ptr, this.t);
133
+ this.ptr += vs.length * elemSize(this.t, this.cx.opts.memory.ptrType());
134
+ }
135
+ else {
136
+ // definitions.py `WritableBufferGuestImpl.write`:
137
+ // `assert(all(v == () for v in vs))` — a zero-width stream carries no
138
+ // payload, so anything but the placeholder means a element-type mix-up
139
+ // upstream (a typed chunk here would be the same mix-up).
140
+ assert_(!(vs instanceof Uint8Array) && vs.every((v) => v === null), "zero-width buffer written with a non-empty element");
141
+ }
142
+ this.progress += vs.length;
143
+ }
144
+ }
145
+ /**
146
+ * definitions.py `none_or_number_type` (line 1070). Guards the "temporary"
147
+ * same-instance restriction below.
148
+ */
149
+ function noneOrNumberType(t) {
150
+ if (t === null)
151
+ return true;
152
+ switch (despecialize(t).kind) {
153
+ case "u8":
154
+ case "u16":
155
+ case "u32":
156
+ case "u64":
157
+ case "s8":
158
+ case "s16":
159
+ case "s32":
160
+ case "s64":
161
+ case "f32":
162
+ case "f64":
163
+ return true;
164
+ default:
165
+ return false;
166
+ }
167
+ }
168
+ export class SharedStreamImpl {
169
+ t;
170
+ /**
171
+ * Optional hook fired when this shared object is lowered into a component
172
+ * instance (`lower_stream`/`lower_future`). Host-owned ends use it to learn
173
+ * which `Store` is driving the guest they were just handed to; guest-owned
174
+ * streams leave it unset. Keeps `cabi` free of any host-stream knowledge.
175
+ */
176
+ onLowered = null;
177
+ /**
178
+ * Optional hook fired by `liftAsyncValue` whenever this object is lifted
179
+ * OUT of a guest table. The receiver — the host, or the destination of a
180
+ * guest-to-guest hop, in which case the immediately following lower fires
181
+ * `onLowered` — may now act on the transferred end. Host wrappers use it to
182
+ * re-arm their activity (#162, embedder-api amendment A15). Guest-owned
183
+ * objects leave it unset.
184
+ */
185
+ onLifted = null;
186
+ /**
187
+ * The `Store` driving the component this object has been handed to, set the
188
+ * first time it is lifted or lowered. Host ends need it to pump the guest
189
+ * between export calls (see exec/host_streams.ts `HostActivity.pump`); a
190
+ * purely guest-to-guest stream never reads it.
191
+ */
192
+ boundStore = null;
193
+ dropped = false;
194
+ pendingInst = null;
195
+ pendingBuffer = null;
196
+ pendingOnCopy = null;
197
+ pendingOnCopyDone = null;
198
+ /**
199
+ * Observers fired once, when this stream becomes dropped — by EITHER
200
+ * side, including the A7 teardown walk (`dropSharedForTeardown`). The
201
+ * embedder's producer pump uses this to cancel a producer parked on an
202
+ * external event (amendment A13's cancellation companion: an
203
+ * accept-shaped producer holds a live platform resource while parked,
204
+ * and the reader dropping is its only stop signal). `null` = already
205
+ * fired.
206
+ */
207
+ #onDropped = [];
208
+ /** Register `fn` for the drop notification (fires now if already dropped). */
209
+ whenDropped(fn) {
210
+ if (this.#onDropped === null) {
211
+ fn();
212
+ return;
213
+ }
214
+ this.#onDropped.push(fn);
215
+ }
216
+ /** @internal — fire the drop observers (idempotent; never throws). */
217
+ notifyDropped() {
218
+ const fns = this.#onDropped;
219
+ if (fns === null)
220
+ return;
221
+ this.#onDropped = null;
222
+ for (const fn of fns) {
223
+ try {
224
+ fn();
225
+ }
226
+ catch {
227
+ // An observer bug must not derail the drop path; the observer's
228
+ // own machinery is responsible for surfacing its failures.
229
+ }
230
+ }
231
+ }
232
+ constructor(t) {
233
+ this.t = t;
234
+ }
235
+ resetPending() {
236
+ this.setPending(null, null, null, null);
237
+ }
238
+ setPending(inst, buffer, onCopy, onCopyDone) {
239
+ this.pendingInst = inst;
240
+ this.pendingBuffer = buffer;
241
+ this.pendingOnCopy = onCopy;
242
+ this.pendingOnCopyDone = onCopyDone;
243
+ }
244
+ resetAndNotifyPending(result) {
245
+ const done = this.pendingOnCopyDone;
246
+ assert_(done !== null, "reset_and_notify_pending with nothing pending");
247
+ this.resetPending();
248
+ done(result);
249
+ }
250
+ cancel() {
251
+ this.resetAndNotifyPending(CopyResult.CANCELLED);
252
+ }
253
+ drop() {
254
+ if (!this.dropped) {
255
+ this.dropped = true;
256
+ if (this.pendingBuffer)
257
+ this.resetAndNotifyPending(CopyResult.DROPPED);
258
+ this.notifyDropped();
259
+ }
260
+ }
261
+ /** definitions.py `SharedStreamImpl.read` (line 1032). */
262
+ read(inst, dstBuffer, onCopy, onCopyDone) {
263
+ if (this.dropped) {
264
+ onCopyDone(CopyResult.DROPPED);
265
+ }
266
+ else if (!this.pendingBuffer) {
267
+ this.setPending(inst, dstBuffer, onCopy, onCopyDone);
268
+ }
269
+ else {
270
+ this.#assertSameElemType(dstBuffer);
271
+ this.#trapOnSameInstance(inst);
272
+ if (this.pendingBuffer.remain() > 0) {
273
+ if (dstBuffer.remain() > 0) {
274
+ const n = Math.min(dstBuffer.remain(), this.pendingBuffer.remain());
275
+ dstBuffer.write(this.pendingBuffer.read(n));
276
+ this.pendingOnCopy(() => this.resetPending());
277
+ }
278
+ onCopyDone(CopyResult.COMPLETED);
279
+ }
280
+ else {
281
+ // The parked writer had nothing left: retire it and park the reader.
282
+ this.resetAndNotifyPending(CopyResult.COMPLETED);
283
+ this.setPending(inst, dstBuffer, onCopy, onCopyDone);
284
+ }
285
+ }
286
+ }
287
+ /** definitions.py `SharedStreamImpl.write` (line 1050). */
288
+ write(inst, srcBuffer, onCopy, onCopyDone) {
289
+ if (this.dropped) {
290
+ onCopyDone(CopyResult.DROPPED);
291
+ }
292
+ else if (!this.pendingBuffer) {
293
+ this.setPending(inst, srcBuffer, onCopy, onCopyDone);
294
+ }
295
+ else {
296
+ this.#assertSameElemType(srcBuffer);
297
+ this.#trapOnSameInstance(inst);
298
+ if (this.pendingBuffer.remain() > 0) {
299
+ if (srcBuffer.remain() > 0) {
300
+ const n = Math.min(srcBuffer.remain(), this.pendingBuffer.remain());
301
+ this.pendingBuffer.write(srcBuffer.read(n));
302
+ this.pendingOnCopy(() => this.resetPending());
303
+ }
304
+ onCopyDone(CopyResult.COMPLETED);
305
+ }
306
+ else if (srcBuffer.isZeroLength() && this.pendingBuffer.isZeroLength()) {
307
+ // Zero-length rendezvous: both sides are empty, which is a *completed*
308
+ // handshake rather than a parked write (definitions.py line 1064 —
309
+ // the case `test/async/zero-length.wast` exists to pin).
310
+ onCopyDone(CopyResult.COMPLETED);
311
+ }
312
+ else {
313
+ this.resetAndNotifyPending(CopyResult.COMPLETED);
314
+ this.setPending(inst, srcBuffer, onCopy, onCopyDone);
315
+ }
316
+ }
317
+ }
318
+ #assertSameElemType(b) {
319
+ // Structural, not identity: definitions.py compares dataclass types with
320
+ // `==`, and our `ValType`s are fresh objects per table (the plan's type
321
+ // table is converted per instantiation), so identity would reject every
322
+ // legitimate cross-instance stream.
323
+ assert_(sameElemType(this.t, b.t) && sameElemType(b.t, this.pendingBuffer.t), "stream element type mismatch between ends");
324
+ }
325
+ /**
326
+ * definitions.py marks this `# temporary`: a same-instance copy of a
327
+ * non-number element type would need the source and destination lifts to
328
+ * interleave, which the reference has not specified yet.
329
+ */
330
+ #trapOnSameInstance(inst) {
331
+ trapIf(inst === this.pendingInst && !noneOrNumberType(this.t), "cannot read from and write to intra-component stream");
332
+ }
333
+ }
334
+ /** definitions.py `class SharedFutureImpl` (line 1119). Exactly one element. */
335
+ export class SharedFutureImpl {
336
+ t;
337
+ /**
338
+ * Optional hook fired when this shared object is lowered into a component
339
+ * instance (`lower_stream`/`lower_future`). Host-owned ends use it to learn
340
+ * which `Store` is driving the guest they were just handed to; guest-owned
341
+ * streams leave it unset. Keeps `cabi` free of any host-stream knowledge.
342
+ */
343
+ onLowered = null;
344
+ /**
345
+ * Optional hook fired by `liftAsyncValue` whenever this object is lifted
346
+ * OUT of a guest table. The receiver — the host, or the destination of a
347
+ * guest-to-guest hop, in which case the immediately following lower fires
348
+ * `onLowered` — may now act on the transferred end. Host wrappers use it to
349
+ * re-arm their activity (#162, embedder-api amendment A15). Guest-owned
350
+ * objects leave it unset.
351
+ */
352
+ onLifted = null;
353
+ /**
354
+ * The `Store` driving the component this object has been handed to, set the
355
+ * first time it is lifted or lowered. Host ends need it to pump the guest
356
+ * between export calls (see exec/host_streams.ts `HostActivity.pump`); a
357
+ * purely guest-to-guest stream never reads it.
358
+ */
359
+ boundStore = null;
360
+ dropped = false;
361
+ /**
362
+ * Set when the future's **writable** side went away without ever delivering
363
+ * its one value (#84 teardown of a trap-poisoned instance, #90 host
364
+ * `drop()` on a lowered-but-unwritten future).
365
+ *
366
+ * definitions.py keeps this state unreachable: `WritableFutureEnd.drop`
367
+ * traps unless the end is DONE (definitions.py:1183-1184), so a readable
368
+ * future end can never observe DROPPED (`future_copy`'s `on_copy_done`
369
+ * assertion, definitions.py:2607). Our two teardown paths deliberately
370
+ * bypass that trap — a poisoned instance cannot be asked to trap again, and
371
+ * the host `drop()` is a public API door — so the state exists here and has
372
+ * to be *total*: an unwritten future whose writer died can never satisfy
373
+ * its reader, so the reader is told at its rendezvous point, with a
374
+ * **trap**, never a DROPPED/COMPLETED answer and never a silent hang.
375
+ *
376
+ * Consumers of the flag:
377
+ * * `read` below, for a reader that has not parked yet (trap on the spot);
378
+ * * intrinsics/stream_builtins.ts `futureCopy`, for a parked guest reader
379
+ * (the pending event's thunk throws instead of producing a tuple);
380
+ * * exec/host_streams.ts leaves host readers on their existing DROPPED
381
+ * path — the conventions layer already brands that outcome.
382
+ */
383
+ abandonReason = null;
384
+ pendingInst = null;
385
+ pendingBuffer = null;
386
+ pendingOnCopyDone = null;
387
+ /**
388
+ * Observers fired once, when this future becomes dropped — by EITHER side,
389
+ * including the A7 teardown walk (`dropSharedForTeardown`). Streams grew
390
+ * this for A13 producer cancellation; futures need it as the release hook
391
+ * for a host wrapper's activity arm (#162, amendment A15): a guest dropping
392
+ * its end with no host operation parked, and the `readResult()`
393
+ * already-dropped fast path, both bypass every other close site. `null` =
394
+ * already fired.
395
+ */
396
+ #onDropped = [];
397
+ /** Register `fn` for the drop notification (fires now if already dropped). */
398
+ whenDropped(fn) {
399
+ if (this.#onDropped === null) {
400
+ fn();
401
+ return;
402
+ }
403
+ this.#onDropped.push(fn);
404
+ }
405
+ /** @internal — fire the drop observers (idempotent; never throws). */
406
+ notifyDropped() {
407
+ const fns = this.#onDropped;
408
+ if (fns === null)
409
+ return;
410
+ this.#onDropped = null;
411
+ for (const fn of fns) {
412
+ try {
413
+ fn();
414
+ }
415
+ catch {
416
+ // An observer bug must not derail the drop path; the observer's
417
+ // own machinery is responsible for surfacing its failures.
418
+ }
419
+ }
420
+ }
421
+ constructor(t) {
422
+ this.t = t;
423
+ }
424
+ resetPending() {
425
+ this.setPending(null, null, null);
426
+ }
427
+ setPending(inst, buffer, onCopyDone) {
428
+ this.pendingInst = inst;
429
+ this.pendingBuffer = buffer;
430
+ this.pendingOnCopyDone = onCopyDone;
431
+ }
432
+ resetAndNotifyPending(result) {
433
+ const done = this.pendingOnCopyDone;
434
+ assert_(done !== null, "reset_and_notify_pending with nothing pending");
435
+ this.resetPending();
436
+ done(result);
437
+ }
438
+ cancel() {
439
+ this.resetAndNotifyPending(CopyResult.CANCELLED);
440
+ }
441
+ drop() {
442
+ if (!this.dropped) {
443
+ this.dropped = true;
444
+ if (this.pendingBuffer)
445
+ this.resetAndNotifyPending(CopyResult.DROPPED);
446
+ this.notifyDropped();
447
+ }
448
+ }
449
+ read(inst, dstBuffer, onCopyDone) {
450
+ // #84 leg (c): the reader arrives AFTER the writable side was abandoned.
451
+ // definitions.py:1154 (SharedFutureImpl.read) asserts `not self.dropped`
452
+ // here because the drop
453
+ // trap keeps that unreachable; for our abandoned state the honest answer
454
+ // is the same trap the parked reader gets, delivered synchronously.
455
+ if (this.dropped && this.abandonReason !== null) {
456
+ throw futureAbandonTrap(this.abandonReason);
457
+ }
458
+ assert_(!this.dropped && dstBuffer.remain() === 1, "future read shape");
459
+ if (!this.pendingBuffer) {
460
+ this.setPending(inst, dstBuffer, onCopyDone);
461
+ }
462
+ else {
463
+ trapIf(inst === this.pendingInst && !noneOrNumberType(this.t), "cannot read from and write to intra-component future");
464
+ dstBuffer.write(this.pendingBuffer.read(1));
465
+ this.resetAndNotifyPending(CopyResult.COMPLETED);
466
+ onCopyDone(CopyResult.COMPLETED);
467
+ }
468
+ }
469
+ write(inst, srcBuffer, onCopyDone) {
470
+ assert_(srcBuffer.remain() === 1, "future write shape");
471
+ if (this.dropped) {
472
+ onCopyDone(CopyResult.DROPPED);
473
+ }
474
+ else if (!this.pendingBuffer) {
475
+ this.setPending(inst, srcBuffer, onCopyDone);
476
+ }
477
+ else {
478
+ trapIf(inst === this.pendingInst && !noneOrNumberType(this.t), "cannot read from and write to intra-component future");
479
+ this.pendingBuffer.write(srcBuffer.read(1));
480
+ this.resetAndNotifyPending(CopyResult.COMPLETED);
481
+ onCopyDone(CopyResult.COMPLETED);
482
+ }
483
+ }
484
+ }
485
+ // ---------------------------------------------------------------------------
486
+ // Copy ends (definitions.py `class CopyEnd`, line 1081)
487
+ // ---------------------------------------------------------------------------
488
+ /**
489
+ * One guest-visible end of a stream or future. It **is** a `Waitable`, so it
490
+ * joins waitable sets and delivers events through the machinery the subtask
491
+ * path already uses.
492
+ */
493
+ export class CopyEnd extends Waitable {
494
+ shared;
495
+ state = CopyState.IDLE;
496
+ constructor(shared) {
497
+ super();
498
+ this.shared = shared;
499
+ }
500
+ copying() {
501
+ return this.state === CopyState.COPYING ||
502
+ this.state === CopyState.CANCELLING_COPY;
503
+ }
504
+ drop() {
505
+ trapIf(this.copying(), this.side === "readable"
506
+ ? `cannot remove busy ${this.kind}`
507
+ : `cannot drop busy ${this.kind}`);
508
+ this.shared.drop();
509
+ super.drop();
510
+ }
511
+ }
512
+ export class ReadableStreamEnd extends CopyEnd {
513
+ kind = "stream";
514
+ side = "readable";
515
+ copy(inst, dst, onCopy, onCopyDone) {
516
+ this.shared.read(inst, dst, onCopy, onCopyDone);
517
+ }
518
+ }
519
+ export class WritableStreamEnd extends CopyEnd {
520
+ kind = "stream";
521
+ side = "writable";
522
+ copy(inst, src, onCopy, onCopyDone) {
523
+ this.shared.write(inst, src, onCopy, onCopyDone);
524
+ }
525
+ }
526
+ export class ReadableFutureEnd extends CopyEnd {
527
+ kind = "future";
528
+ side = "readable";
529
+ copy(inst, dst, onCopyDone) {
530
+ this.shared.read(inst, dst, onCopyDone);
531
+ }
532
+ }
533
+ export class WritableFutureEnd extends CopyEnd {
534
+ kind = "future";
535
+ side = "writable";
536
+ copy(inst, src, onCopyDone) {
537
+ this.shared.write(inst, src, onCopyDone);
538
+ }
539
+ /**
540
+ * definitions.py `WritableFutureEnd.drop` (line 1183): a future's writable
541
+ * end may only be dropped once it has actually delivered its one value —
542
+ * `test/async/futures-must-write.wast` is the case this exists for.
543
+ */
544
+ drop() {
545
+ trapIf(this.state !== CopyState.DONE, "cannot drop future write end without first writing a value");
546
+ super.drop();
547
+ }
548
+ }
549
+ // ---------------------------------------------------------------------------
550
+ // Poisoned-instance retirement (#66)
551
+ // ---------------------------------------------------------------------------
552
+ /**
553
+ * Failures recorded against shared stream/future objects whose peer end died
554
+ * inside a trap-poisoned instance's handle table. The embedder layer consults
555
+ * this to reject host operations loudly (contracts/embedder-api.md amendment
556
+ * A7) instead of letting them hang forever or fake a clean end-of-stream.
557
+ */
558
+ const poisonFailures = new WeakMap();
559
+ /** The recorded poisoning failure for a shared stream/future value, if any. */
560
+ export function poisonFailureOf(shared) {
561
+ return typeof shared === "object" && shared !== null
562
+ ? poisonFailures.get(shared)
563
+ : undefined;
564
+ }
565
+ /** Instances whose async ends have already been retired (idempotence). */
566
+ const retiredInstances = new WeakSet();
567
+ // ---------------------------------------------------------------------------
568
+ // Abandoned futures (#84, #90)
569
+ // ---------------------------------------------------------------------------
570
+ /**
571
+ * The trap a reader of an abandoned future observes at its rendezvous point.
572
+ *
573
+ * `Trap` is the guest-visible fault vocabulary (cabi/trap.ts); the recorded
574
+ * reason rides as `cause` so the embedder/host layers can still attribute the
575
+ * original fault. (`Trap`'s constructor takes only a message, so `cause` is
576
+ * attached after construction rather than through `ErrorOptions`.)
577
+ */
578
+ export function futureAbandonTrap(reason) {
579
+ const t = new Trap(`future.read can never complete: ${reason.message}`);
580
+ t.cause = reason;
581
+ return t;
582
+ }
583
+ /** The abandonment reason of a shared future, if it has one (#84/#90). */
584
+ export function abandonReasonOf(shared) {
585
+ return shared instanceof SharedFutureImpl ? shared.abandonReason : null;
586
+ }
587
+ /**
588
+ * Mark a future's writable side as gone-without-a-value and settle the
589
+ * rendezvous (#90's host `drop()` door; the poisoning walk below routes
590
+ * through `dropSharedForTeardown` instead, which adds the dead-guest
591
+ * discipline).
592
+ *
593
+ * Never throws, and idempotent: a second call on an already-dropped future is
594
+ * a no-op, so `drop()`/`Symbol.dispose` at the layers above are total.
595
+ */
596
+ export function abandonSharedFuture(shared, reason) {
597
+ if (shared.dropped)
598
+ return;
599
+ shared.abandonReason ??= reason;
600
+ dropSharedForTeardown(shared);
601
+ }
602
+ /**
603
+ * Drop a shared stream/future as *teardown*, without waking a doomed guest.
604
+ *
605
+ * Same outcome as `drop()` for host ends and healthy guest peers (a DROPPED
606
+ * notification), with one difference: a parked side belonging to a
607
+ * **poisoned** guest instance is retired silently via `resetPending`.
608
+ * Notifying it would queue a phantom event into the corpse's waitables, and
609
+ * a later driving loop servicing it would resume machinery whose instance
610
+ * can no longer be entered (`tick` asserts enterability). Host sentinels are
611
+ * not instances at all, so they are always notified.
612
+ *
613
+ * #100: THE HEALTH TEST IS "POISONED", NOT "`mayEnter === false`". The
614
+ * original test used non-enterability as a proxy for deadness. The proxy is
615
+ * unsound in one direction, and the unsoundness stranded healthy tasks:
616
+ *
617
+ * * (sound half, #84 audit) a healthy guest peer always parks with
618
+ * `mayEnter === true`. Every park — the callback ABI's waitable-set wait,
619
+ * and equally a sync-lowered/JSPI peer blocked inside `finishCopy`'s
620
+ * SITE 4 via `blockCurrentActivation` — yields the thread out of the
621
+ * scheduler's enter/leave bracket, and the bracket's `leaveTo` runs on the
622
+ * way out (task/scheduler.ts `Store.tick` :905-917, task/thread.ts
623
+ * `Thread.resumeWith` :157-179, whose resume-side
624
+ * `assert_(mayEnterFrom(null))` would fire otherwise). Blocking inside a
625
+ * wasm frame does NOT hold the enter bracket.
626
+ * * (unsound converse) `mayEnter === false` does not imply "poisoned". An
627
+ * instance that is merely mid-call is also non-enterable, and a CALLER
628
+ * instance stays non-enterable for the whole duration of a
629
+ * cross-component (FACT) call into an instance that traps
630
+ * (`ComponentInstanceState.enterFrom` clears `mayEnter` on the callee's
631
+ * entering set only, task/mod.ts). A *different*, healthy task of that
632
+ * caller, parked on an end of a stream/future the trapping callee also
633
+ * held, was classified dead here and retired silently — stranded, the
634
+ * exact outcome #66 exists to prevent.
635
+ *
636
+ * So the test consults the poison marker itself. It is per-instance and
637
+ * recorded at the single seam every bracket-break site routes through
638
+ * (`notifyInstancePoisoned`, task/scheduler.ts: exec/boundary.ts `poison`,
639
+ * `Store.tick`, `Thread.resumeWith`, the FACT cross-component catches in
640
+ * intrinsics/fact_calls.ts, and cabi/handles.ts's gated destructor call),
641
+ * and it is recorded *before* the retirement walk runs, so an instance's own
642
+ * parked ends still see it during its own walk. `retiredInstances` is
643
+ * consulted alongside it because the walk is also reachable directly (it is
644
+ * set at walk entry, so the two agree); neither ever contains the synthetic
645
+ * per-instantiation root, which every poison site skips or releases (plan v3
646
+ * amendment 4, `releaseSyntheticRootOnPoison`).
647
+ *
648
+ * Why this does not re-open review B2 (phantom events into a corpse): the
649
+ * concern is that a DROPPED event queued onto a waitable of an instance that
650
+ * can never be entered again would be serviced by a later driving loop and
651
+ * resume machinery whose `tick` asserts enterability. "Can never be entered
652
+ * again" is precisely poisoning — a mid-call instance's `mayEnter` is
653
+ * restored by its own `leaveTo` when the call returns, and its parked task
654
+ * then resumes normally and consumes the event. The narrowed predicate
655
+ * therefore excludes exactly the population B2 is about, and admits only
656
+ * peers that will run again.
657
+ *
658
+ * Used by the poisoning walk below and by the trapping-import abandonment
659
+ * path (embedder/instantiate.ts `releaseAsyncArgs`). Idempotent.
660
+ */
661
+ export function dropSharedForTeardown(shared) {
662
+ if (shared.dropped)
663
+ return;
664
+ shared.dropped = true;
665
+ if (shared.pendingBuffer) {
666
+ const pi = shared.pendingInst;
667
+ const parkedInDeadGuest = typeof pi === "object" && pi !== null &&
668
+ (isInstancePoisoned(pi) || retiredInstances.has(pi));
669
+ if (parkedInDeadGuest)
670
+ shared.resetPending();
671
+ else
672
+ shared.resetAndNotifyPending(CopyResult.DROPPED);
673
+ }
674
+ // The drop observers also fire on the teardown path: a stream producer
675
+ // parked behind a trap-poisoned reader must be cancelled the same as behind
676
+ // a cleanly-dropped one (A13), and a host wrapper's activity arm must be
677
+ // released the same way (#162, amendment A15). Both classes carry the
678
+ // observer machinery, so this is unconditional.
679
+ shared.notifyDropped();
680
+ }
681
+ /**
682
+ * Retire every live stream/future end in a trap-poisoned instance's handle
683
+ * table (#66).
684
+ *
685
+ * Rationale: after a trap breaks the enter/leave bracket, `mayEnter` stays
686
+ * false forever, so no task of this instance can ever rendezvous again. Its
687
+ * table's `CopyEnd`s are therefore unreachable-forever — leaving their shared
688
+ * objects live strands the peers: a parked HOST operation never settles (its
689
+ * promise hangs), and a LATER host operation would "succeed" against the
690
+ * corpse (a copy into memory nothing will ever read — silent data loss).
691
+ * Dropping the shared object now converts both into the spec-shaped DROPPED
692
+ * outcome, and the recorded failure lets the embedder layer brand it.
693
+ *
694
+ * Called from every bracket-break site — exec/boundary.ts `poison()` (the
695
+ * sync-lift path), scheduler.ts `Store.tick` and thread.ts
696
+ * `Thread.resumeWith` (traps during a resumed thread), and the FACT
697
+ * cross-component catches (intrinsics/fact_calls.ts, callee side) — with the
698
+ * trap as `cause`. Idempotent per instance. The parked-side notification
699
+ * discipline lives in `dropSharedForTeardown` above.
700
+ *
701
+ * Two refinements over the original #66 walk, both from #84:
702
+ *
703
+ * 1. FUTURES ARE NOT STREAMS. A `stream`'s reader may legitimately observe
704
+ * DROPPED (that is end-of-stream), but a `future`'s reader may not
705
+ * (definitions.py:2607) — the reference keeps the state unreachable by
706
+ * trapping an early writable-end drop (definitions.py:1183-1184), which
707
+ * a poisoned instance can no longer be made to do. So an unwritten
708
+ * writable future end in this table marks its shared object *abandoned*
709
+ * (first pass below) and its reader traps instead. A writable end that
710
+ * already reached `CopyState.DONE` delivered its value; nothing is owed.
711
+ *
712
+ * 2. ONE END'S FAILURE MUST NOT STRAND THE REST. The notification of a
713
+ * retired end runs arbitrary peer callbacks (host settlers, event
714
+ * thunks); a throw used to abort the loop mid-table, leaving the
715
+ * remaining ends live and their peers hanging — exactly the outcome this
716
+ * walk exists to prevent. The walk now always completes and rethrows the
717
+ * first failure afterwards.
718
+ */
719
+ export function retireInstanceAsyncEnds(inst, cause) {
720
+ if (retiredInstances.has(inst))
721
+ return;
722
+ retiredInstances.add(inst);
723
+ const where = inst.index !== undefined
724
+ ? `component instance ${inst.index}`
725
+ : "a component instance";
726
+ // Snapshot: the notifications below can run peer code that mutates tables.
727
+ const ends = [];
728
+ for (const e of inst.handles)
729
+ if (e instanceof CopyEnd)
730
+ ends.push(e);
731
+ // Pass 1: record the failure, and mark abandoned every future this table
732
+ // owes a value on. Done before ANY notification, so the reader-side trap
733
+ // decision cannot depend on the order the handle table happens to yield
734
+ // the two ends of one future in.
735
+ for (const e of ends) {
736
+ const shared = e.shared;
737
+ if (poisonFailures.get(shared) === undefined) {
738
+ poisonFailures.set(shared, new Error(`${where} trapped while it held an end of this stream/future; ` +
739
+ `the peer can never rendezvous again`, { cause }));
740
+ }
741
+ if (e instanceof WritableFutureEnd && shared instanceof SharedFutureImpl &&
742
+ e.state !== CopyState.DONE && !shared.dropped) {
743
+ shared.abandonReason ??= poisonFailures.get(shared);
744
+ }
745
+ }
746
+ // Pass 2: retire. Collect failures rather than abandoning the walk.
747
+ let first;
748
+ let failed = false;
749
+ for (const e of ends) {
750
+ try {
751
+ dropSharedForTeardown(e.shared);
752
+ }
753
+ catch (err) {
754
+ if (!failed) {
755
+ failed = true;
756
+ first = err;
757
+ }
758
+ }
759
+ }
760
+ if (failed)
761
+ throw first;
762
+ }
763
+ // `Store.tick`'s bracket-break site reaches the walk through this seam (its
764
+ // module cannot import ours — see `setOnInstancePoisoned`); the sync-lift
765
+ // site (exec/boundary.ts `poison`) imports it directly.
766
+ setOnInstancePoisoned(retireInstanceAsyncEnds);
767
+ // ---------------------------------------------------------------------------
768
+ // error-context (definitions.py `class ErrorContext`, line 2775)
769
+ // ---------------------------------------------------------------------------
770
+ /**
771
+ * definitions.py models the debug message as a `String` triple; we keep the
772
+ * decoded JS string plus its encoding, which is all `store_string` needs.
773
+ */
774
+ export class ErrorContext {
775
+ debugMessage;
776
+ constructor(debugMessage) {
777
+ this.debugMessage = debugMessage;
778
+ }
779
+ }
780
+ // A9 brand (contracts/embedder-api.md §"Module identity"): error-contexts are
781
+ // STATEFUL — they live in a component instance's handle table — so the brand
782
+ // exists to make a foreign one diagnosable at the lowering sites, never
783
+ // usable. Both this internal class and the embedder-facing wrapper
784
+ // (embedder/streams.ts) carry it, because either shape can be handed back to
785
+ // a lowering site by embedder code.
786
+ defineBrand(ErrorContext.prototype, ERROR_CONTEXT);