@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.
- package/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cabi/async_values.js +162 -0
- package/esm/cabi/bulk_lists.js +198 -0
- package/esm/cabi/context.js +42 -0
- package/esm/cabi/flatten.js +145 -0
- package/esm/cabi/float.js +67 -0
- package/esm/cabi/handles.js +253 -0
- package/esm/cabi/layout.js +149 -0
- package/esm/cabi/lift.js +196 -0
- package/esm/cabi/load.js +146 -0
- package/esm/cabi/lower.js +141 -0
- package/esm/cabi/memory.js +182 -0
- package/esm/cabi/mod.js +22 -0
- package/esm/cabi/store.js +186 -0
- package/esm/cabi/strings.js +336 -0
- package/esm/cabi/trap.js +38 -0
- package/esm/cabi/types.js +264 -0
- package/esm/cabi/values.js +64 -0
- package/esm/cache/core.js +156 -0
- package/esm/cache/dir.js +170 -0
- package/esm/cache/mod.js +4 -0
- package/esm/cache/web.js +136 -0
- package/esm/digest/digest.js +332 -0
- package/esm/digest/mod.js +3 -0
- package/esm/digest/verify.js +129 -0
- package/esm/embedder/casing.js +56 -0
- package/esm/embedder/copy.js +42 -0
- package/esm/embedder/errors.js +26 -0
- package/esm/embedder/imports.js +63 -0
- package/esm/embedder/instantiate.js +978 -0
- package/esm/embedder/mod.js +40 -0
- package/esm/embedder/resources.js +406 -0
- package/esm/embedder/streams.js +770 -0
- package/esm/embedder/values.js +450 -0
- package/esm/embedder/version.js +273 -0
- package/esm/exec/boundary.js +1902 -0
- package/esm/exec/executor.js +1004 -0
- package/esm/exec/host_streams.js +818 -0
- package/esm/exec/mod.js +4 -0
- package/esm/intrinsics/async_builtins.js +510 -0
- package/esm/intrinsics/context.js +90 -0
- package/esm/intrinsics/errors.js +13 -0
- package/esm/intrinsics/fact_calls.js +865 -0
- package/esm/intrinsics/mod.js +564 -0
- package/esm/intrinsics/stream_builtins.js +578 -0
- package/esm/intrinsics/transcode.js +443 -0
- package/esm/jspi/bridge.js +579 -0
- package/esm/jspi/mechanics.js +89 -0
- package/esm/jspi/mod.js +5 -0
- package/esm/jspi/suspending.js +15 -0
- package/esm/jspi/types.js +29 -0
- package/esm/package.json +3 -0
- package/esm/plan/format.js +5 -0
- package/esm/plan/loader.js +657 -0
- package/esm/plan/mod.js +3 -0
- package/esm/shim/mod.js +2 -0
- package/esm/shim/translator.js +94 -0
- package/esm/task/mod.js +483 -0
- package/esm/task/scheduler.js +1028 -0
- package/esm/task/streams.js +786 -0
- package/esm/task/subtask.js +135 -0
- package/esm/task/thread.js +255 -0
- package/esm/task/waitable.js +144 -0
- package/package.json +91 -0
- package/types/cabi/async_values.d.ts +35 -0
- package/types/cabi/bulk_lists.d.ts +18 -0
- package/types/cabi/context.d.ts +59 -0
- package/types/cabi/flatten.d.ts +14 -0
- package/types/cabi/float.d.ts +14 -0
- package/types/cabi/handles.d.ts +70 -0
- package/types/cabi/layout.d.ts +13 -0
- package/types/cabi/lift.d.ts +25 -0
- package/types/cabi/load.d.ts +14 -0
- package/types/cabi/lower.d.ts +10 -0
- package/types/cabi/memory.d.ts +58 -0
- package/types/cabi/mod.d.ts +15 -0
- package/types/cabi/store.d.ts +12 -0
- package/types/cabi/strings.d.ts +23 -0
- package/types/cabi/trap.d.ts +11 -0
- package/types/cabi/types.d.ts +206 -0
- package/types/cabi/values.d.ts +5 -0
- package/types/cache/core.d.ts +97 -0
- package/types/cache/dir.d.ts +6 -0
- package/types/cache/mod.d.ts +3 -0
- package/types/cache/web.d.ts +10 -0
- package/types/digest/digest.d.ts +17 -0
- package/types/digest/mod.d.ts +2 -0
- package/types/digest/verify.d.ts +48 -0
- package/types/embedder/casing.d.ts +40 -0
- package/types/embedder/copy.d.ts +24 -0
- package/types/embedder/errors.d.ts +11 -0
- package/types/embedder/imports.d.ts +47 -0
- package/types/embedder/instantiate.d.ts +88 -0
- package/types/embedder/mod.d.ts +11 -0
- package/types/embedder/resources.d.ts +158 -0
- package/types/embedder/streams.d.ts +202 -0
- package/types/embedder/values.d.ts +70 -0
- package/types/embedder/version.d.ts +85 -0
- package/types/exec/boundary.d.ts +360 -0
- package/types/exec/executor.d.ts +125 -0
- package/types/exec/host_streams.d.ts +165 -0
- package/types/exec/mod.d.ts +3 -0
- package/types/intrinsics/async_builtins.d.ts +69 -0
- package/types/intrinsics/context.d.ts +28 -0
- package/types/intrinsics/errors.d.ts +5 -0
- package/types/intrinsics/fact_calls.d.ts +120 -0
- package/types/intrinsics/mod.d.ts +187 -0
- package/types/intrinsics/stream_builtins.d.ts +113 -0
- package/types/intrinsics/transcode.d.ts +21 -0
- package/types/jspi/bridge.d.ts +227 -0
- package/types/jspi/mechanics.d.ts +50 -0
- package/types/jspi/mod.d.ts +3 -0
- package/types/jspi/suspending.d.ts +1 -0
- package/types/jspi/types.d.ts +26 -0
- package/types/plan/format.d.ts +369 -0
- package/types/plan/loader.d.ts +113 -0
- package/types/plan/mod.d.ts +2 -0
- package/types/shim/mod.d.ts +1 -0
- package/types/shim/translator.d.ts +55 -0
- package/types/task/mod.d.ts +257 -0
- package/types/task/scheduler.d.ts +421 -0
- package/types/task/streams.d.ts +370 -0
- package/types/task/subtask.d.ts +96 -0
- package/types/task/thread.d.ts +73 -0
- 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);
|