@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,770 @@
1
+ // Stream / future conventions (contracts/embedder-api.md §"Streams and
2
+ // futures"; C2 checklist item 4).
3
+ //
4
+ // The low-level seam is `exec/host_streams.ts` — `HostStream`/`HostFuture`
5
+ // over the shared rendezvous object. This file is the *handle* layer named by
6
+ // the contract: `SharedStreamImpl` identity stays internal, embedders see
7
+ // `Stream<T>` / `Future<T>` / `ErrorContext`, and lowering accepts the natural
8
+ // JS producers (`ReadableStream`, `AsyncIterable`, arrays, `Promise`) with the
9
+ // layer owning the pumping.
10
+ import { despecialize } from "../cabi/types.js";
11
+ import { hostFuture, hostFutureFor, hostStream, hostStreamFor, } from "../exec/host_streams.js";
12
+ import { CopyResult, dropSharedForTeardown, poisonFailureOf, } from "../task/mod.js";
13
+ import { defineBrand, ERROR_CONTEXT, FUTURE, hasBrand, isStreamProducerError, STREAM, StreamProducerError, } from "@polyengine/protocol";
14
+ import { describeCrossCopy } from "./copy.js";
15
+ import { DroppedError, PeerTrappedError } from "./errors.js";
16
+ // `StreamProducerError`'s canonical definition moved to `@polyengine/protocol`
17
+ // with amendment A9 (it is an embedder-contract value: recognition must
18
+ // survive multiple runtime copies, issue #83). Re-exported here so every
19
+ // existing import path is unchanged.
20
+ export { StreamProducerError } from "@polyengine/protocol";
21
+ /**
22
+ * Failures recorded against a shared stream object whose driving store could
23
+ * not be reached (the stream was never lowered, or the store already carries a
24
+ * failure). Surfaced on the next interaction with the handle.
25
+ */
26
+ const producerFailures = new WeakMap();
27
+ /**
28
+ * Report a producer failure on the channel that can actually attribute it.
29
+ *
30
+ * PRIMARY channel: `store.hostFailure`. This is the runtime's existing
31
+ * host-side failure slot — `driveAsync` checks it after every tick and throws
32
+ * it out of the driving loop (exec/boundary.ts:468/647/679/692), which is the
33
+ * driving loop of *the export call that is consuming this stream*. So the call
34
+ * that would otherwise have resolved with truncated data rejects with this
35
+ * error instead. It is the same channel `HostActivity.#pumpAsync` already uses
36
+ * for a trap raised while pumping between export calls
37
+ * (exec/host_streams.ts:284), so the two host-side stream failure paths agree.
38
+ *
39
+ * The report happens BEFORE the write end is dropped: the drop is what lets
40
+ * the guest observe end-of-stream and resolve, and `driveAsync` checks
41
+ * `hostFailure` before it checks `done()`.
42
+ *
43
+ * FALLBACK: no store bound (the stream was never lowered), or the store
44
+ * already carries an earlier failure. Then the cause is recorded against the
45
+ * shared object and raised on the next interaction with the handle.
46
+ */
47
+ function reportProducerFailure(host, where, cause) {
48
+ // Brand, not class (A9): a producer failure raised by another runtime copy
49
+ // must not be re-wrapped into a second layer of the same error.
50
+ const err = isStreamProducerError(cause)
51
+ ? cause
52
+ : new StreamProducerError(where, cause);
53
+ const shared = host.value;
54
+ producerFailures.set(host.value, err);
55
+ const store = shared.boundStore;
56
+ if (store != null && typeof store === "object") {
57
+ if (store.hostFailure === undefined)
58
+ store.hostFailure = err;
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ /** @internal — raise a recorded producer failure, if any. */
64
+ function throwIfFailed(value, where = "stream") {
65
+ const e = producerFailures.get(value);
66
+ if (e !== undefined)
67
+ throw e;
68
+ throwIfPeerTrapped(value, where);
69
+ }
70
+ /**
71
+ * @internal — raise the recorded poisoning failure, if any (#66, amendment
72
+ * A7). Pre-op: an operation started after the peer's instance trapped must
73
+ * reject rather than park forever. Post-await (with the op's outcome in
74
+ * hand): an operation the retirement walk settled DROPPED-shaped must reject
75
+ * rather than fake a clean end — but an op that genuinely COMPLETED before
76
+ * the trap keeps its result (the fault still surfaces on the export call,
77
+ * and on this handle's next operation).
78
+ */
79
+ function throwIfPeerTrapped(value, where, progress) {
80
+ const p = poisonFailureOf(value);
81
+ if (p !== undefined)
82
+ throw new PeerTrappedError(where, p, progress);
83
+ }
84
+ /** True for `stream<u8>` / `future<u8>`, whose chunks are `Uint8Array`. */
85
+ export function isU8Element(element) {
86
+ return element !== null && despecialize(element).kind === "u8";
87
+ }
88
+ /**
89
+ * A stream handle.
90
+ *
91
+ * `read` returning an empty chunk is end-of-stream, exactly as the contract
92
+ * spells it; `readable()` and the async iterator are built on it.
93
+ */
94
+ export class Stream {
95
+ #host;
96
+ #codec;
97
+ /** Set once the handle's shared object has been handed to a guest. */
98
+ #consumed = false;
99
+ #dropped = false;
100
+ /** Waiters parked in `Stream.create()` until an element type is known. */
101
+ #binders = [];
102
+ constructor(host, codec) {
103
+ this.#host = host;
104
+ this.#codec = codec;
105
+ }
106
+ /** Wrap a stream value that was lifted out of a guest. */
107
+ static fromLifted(value, codec) {
108
+ return new Stream(hostStreamFor(value), codec);
109
+ }
110
+ /** Wrap a freshly created host-owned stream of a known element type. */
111
+ static fromHostStream(host, codec) {
112
+ return new Stream(host, codec);
113
+ }
114
+ /**
115
+ * `Stream.create<T>(): { stream, writer }` — the writer-side host end the
116
+ * contract names.
117
+ *
118
+ * The element type is deliberately NOT a parameter: the embedder does not
119
+ * have one (a `ValType` is a runtime-internal shape) and the *lowering site*
120
+ * always does. So the shared object is created lazily, at the moment the
121
+ * stream is passed to a guest, and writer operations issued before that park
122
+ * until then. A stream created and written but never passed anywhere simply
123
+ * never completes — the same honest hang the low-level layer documents.
124
+ */
125
+ static create() {
126
+ const stream = new Stream(null, null);
127
+ return { stream, writer: new StreamWriter(stream) };
128
+ }
129
+ /** @internal — bind a lazily created stream to the lowering site's type. */
130
+ bindElement(codec) {
131
+ if (this.#host !== null)
132
+ return;
133
+ this.#codec = codec;
134
+ this.#host = hostStream(codec.element);
135
+ publishHostStream(this, this.#host);
136
+ const waiters = this.#binders;
137
+ this.#binders = [];
138
+ for (const w of waiters)
139
+ w();
140
+ }
141
+ /** @internal — resolve once this handle has a shared object. */
142
+ whenBound() {
143
+ if (this.#host !== null)
144
+ return Promise.resolve();
145
+ return new Promise((r) => this.#binders.push(r));
146
+ }
147
+ /** @internal */
148
+ get bound() {
149
+ return this.#host !== null;
150
+ }
151
+ /** @internal — the shared value to hand to a lowering site. */
152
+ takeValue(codec) {
153
+ this.bindElement(codec);
154
+ if (this.#consumed) {
155
+ throw new TypeError("this Stream handle has already been passed to a guest; a stream " +
156
+ "value may only be transferred once");
157
+ }
158
+ this.#consumed = true;
159
+ return this.#host.value;
160
+ }
161
+ /** @internal */
162
+ get codec() {
163
+ return this.#codec;
164
+ }
165
+ #require() {
166
+ if (this.#host === null) {
167
+ throw new TypeError("this Stream was created with Stream.create() and has not been " +
168
+ "passed to a guest yet, so it has no element type; pass it first, " +
169
+ "or use the writer, which parks until then");
170
+ }
171
+ // Post-transfer refusal (#162, embedder-api amendment A15). Lifting
172
+ // removes the handle from the source table and lowering installs it in
173
+ // the destination's (definitions.py `lift_async_value` line 1530,
174
+ // `lower_stream` line 1828): once this handle's shared object has been
175
+ // passed to a guest, the guest owns the readable end and a host read here
176
+ // would operate a phantom duplicate of it. Refuse loudly instead.
177
+ // `StreamWriter` is deliberately unaffected — the host retains the
178
+ // writable end, and writing after the pass is the normal A5 pattern —
179
+ // and `drop()`/`cancelRead()` stay permissive.
180
+ if (this.#consumed) {
181
+ throw new TypeError("this Stream handle has already been passed to a guest; the guest " +
182
+ "owns its readable end, so it can no longer be read from the host " +
183
+ "(issue #162)");
184
+ }
185
+ return this.#host;
186
+ }
187
+ /** Low-level read: up to `max` elements; an empty chunk means end-of-stream. */
188
+ async read(max) {
189
+ const host = this.#require();
190
+ const where = this.#codec?.where ?? "stream read";
191
+ throwIfFailed(host.value, where);
192
+ const raw = await host.readable.read(max);
193
+ // An empty chunk normally means clean end-of-stream; when the peer's
194
+ // instance trapped it means the retirement walk settled us — reject
195
+ // instead of faking EOS (amendment A7). A non-empty chunk was really
196
+ // copied before the trap and is delivered; the next read rejects.
197
+ if (raw.length === 0)
198
+ throwIfPeerTrapped(host.value, where);
199
+ return this.#chunk(raw);
200
+ }
201
+ #chunk(raw) {
202
+ const codec = this.#codec;
203
+ if (isU8Element(codec.element)) {
204
+ // The exec layer already resolves u8 reads as a Uint8Array (the
205
+ // rendezvous copy itself — issue #54); pass it through untouched so a
206
+ // host read costs exactly that one copy. Uint8Array.from covers
207
+ // raw-layer writers that fed plain arrays.
208
+ return (raw instanceof Uint8Array
209
+ ? raw
210
+ : Uint8Array.from(raw));
211
+ }
212
+ const vs = raw instanceof Uint8Array ? Array.from(raw) : raw;
213
+ return vs.map((v) => codec.toHost(v));
214
+ }
215
+ /**
216
+ * Cancel an in-flight `read` (R-fix review advisory 1).
217
+ *
218
+ * #97, DELIBERATE AND PINNED: the cancelled `read` resolves with whatever
219
+ * had already arrived — typically the empty chunk, which this layer also
220
+ * uses as end-of-stream (`read`'s contract, and hence `readable()` and the
221
+ * async iterator, which close on it). **A cancelled read is therefore
222
+ * indistinguishable from EOS at this layer.** Kept as-is rather than given
223
+ * a distinct signal: the caller of `cancelRead()` is the same code that
224
+ * observes the read's result, so it already knows which happened, and only
225
+ * that caller can reach the state. See exec/host_streams.ts
226
+ * `HostReadableEnd.cancelRead` for the mechanism.
227
+ */
228
+ cancelRead() {
229
+ this.#host?.readable.cancelRead();
230
+ }
231
+ drop() {
232
+ if (this.#dropped)
233
+ return;
234
+ this.#dropped = true;
235
+ // Both ends of a host wrapper name the same shared object; dropping once
236
+ // is enough (`SharedStreamImpl.drop` is idempotent).
237
+ this.#host?.readable.drop();
238
+ }
239
+ /**
240
+ * @internal — teardown after a trapping import abandoned this handle
241
+ * (#66, instantiate.ts `releaseAsyncArgs`). Unlike `drop()`, this goes
242
+ * through `dropSharedForTeardown`, whose parked-side discipline never
243
+ * wakes the about-to-be-poisoned caller (review B2: a plain drop queued a
244
+ * DROPPED event into the trapping instance's waitables, and a later
245
+ * driving loop asserted on the corpse).
246
+ *
247
+ * The arm is released on this path too (#162, amendment A15): the wrapper's
248
+ * `HostActivity` now closes through the shared object's drop observers,
249
+ * which `dropSharedForTeardown` fires unconditionally — so a teardown with
250
+ * nothing parked no longer leaves the arm outliving the stream. (This
251
+ * paragraph previously recorded that asymmetry as a known, non-blocking
252
+ * review advisory.)
253
+ */
254
+ dropForTeardown() {
255
+ if (this.#dropped)
256
+ return;
257
+ this.#dropped = true;
258
+ if (this.#host !== null) {
259
+ dropSharedForTeardown(this.#host.value);
260
+ }
261
+ }
262
+ [Symbol.dispose]() {
263
+ this.drop();
264
+ }
265
+ /** Web-native view: `ReadableStream<Chunk<T>>`. */
266
+ readable() {
267
+ const self = this;
268
+ return new ReadableStream({
269
+ async pull(controller) {
270
+ const chunk = await self.read(READ_CHUNK);
271
+ if (chunk.length === 0) {
272
+ controller.close();
273
+ return;
274
+ }
275
+ controller.enqueue(chunk);
276
+ },
277
+ cancel() {
278
+ self.drop();
279
+ },
280
+ });
281
+ }
282
+ async *[Symbol.asyncIterator]() {
283
+ for (;;) {
284
+ const chunk = await this.read(READ_CHUNK);
285
+ if (chunk.length === 0)
286
+ return;
287
+ yield chunk;
288
+ }
289
+ }
290
+ }
291
+ /** How many elements a convenience read asks for at a time. */
292
+ const READ_CHUNK = 4096;
293
+ /** Writer half of `Stream.create()`. */
294
+ export class StreamWriter {
295
+ #stream;
296
+ constructor(stream) {
297
+ this.#stream = stream;
298
+ }
299
+ /**
300
+ * Offer values; resolves with how many the reader took.
301
+ *
302
+ * `Chunk<T>` mirrors the read side: a u8 stream accepts a `Uint8Array`
303
+ * (taken as already-lowered bytes), and a plain array of any element type
304
+ * is lowered per element. u8 chunks travel as `Uint8Array` all the way to
305
+ * the CABI store's bulk path (issue #54) — which makes a `Uint8Array`
306
+ * chunk a BORROW until the returned promise settles; mutating it in that
307
+ * window is misuse. Plain-array chunks are lowered (copied) up front.
308
+ */
309
+ async write(values) {
310
+ await this.#stream.whenBound();
311
+ const host = hostOf(this.#stream);
312
+ const where = this.#stream.codec?.where ?? "stream write";
313
+ throwIfFailed(host.value, where);
314
+ const n = await host.writable.write(packChunk(values, this.#stream.codec));
315
+ // A short take normally means "re-offer later" / "reader done"; when the
316
+ // reader's instance trapped it means the retirement walk settled us —
317
+ // reject, carrying the delivered count (amendment A7). A full take
318
+ // genuinely completed before the trap and stays a success.
319
+ if (n < values.length)
320
+ throwIfPeerTrapped(host.value, where, n);
321
+ return n;
322
+ }
323
+ /** Offer values until all are taken or the reader goes away. */
324
+ async writeAll(values) {
325
+ await this.#stream.whenBound();
326
+ const host = hostOf(this.#stream);
327
+ const where = this.#stream.codec?.where ?? "stream write";
328
+ throwIfFailed(host.value, where);
329
+ const n = await host.writable.writeAll(packChunk(values, this.#stream.codec));
330
+ if (n < values.length)
331
+ throwIfPeerTrapped(host.value, where, n);
332
+ return n;
333
+ }
334
+ cancelWrite() {
335
+ if (!this.#stream.bound)
336
+ return;
337
+ hostOf(this.#stream).writable.cancelWrite();
338
+ }
339
+ /** End-of-stream. */
340
+ async close() {
341
+ await this.#stream.whenBound();
342
+ hostOf(this.#stream).writable.drop();
343
+ }
344
+ }
345
+ const hostOfStream = new WeakMap();
346
+ function hostOf(s) {
347
+ // The host end lives behind `Stream`'s private field; `takeValue`/`read`
348
+ // are the public routes. The writer needs the writable half, so the handle
349
+ // publishes it here at bind time.
350
+ const h = hostOfStream.get(s);
351
+ if (h === undefined) {
352
+ throw new TypeError("stream writer used before the stream was bound");
353
+ }
354
+ return h;
355
+ }
356
+ /** @internal — publish the host end for `StreamWriter` (see `hostOf`). */
357
+ export function publishHostStream(s, h) {
358
+ hostOfStream.set(s, h);
359
+ }
360
+ /**
361
+ * A future handle. `await`able directly (`PromiseLike`), and droppable.
362
+ *
363
+ * A future whose write end dropped without ever writing rejects with
364
+ * `DroppedError` — not `undefined`, which `future<void>` legitimately yields.
365
+ */
366
+ export class Future {
367
+ /** Present once the underlying host end exists. */
368
+ #host;
369
+ /** Always present; resolves to the host end (immediately, when not deferred). */
370
+ #hostP;
371
+ #codec;
372
+ #consumed = false;
373
+ #dropped = false;
374
+ #settled = null;
375
+ constructor(host, hostP, codec) {
376
+ this.#host = host;
377
+ this.#hostP = hostP;
378
+ this.#codec = codec;
379
+ }
380
+ static fromLifted(value, codec) {
381
+ const h = hostFutureFor(value);
382
+ return new Future(h, Promise.resolve(h), codec);
383
+ }
384
+ static fromHostFuture(host, codec) {
385
+ return new Future(host, Promise.resolve(host), codec);
386
+ }
387
+ /**
388
+ * A future that is still in flight: the guest call that produces it has not
389
+ * resolved yet.
390
+ *
391
+ * CONTRACT (contracts/embedder-api.md): §"Functions and async" makes every
392
+ * export Promise-shaped, and §"Streams and futures" makes `Future<T>` a
393
+ * `PromiseLike<T>`. For an export whose *result* is a `future<T>` those two
394
+ * collide irreducibly: JS promise resolution unconditionally adopts a
395
+ * thenable, so `await someExport()` can never hand back a thenable handle —
396
+ * it hands back the value the handle would have yielded. Conservative
397
+ * reading, implemented here: the export returns the handle **eagerly** (it
398
+ * is itself PromiseLike, so `await` still works and still yields `T`), which
399
+ * keeps `drop()`/`cancel()` reachable for a caller that does not await. The
400
+ * alternative — resolving a Promise *to* the handle — is not expressible.
401
+ * Flagged in the C2 report.
402
+ */
403
+ static deferred(pending, codec) {
404
+ const hostP = pending.then((v) => {
405
+ const h = hostFutureFor(v);
406
+ f.adopt(h);
407
+ return h;
408
+ });
409
+ // Backstop (issue #182): a deferred handle that is never awaited,
410
+ // dropped, or cancelled still has `#hostP` sitting there uninspected — if
411
+ // the producing export call rejects, that is an unhandled rejection at
412
+ // the process level with no handle-level operation to blame. Attach a
413
+ // no-op rejection handler to a SEPARATE derived promise; `#read()` above
414
+ // still awaits the original `hostP`, so a real failure still surfaces to
415
+ // an awaiter (or through `cancel()`/`drop()`'s own swallows) exactly as
416
+ // before.
417
+ hostP.catch(() => { });
418
+ const f = new Future(null, hostP, codec);
419
+ return f;
420
+ }
421
+ /** @internal */
422
+ adopt(h) {
423
+ this.#host = h;
424
+ }
425
+ /** @internal */
426
+ takeValue() {
427
+ if (this.#host === null) {
428
+ throw new TypeError("this Future is still in flight and cannot be passed to a guest yet");
429
+ }
430
+ if (this.#consumed) {
431
+ throw new TypeError("this Future handle has already been passed to a guest");
432
+ }
433
+ this.#consumed = true;
434
+ return this.#host.value;
435
+ }
436
+ #read() {
437
+ // Post-transfer refusal (#162, amendment A15), the `Stream.read` mirror:
438
+ // once this handle was passed to a guest, the guest owns the readable end
439
+ // and a host read would operate a phantom duplicate. A read MEMOIZED
440
+ // before the transfer keeps resolving — it genuinely happened while the
441
+ // host still owned the end. Rejected rather than thrown: this runs under
442
+ // `then()`, where a synchronous throw escapes the promise chain.
443
+ if (this.#consumed && this.#settled === null) {
444
+ return Promise.reject(new TypeError("this Future handle has already been passed to a guest; the guest " +
445
+ "owns its readable end, so it can no longer be read from the " +
446
+ "host (issue #162)"));
447
+ }
448
+ this.#settled ??= (async () => {
449
+ const host = await this.#hostP;
450
+ const { value, result } = await host.readResult();
451
+ if (result !== CopyResult.COMPLETED) {
452
+ // A drop caused by the writer's instance trapping is a fault, not a
453
+ // "no value" outcome — brand it (#66, amendment A7).
454
+ throwIfPeerTrapped(host.value, this.#codec.where ?? "future read");
455
+ throw new DroppedError(result === CopyResult.CANCELLED
456
+ ? "the future read was cancelled"
457
+ : "the future's write end was dropped without a value");
458
+ }
459
+ return this.#codec.toHost(value);
460
+ })();
461
+ return this.#settled;
462
+ }
463
+ then(onfulfilled, onrejected) {
464
+ return this.#read().then(onfulfilled, onrejected);
465
+ }
466
+ cancel() {
467
+ if (this.#host !== null)
468
+ this.#host.cancel();
469
+ // A deferred future whose host end never materialized has nothing to
470
+ // cancel; swallow that rejection rather than let `cancel()` produce an
471
+ // unhandled one (issue #182 — mirrors `drop()` below).
472
+ else
473
+ void this.#hostP.then((h) => h.cancel(), () => { });
474
+ }
475
+ /**
476
+ * Release this future handle. Total and idempotent (#90): it never throws,
477
+ * and calling it twice — or after `Symbol.dispose` — is a no-op.
478
+ *
479
+ * Dropping a future the host never wrote to, once the guest already holds
480
+ * its readable end, is **abandonment**: the guest's reader can never be
481
+ * satisfied, so it is armed with a trap at its rendezvous point rather than
482
+ * being handed a value-less completion (exec/host_streams.ts
483
+ * `HostFuture.drop`, task/streams.ts `abandonSharedFuture`; the spec keeps
484
+ * that state unreachable by trapping the early writable drop,
485
+ * definitions.py:1183-1184). Write-then-drop is the normal path and is
486
+ * unaffected; a future no guest ever saw is plain cleanup.
487
+ */
488
+ drop() {
489
+ if (this.#dropped)
490
+ return;
491
+ this.#dropped = true;
492
+ if (this.#host !== null)
493
+ this.#host.drop();
494
+ // A deferred future whose host end never materialized has nothing to
495
+ // release; swallow that rejection rather than let `drop()` produce an
496
+ // unhandled one.
497
+ else
498
+ void this.#hostP.then((h) => h.drop(), () => { });
499
+ }
500
+ /** @internal — see `Stream.dropForTeardown` (#66). */
501
+ dropForTeardown() {
502
+ if (this.#dropped)
503
+ return;
504
+ if (this.#host !== null) {
505
+ this.#dropped = true;
506
+ dropSharedForTeardown(this.#host.value);
507
+ }
508
+ else {
509
+ // A deferred future (still in flight) cannot be an import argument;
510
+ // fall back to the plain drop for completeness.
511
+ this.drop();
512
+ }
513
+ }
514
+ [Symbol.dispose]() {
515
+ this.drop();
516
+ }
517
+ }
518
+ /**
519
+ * `error-context` as the contract spells it: `{ readonly message: string }`.
520
+ * The internal value is `task/streams.ts`'s `ErrorContext` (debug message
521
+ * only, per definitions.py).
522
+ */
523
+ export class ErrorContext {
524
+ message;
525
+ /** @internal — the internal value, preserved so it can be lowered back. */
526
+ internal;
527
+ constructor(internal) {
528
+ this.internal = internal;
529
+ this.message = internal.debugMessage;
530
+ }
531
+ }
532
+ // A9 brands (contracts/embedder-api.md §"Module identity"): the three
533
+ // STATEFUL embedder-facing handle classes. Their machinery lives in the copy
534
+ // that minted them, so the brand never makes a foreign handle usable — it
535
+ // makes it DIAGNOSABLE, at the lowering sites below.
536
+ defineBrand(Stream.prototype, STREAM);
537
+ defineBrand(Future.prototype, FUTURE);
538
+ defineBrand(ErrorContext.prototype, ERROR_CONTEXT);
539
+ /**
540
+ * Adapt a producer to a lowered `stream<T>` value, and own the pumping.
541
+ *
542
+ * The driving arm auto-closes on end (the pump drops the write end when the
543
+ * producer is exhausted) and on `DROPPED` (host_streams settles the activity
544
+ * arm) — R-fix review advisory 2, the deadlock-masking activity-lifetime
545
+ * footgun.
546
+ */
547
+ export function lowerStreamSource(src, codec) {
548
+ // Order matters (amendment A9). Same-copy handle: the fast path, unchanged.
549
+ if (src instanceof Stream) {
550
+ return src.takeValue(codec);
551
+ }
552
+ // Branded but not ours: a `Stream` minted by ANOTHER runtime copy. Without
553
+ // this check it would fall through to producer adaptation below and be
554
+ // pumped by its async iterator — a silent downgrade that quietly voids A5's
555
+ // identity guarantees. Refused, loudly, naming both copies (issue #83).
556
+ if (hasBrand(src, STREAM)) {
557
+ throw new TypeError(describeCrossCopy("this stream handle", "To pipe it by value, pass `src.readable()` instead."));
558
+ }
559
+ const host = hostStream(codec.element);
560
+ const stream = Stream.fromHostStream(host, codec);
561
+ publishHostStream(stream, host);
562
+ void pump(src, host, codec);
563
+ return host.value;
564
+ }
565
+ /**
566
+ * Lower one chunk of stream elements.
567
+ *
568
+ * u8 chunks come out as `Uint8Array` — either the caller's own (bytes are
569
+ * already canonical component values; issue #54's bulk-store path picks the
570
+ * typed array up unchanged at the rendezvous) or packed from a validated
571
+ * plain array. A `Uint8Array` offered to a NON-u8 stream keeps the legacy
572
+ * behavior: elements are fed through the per-element codec like any array.
573
+ */
574
+ function packChunk(values, codec) {
575
+ const u8 = isU8Element(codec.element);
576
+ if (values instanceof Uint8Array) {
577
+ if (u8)
578
+ return values;
579
+ return Array.from(values).map((v) => codec.fromHost(v));
580
+ }
581
+ const lowered = values.map((v) => codec.fromHost(v));
582
+ return u8
583
+ ? Uint8Array.from(lowered)
584
+ : lowered;
585
+ }
586
+ /** Race sentinel: the reader's end dropped while the producer was parked. */
587
+ const READER_GONE = Symbol("polyengine reader gone");
588
+ async function pump(src, host, codec) {
589
+ const where = codec.where ?? "stream producer";
590
+ let failure;
591
+ let produced = 0;
592
+ // A13 cancellation companion: the pump learns of the reader dropping
593
+ // through short writes, but a producer PARKED on an external event (an
594
+ // accept-shaped source holding a live platform resource) offers no write
595
+ // to shorten — this notification is its only stop signal. It also fires
596
+ // on the A7 teardown walk and on our own end-of-pump drop (harmless: the
597
+ // loop has exited by then).
598
+ const gone = new Promise((resolve) => host.writable.onDropped(() => resolve(READER_GONE)));
599
+ try {
600
+ for await (const batch of batches(src, gone)) {
601
+ // Lowering is the likeliest failure (a value of the wrong shape) and it
602
+ // must be attributed to the site, not swallowed into a short stream.
603
+ const lowered = packChunk(batch, codec);
604
+ let n;
605
+ try {
606
+ n = await host.writable.writeAll(lowered);
607
+ }
608
+ catch (e) {
609
+ // A13: elements past the fault's progress point were lowered but
610
+ // will never be taken — destroy them (an `own` element may hold a
611
+ // live platform resource). `PeerTrappedError.progress` reports
612
+ // delivered-before-the-fault; anything else delivered nothing.
613
+ releaseUntaken(lowered, e instanceof PeerTrappedError ? e.progress ?? 0 : 0, codec);
614
+ throw e;
615
+ }
616
+ produced += n;
617
+ if (n < lowered.length) {
618
+ // The reader went away: a clean end — but the un-taken tail of this
619
+ // chunk was already lowered and must be destroyed, not leaked (A13).
620
+ releaseUntaken(lowered, n, codec);
621
+ break;
622
+ }
623
+ }
624
+ }
625
+ catch (e) {
626
+ failure = e;
627
+ }
628
+ if (failure !== undefined) {
629
+ void produced;
630
+ // Report BEFORE dropping: the drop is what lets the guest see
631
+ // end-of-stream and resolve, and the driving loop checks `hostFailure`
632
+ // before it checks `done()`.
633
+ reportProducerFailure(host, where, failure);
634
+ }
635
+ // End of production == end of stream. Dropping unconditionally is what keeps
636
+ // the activity arm from outliving the data (R-fix advisory 2) and what stops
637
+ // a failed producer from hanging the guest forever; the failure has already
638
+ // been recorded on the store, so the call fails rather than resolving.
639
+ host.writable.drop();
640
+ }
641
+ /** A13: destroy `lowered[taken..]` when a codec's elements hold resources. */
642
+ function releaseUntaken(lowered, taken, codec) {
643
+ const release = codec.release;
644
+ if (release === undefined || lowered instanceof Uint8Array)
645
+ return;
646
+ for (let i = taken; i < lowered.length; i++)
647
+ release(lowered[i]);
648
+ }
649
+ /**
650
+ * Normalize every accepted producer shape to an async iterator of batches,
651
+ * racing each pull against `gone` (A13 cancellation): when the stream dies
652
+ * with the producer parked, a `ReadableStream` source is `cancel()`ed
653
+ * through its reader, and an (async-)iterable source gets its optional
654
+ * `cancel()` method invoked — the documented producer-cancellation hook —
655
+ * then its pending pull is drained so a straggler element the producer
656
+ * already minted still reaches the caller's release path. A source with no
657
+ * cancel hook keeps the pre-A13 behavior: the pump stays parked until the
658
+ * producer's next element (or forever — the documented embedder-negligence
659
+ * hang class).
660
+ */
661
+ async function* batches(src, gone) {
662
+ if (isReadableStream(src)) {
663
+ const reader = src.getReader();
664
+ try {
665
+ for (;;) {
666
+ const r = await Promise.race([reader.read(), gone]);
667
+ if (r === READER_GONE) {
668
+ // `cancel` settles the pending read and runs the source's own
669
+ // cancel() — releasing whatever platform resource backed it.
670
+ await reader.cancel().catch(() => { });
671
+ return;
672
+ }
673
+ if (r.done)
674
+ return;
675
+ yield asBatch(r.value);
676
+ }
677
+ }
678
+ finally {
679
+ reader.releaseLock();
680
+ }
681
+ }
682
+ if (Symbol.asyncIterator in src) {
683
+ const it = src[Symbol.asyncIterator]();
684
+ try {
685
+ for (;;) {
686
+ const pending = it.next();
687
+ const r = await Promise.race([pending, gone]);
688
+ if (r === READER_GONE) {
689
+ src.cancel?.();
690
+ try {
691
+ const last = await pending;
692
+ if (!last.done)
693
+ yield asBatch(last.value);
694
+ }
695
+ catch {
696
+ // A cancelled pull rejecting is its natural shape; the
697
+ // producer's own failure reporting has nothing to add here —
698
+ // the stream is already dead.
699
+ }
700
+ return;
701
+ }
702
+ if (r.done)
703
+ return;
704
+ yield asBatch(r.value);
705
+ }
706
+ }
707
+ finally {
708
+ // Runs the source generator's own finally blocks. Queued behind any
709
+ // still-pending pull, which the GONE arm above has already drained.
710
+ await it.return?.();
711
+ }
712
+ }
713
+ for (const v of src)
714
+ yield asBatch(v);
715
+ }
716
+ function asBatch(v) {
717
+ // Kept whole: `packChunk` decides whether the bytes are already lowered
718
+ // (u8 element) or need the per-element codec (any other element type).
719
+ if (v instanceof Uint8Array)
720
+ return v;
721
+ if (Array.isArray(v))
722
+ return v;
723
+ return [v];
724
+ }
725
+ function isReadableStream(v) {
726
+ return typeof ReadableStream !== "undefined" && v instanceof ReadableStream;
727
+ }
728
+ /** Adapt a `Promise`/`Future`/plain value to a lowered `future<T>` value. */
729
+ export function lowerFutureSource(src, codec) {
730
+ if (src instanceof Future)
731
+ return src.takeValue();
732
+ // Branded but not ours (amendment A9). This one is the sharpest edge in the
733
+ // family: `Future` is a `PromiseLike`, so a foreign future would otherwise
734
+ // be adopted as a plain thenable and appear to work — exactly the silent
735
+ // path A9 bans, since the awaited value would ride the OTHER copy's
736
+ // machinery with no handle transfer at all.
737
+ if (hasBrand(src, FUTURE)) {
738
+ throw new TypeError(describeCrossCopy("this future handle", "To pipe it by value, pass `Promise.resolve(f)` instead."));
739
+ }
740
+ const host = hostFuture(codec.element);
741
+ void (async () => {
742
+ try {
743
+ const v = await src;
744
+ await host.write(codec.fromHost(v));
745
+ }
746
+ catch (e) {
747
+ // The producer failed. `future<T>` has no error channel of its own, so
748
+ // the guest could only ever see a bare drop — the cause goes on the
749
+ // store's host-failure channel instead, exactly as for streams, so the
750
+ // in-flight call fails with a site-named error.
751
+ //
752
+ // And then we do NOT drop -- for ATTRIBUTION, not for safety. Dropping
753
+ // here is now well-defined (#90: an unwritten, lowered future's drop
754
+ // abandons it and the guest reader traps at its rendezvous point,
755
+ // exec/host_streams.ts `HostFuture.drop`); the stale version of this
756
+ // comment claimed it would trip an internal invariant, which was true
757
+ // before the abandonment mechanism existed and is not true now.
758
+ // Reporting instead of dropping is still the better outcome: the
759
+ // store-level failure names the producer and the site, so the in-flight
760
+ // call fails with the real cause rather than with a generic
761
+ // "the writable end went away" trap. Only when there is NO store to
762
+ // report to (the future was never lowered) do we fall back to dropping,
763
+ // so nothing can hang forever.
764
+ const reported = reportProducerFailure({ value: host.value }, codec.where ?? "future producer", e);
765
+ if (!reported)
766
+ host.drop();
767
+ }
768
+ })();
769
+ return host.value;
770
+ }