@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,94 @@
1
+ // Client for the translator shim's wasm32 C-ABI (crates/translator-shim).
2
+ // Platform-neutral: callers provide the shim module/bytes; file loading is
3
+ // embedder territory.
4
+ //
5
+ // C-ABI (see crates/translator-shim/src/lib.rs `cabi`):
6
+ // ts_alloc(len) -> ptr
7
+ // ts_translate(ptr, len, out_len_ptr) -> out_ptr (out = envelope JSON)
8
+ // ts_dealloc(ptr, len)
9
+ import { loadEnvelope, PlanError } from "../plan/loader.js";
10
+ /** An instantiated translator shim. One instance is reusable across calls. */
11
+ export class Translator {
12
+ #exports;
13
+ /**
14
+ * sha256 of the shim wasm bytes this instance was built from, hex-encoded;
15
+ * `null` when constructed from a pre-compiled `WebAssembly.Module` with no
16
+ * bytes available (module identity can't be recovered post-compile).
17
+ *
18
+ * This is the honest translator "build hash" for the artifact cache
19
+ * (docs/architecture.md §10): the wire envelope's `producer` block records
20
+ * `{shimVersion, wasmtimeEnviron, features}`, which does NOT change when
21
+ * the shim wasm is rebuilt from the same source versions (e.g. a local
22
+ * patch or a different toolchain producing different codegen) — see the
23
+ * M3-B dispatch. Digesting the actual bytes is the only sound cache key
24
+ * component for translator identity.
25
+ */
26
+ buildHash;
27
+ constructor(exports, buildHash) {
28
+ this.#exports = exports;
29
+ this.buildHash = buildHash;
30
+ for (const name of ["memory", "ts_alloc", "ts_dealloc", "ts_translate"]) {
31
+ if (!(name in this.#exports)) {
32
+ throw new PlanError(`shim module missing export '${name}'`);
33
+ }
34
+ }
35
+ }
36
+ /** Instantiate from compiled module or raw wasm bytes. */
37
+ static async create(source) {
38
+ let module;
39
+ let buildHash = null;
40
+ if (source instanceof WebAssembly.Module) {
41
+ module = source;
42
+ }
43
+ else {
44
+ module = await WebAssembly.compile(source.slice().buffer);
45
+ const digest = await crypto.subtle.digest("SHA-256", source.slice().buffer);
46
+ buildHash = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
47
+ }
48
+ const instance = await WebAssembly.instantiate(module, {});
49
+ return new Translator(instance.exports, buildHash);
50
+ }
51
+ /**
52
+ * Wrap an ALREADY-INSTANTIATED shim — the ESM wasm-module import path
53
+ * (issue #16 delivery design): `import * as shim from ".../translator_shim.wasm"`
54
+ * hands back an instantiated namespace (the shim imports nothing, so the
55
+ * ESM integration instantiates it trivially), and this wraps it with no
56
+ * further compile or copy.
57
+ *
58
+ * Sharing note: ESM gives ONE instance per realm, so every `fromExports`
59
+ * wrapper over the same namespace shares linear memory. That is safe by
60
+ * construction — `translate` is synchronous end-to-end (alloc → call →
61
+ * copy out → dealloc within one JS frame), so calls can never interleave —
62
+ * but treat the wrappers as equivalent, not independent.
63
+ *
64
+ * `buildHash` (hex sha-256 of the shim wasm bytes) cannot be recovered
65
+ * from an instance; pass it when known — a published package can ship the
66
+ * hash of the exact asset it carries — or leave it absent and the
67
+ * artifact cache politely refuses to key on translator identity
68
+ * (cache/core.ts).
69
+ */
70
+ static fromExports(exports, opts = {}) {
71
+ return new Translator(exports, opts.buildHash ?? null);
72
+ }
73
+ /** Translate a component binary into plan v0 + adapter artifacts. */
74
+ translate(componentBytes) {
75
+ const json = this.translateRaw(componentBytes);
76
+ const { wire, adapters } = loadEnvelope(json);
77
+ return { plan: wire, adapters, envelopeJson: json };
78
+ }
79
+ /** Translate, returning the raw envelope JSON without validation. */
80
+ translateRaw(componentBytes) {
81
+ const ex = this.#exports;
82
+ const inPtr = ex.ts_alloc(componentBytes.length);
83
+ new Uint8Array(ex.memory.buffer, inPtr, componentBytes.length).set(componentBytes);
84
+ const outLenPtr = ex.ts_alloc(4);
85
+ const outPtr = ex.ts_translate(inPtr, componentBytes.length, outLenPtr);
86
+ // Re-acquire views: translation may have grown (detached) the memory.
87
+ const outLen = new DataView(ex.memory.buffer).getUint32(outLenPtr, true);
88
+ const json = new TextDecoder().decode(new Uint8Array(ex.memory.buffer, outPtr, outLen));
89
+ ex.ts_dealloc(outPtr, outLen);
90
+ ex.ts_dealloc(outLenPtr, 4);
91
+ ex.ts_dealloc(inPtr, componentBytes.length);
92
+ return json;
93
+ }
94
+ }
@@ -0,0 +1,483 @@
1
+ // The 0.3 task model (docs/architecture.md §6): `ComponentInstance`, `Task`, and the
2
+ // re-export surface of the task core. Thread, Waitable/WaitableSet, Subtask
3
+ // and the scheduler live in sibling modules; see ./scheduler.ts for the
4
+ // scheduling-policy rationale and the generator-based thread model.
5
+ //
6
+ // Structural correspondence to definitions.py is the design constraint here:
7
+ // where this file diverges, the divergence is called out in a comment with
8
+ // the reference's line number. The two systematic divergences are
9
+ //
10
+ // 1. threads are generators, not OS threads (./scheduler.ts header), and
11
+ // 2. the shared-everything-threads built-ins (`thread.suspend-then-resume`
12
+ // and friends, 🧵) are absent rather than approximated — https://github.com/polymorph-components/polyengine/issues/12
13
+ // defers that feature with memory64.
14
+ import { Table } from "../cabi/handles.js";
15
+ import { assert_, trapIf } from "../cabi/trap.js";
16
+ import { CANCELLED_TRUE, chooseCandidate, Store, dbgId, NeedsJspi, notifyInstancePoisoned, PendingCapability, } from "./scheduler.js";
17
+ import { Thread } from "./thread.js";
18
+ import { Waitable, WaitableSet } from "./waitable.js";
19
+ import { Subtask } from "./subtask.js";
20
+ export * from "./scheduler.js";
21
+ export * from "./thread.js";
22
+ export * from "./waitable.js";
23
+ export * from "./subtask.js";
24
+ export * from "./streams.js";
25
+ /**
26
+ * Synthetic-root registry: one root per `Store` (see
27
+ * `ComponentInstanceState.rootOf`). A `WeakMap` so a dead store's root dies
28
+ * with it.
29
+ */
30
+ const syntheticRoots = new WeakMap();
31
+ /** `index` of the synthetic root — outside the real instance index space. */
32
+ const ROOT_INDEX = -1;
33
+ /** Constructor marker: "this one IS the root, do not give it a parent". */
34
+ const ROOT_TOKEN = Symbol("synthetic-root");
35
+ /**
36
+ * Per-component-instance runtime state (definitions.py `ComponentInstance`,
37
+ * line 191).
38
+ *
39
+ * `mayLeave` is backed by a real `WebAssembly.Global(i32, mutable)` because
40
+ * FACT adapters import that global (`flags` namespace) and read/write it as
41
+ * the may_leave boolean (wasmtime 47 FACT treats the whole flags global as
42
+ * may_leave; there is no bitmask). Initial value 1 (true). `mayEnter` is
43
+ * host-side state: nothing wasm-visible reads it.
44
+ */
45
+ export class ComponentInstanceState {
46
+ index;
47
+ flags;
48
+ handles = new Table();
49
+ /** definitions.py `ComponentInstance.threads` — a Table, so `thread.index`. */
50
+ threads = new Table();
51
+ mayEnter = true;
52
+ /** definitions.py `backpressure: int` — a *counter* (backpressure.{inc,dec}). */
53
+ backpressure = 0;
54
+ /** definitions.py `num_waiting_to_enter`. */
55
+ numWaitingToEnter = 0;
56
+ /** definitions.py `exclusive_thread`. */
57
+ exclusiveThread = null;
58
+ /**
59
+ * definitions.py `ComponentInstance.parent`.
60
+ *
61
+ * The plan still gives us a flat instance space, but the tree is no longer
62
+ * needed: every instance of one instantiation gets the same **synthetic
63
+ * root** as its parent (contracts/plan-format.md v3 amendment 4 /
64
+ * polyengine#101). See `enteringSet` for why that is observably equivalent to
65
+ * the real chain. The root itself has no parent.
66
+ */
67
+ parent;
68
+ store;
69
+ /**
70
+ * The synthetic per-instantiation root (v3 amendment 4). One per `Store`:
71
+ * a `Store` is exactly one component instantiation's scheduling scope, so
72
+ * "all `ComponentInstanceState`s sharing a `Store`" is the set that shares
73
+ * a top-level component — which is the granularity wasmtime's own
74
+ * top-level-instance-id comparison uses (concurrent.rs:1876-1886).
75
+ *
76
+ * It is a real `ComponentInstanceState` (index -1) rather than a bare flag
77
+ * so it flows through `selfAndAncestors`/`enteringSet` unchanged; its
78
+ * handle table stays empty and no task ever runs on it.
79
+ */
80
+ static rootOf(store) {
81
+ let root = syntheticRoots.get(store);
82
+ if (root === undefined) {
83
+ root = new ComponentInstanceState(ROOT_INDEX, store, ROOT_TOKEN);
84
+ syntheticRoots.set(store, root);
85
+ }
86
+ return root;
87
+ }
88
+ /** Is this the synthetic root (never a real component instance)? */
89
+ get isSyntheticRoot() {
90
+ return this.index === ROOT_INDEX && this.parent === null;
91
+ }
92
+ constructor(index, store, root) {
93
+ this.index = index;
94
+ this.store = store ?? new Store();
95
+ this.flags = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
96
+ // The root is its own tree's top; everything else hangs off it. Built
97
+ // lazily here so no call site has to remember to wire it up.
98
+ this.parent = root === ROOT_TOKEN
99
+ ? null
100
+ : ComponentInstanceState.rootOf(this.store);
101
+ }
102
+ /**
103
+ * Release the synthetic root after a trap broke the enter/leave bracket
104
+ * (v3 amendment 4, and a **named divergence** from the reference).
105
+ *
106
+ * definitions.py poisons the whole entering set: `Store.lift` never reaches
107
+ * `leave_to`, so the root — which is in every host entry's entering set —
108
+ * stays `may_enter == False` forever and NO instance of the component can
109
+ * be entered again. wasmtime is the same by other means (it poisons the
110
+ * store). polyengine deliberately supports post-trap re-entry of instances the
111
+ * trap did not touch (exec/boundary.ts `poison`: "sibling instances stay
112
+ * usable, which is why the lock is released per-instance rather than by
113
+ * poisoning a whole store the way wasmtime does"), and the synthetic root
114
+ * must not silently convert that documented divergence into store-wide
115
+ * poisoning. So a trap poisons the LEAF set only, and the root is released
116
+ * here — the reentrance gate the root exists for (a *second, concurrent*
117
+ * host entry) is about a live entry, and after a trap unwinds to the host
118
+ * there is none.
119
+ */
120
+ releaseSyntheticRootOnPoison() {
121
+ for (const inst of this.selfAndAncestors()) {
122
+ if (inst.isSyntheticRoot)
123
+ inst.mayEnter = true;
124
+ }
125
+ }
126
+ get mayLeave() {
127
+ return this.flags.value !== 0;
128
+ }
129
+ set mayLeave(v) {
130
+ this.flags.value = v ? 1 : 0;
131
+ }
132
+ /** definitions.py `ComponentInstance.self_and_ancestors` (line 236). */
133
+ selfAndAncestors() {
134
+ const s = new Set([this]);
135
+ let a = this.parent;
136
+ while (a !== null) {
137
+ s.add(a);
138
+ a = a.parent;
139
+ }
140
+ return s;
141
+ }
142
+ /**
143
+ * definitions.py `ComponentInstance.entering_set` (line 230):
144
+ * `self_and_ancestors() - caller.self_and_ancestors()`.
145
+ *
146
+ * CONTRACT (contracts/plan-format.md v3 amendment 4, polyengine#101): the plan
147
+ * still carries no wire form for the component-instance tree, and it no
148
+ * longer needs one. Every instance's parent is the synthetic
149
+ * per-instantiation root, so:
150
+ *
151
+ * * host entry (`caller === null`): `{this, root}` — the reference's
152
+ * entering set for a host entry is `self_and_ancestors()`, which always
153
+ * contains the top-level root, so a second host entry anywhere in the
154
+ * tree trips on the root either way. This is the divergence #101
155
+ * reported (host -> A.f -> host import -> host enters a *different*
156
+ * instance): now caught.
157
+ * * guest-to-guest (`caller !== null`): `{this}` — the root is in the
158
+ * caller's ancestor set and cancels out. Intermediate ancestors would
159
+ * be the only difference from the real chain, and they are never
160
+ * reachably consulted: FACT compiles same-instance and ancestor calls
161
+ * to unconditional compile-time traps, and sibling cycles are
162
+ * unreachable because instance imports form a DAG (polyengine#99
163
+ * adjudication).
164
+ *
165
+ * So the synthetic root is observably equivalent to the full chain, and it
166
+ * matches wasmtime's own shortcut — a top-level instance-id comparison
167
+ * (concurrent.rs:1876-1886) — by construction. This reopens only if some
168
+ * future upstream shape makes nesting depth observable.
169
+ *
170
+ * One deliberate departure remains, at the trap path rather than here: see
171
+ * `releaseSyntheticRootOnPoison`.
172
+ */
173
+ enteringSet(caller) {
174
+ const mine = this.selfAndAncestors();
175
+ if (caller === null)
176
+ return mine;
177
+ for (const c of caller.selfAndAncestors())
178
+ mine.delete(c);
179
+ return mine;
180
+ }
181
+ /** definitions.py `ComponentInstance.may_enter_from` (line 214). */
182
+ mayEnterFrom(caller) {
183
+ for (const inst of this.enteringSet(caller)) {
184
+ if (!inst.mayEnter)
185
+ return false;
186
+ }
187
+ return true;
188
+ }
189
+ /** definitions.py `ComponentInstance.enter_from` (line 220). */
190
+ enterFrom(caller) {
191
+ for (const inst of this.enteringSet(caller)) {
192
+ assert_(inst.mayEnter, "enter_from without may_enter");
193
+ inst.mayEnter = false;
194
+ }
195
+ }
196
+ /** definitions.py `ComponentInstance.leave_to` (line 225). */
197
+ leaveTo(caller) {
198
+ for (const inst of this.enteringSet(caller)) {
199
+ assert_(!inst.mayEnter, "leave_to without a matching enter_from");
200
+ inst.mayEnter = true;
201
+ }
202
+ }
203
+ /** Backwards-compatible host-entry helpers (the M0 spelling). */
204
+ enter() {
205
+ this.enterFrom(null);
206
+ }
207
+ leave() {
208
+ this.leaveTo(null);
209
+ }
210
+ }
211
+ /** definitions.py `LiftOptions.equal` (line 643): encoding + memory identity. */
212
+ export function liftOptionsEqual(a, b) {
213
+ return a.stringEncoding === b.stringEncoding && a.memory === b.memory;
214
+ }
215
+ /**
216
+ * One export activation (definitions.py `class Task`, line 444). Also the
217
+ * task-side borrow scope: `numBorrows` satisfies cabi's `TaskBorrowScope`.
218
+ */
219
+ const ADMIT_TRACE = (() => {
220
+ try {
221
+ return Deno.env.get("CE_SP_TRACE") === "1";
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ })();
227
+ export class Task {
228
+ ft;
229
+ opts;
230
+ inst;
231
+ onStart;
232
+ onResolve;
233
+ state = "initial";
234
+ /** TaskBorrowScope (cabi/context.ts): live borrows lowered into this task. */
235
+ numBorrows = 0;
236
+ implicitThread = null;
237
+ threads = [];
238
+ /**
239
+ * True for a task created by a FACT cross-component call
240
+ * (`prepare-call`, see intrinsics/fact_calls.ts).
241
+ *
242
+ * Such a task's `onStart` / `onResolve` carry **flat core values**, not
243
+ * lifted component values: FACT fuses the caller-side lift and callee-side
244
+ * lower into a pair of adapter functions (`[async-start]` / `[async-return]`)
245
+ * that run *in wasm*, so the host only shuttles the core values between
246
+ * them. definitions.py has no analogue because it has no fused adapters —
247
+ * there, `canon_lift` lowers the params and `canon_lower`'s `on_resolve`
248
+ * lifts the results, both in the host. The observable semantics are
249
+ * identical; only which side of the boundary performs the copy differs.
250
+ *
251
+ * `canon_task_return` consults this to decide whether to lift its flat
252
+ * arguments (host-boundary task) or pass them straight through (FACT task).
253
+ */
254
+ factPassthrough = false;
255
+ /**
256
+ * Plan v3: does `ft.results` hold this FACT task's *declared* result type?
257
+ *
258
+ * A FACT callee task's result type arrives as the raw wasmtime
259
+ * `TypeTupleIndex` `prepare-call` passes as `task_return_type`; v3's
260
+ * `task-return.results` / `resultType` pair is the dictionary for it
261
+ * (contracts/plan-format.md v3 amendment 3). It resolves for every callee
262
+ * that has a `task.return` trampoline of its own — which is every callee
263
+ * that can call `task.return` — but a callee with none (sync-lifted,
264
+ * reached through an async-to-sync adapter) contributes no entry, and then
265
+ * `ft.results` is the empty placeholder it was before v3. Only when this is
266
+ * true may `canon_task_return` compare against it.
267
+ */
268
+ factResultTypesKnown = false;
269
+ /**
270
+ * In-flight FACT sync-call brackets for THIS task
271
+ * (`enter-sync-call`/`exit-sync-call`).
272
+ *
273
+ * MOVED to `Thread` (see `Thread.syncCallStack`). Per-task was already an
274
+ * improvement on per-executor, but it is still not the right unit: a task
275
+ * can own several threads, so one activation's `exit-sync-call` could pop a
276
+ * sibling activation's scope. Tracing big-interleaving showed exactly that
277
+ * -- tasks whose `enter` count exceeded their `exit` count by one, and other
278
+ * tasks taking an `exit` at depth 0, with the `ctx` fallback never firing.
279
+ *
280
+ * The bracket belongs to the ACTIVATION that opened it: FACT emits the
281
+ * matching `enter-sync-call` and `exit-sync-call` from the same wasm
282
+ * activation by construction, so riding the activation identity makes the
283
+ * exit find the same stack the enter used no matter which task the scheduler
284
+ * considers current in between (the 3i bracket-spans-suspension ruling).
285
+ */
286
+ constructor(ft, opts, inst, onStart, onResolve) {
287
+ this.ft = ft;
288
+ this.opts = opts;
289
+ this.inst = inst;
290
+ this.onStart = onStart;
291
+ this.onResolve = onResolve;
292
+ }
293
+ /**
294
+ * definitions.py `Task.needs_exclusive` (line 473): an async-typed task
295
+ * needs the instance's exclusive thread unless it is a *stackful* async
296
+ * lift. Sync-lowered (`not opts.async_`) and callback-ABI tasks both do.
297
+ */
298
+ needsExclusive() {
299
+ assert_(this.ft.async === true, "needs_exclusive on a sync-typed task");
300
+ return !this.opts.async_ || this.opts.callback;
301
+ }
302
+ /**
303
+ * definitions.py `Task.enter_implicit_thread` (line 477) — the backpressure
304
+ * and exclusivity gate, in full.
305
+ *
306
+ * Returns false when the task was cancelled while waiting to enter, in
307
+ * which case the caller must return immediately (the task is already
308
+ * resolved by `cancel()`).
309
+ */
310
+ *enterImplicitThread(thread) {
311
+ assert_(this.state === "initial", "enter_implicit_thread after start");
312
+ this.implicitThread = thread;
313
+ if (this.ft.async === true) {
314
+ const hasBackpressure = () => this.inst.backpressure > 0 ||
315
+ (this.needsExclusive() && this.inst.exclusiveThread !== null);
316
+ // The `num_waiting_to_enter > 0` disjunct is what makes entry a queue
317
+ // rather than a stampede: once anyone is waiting, later arrivals wait
318
+ // too, even if backpressure has since cleared.
319
+ if (hasBackpressure() || this.inst.numWaitingToEnter > 0) {
320
+ this.inst.numWaitingToEnter += 1;
321
+ let cancelled;
322
+ try {
323
+ cancelled = yield* thread.waitUntil(() => !hasBackpressure(), true);
324
+ }
325
+ finally {
326
+ this.inst.numWaitingToEnter -= 1;
327
+ }
328
+ if (cancelled) {
329
+ this.cancel();
330
+ return false;
331
+ }
332
+ }
333
+ if (this.needsExclusive()) {
334
+ assert_(this.inst.exclusiveThread === null, "entering with the exclusive thread already taken");
335
+ this.inst.exclusiveThread = thread;
336
+ }
337
+ }
338
+ if (ADMIT_TRACE) {
339
+ console.error(`[admit] task=${dbgId(this)} thread=${dbgId(thread)}`);
340
+ }
341
+ this.registerThread(thread);
342
+ return true;
343
+ }
344
+ /** definitions.py `Task.register_thread` (line 497). */
345
+ registerThread(thread) {
346
+ assert_(!this.threads.includes(thread) && thread.task === this, "register_thread of a foreign or duplicate thread");
347
+ this.threads.push(thread);
348
+ assert_(thread.index === null, "register_thread of an indexed thread");
349
+ thread.index = this.inst.threads.add(thread);
350
+ }
351
+ /** definitions.py `Task.exit_implicit_thread` (line 503). */
352
+ exitImplicitThread(thread) {
353
+ assert_(thread === this.implicitThread, "exit of a non-implicit thread");
354
+ this.unregisterThread(thread);
355
+ if (this.ft.async === true && this.needsExclusive()) {
356
+ // definitions.py lines 506-508, verbatim shape: assert-held, then
357
+ // release. The former release-if-held tolerance existed only for the
358
+ // removed release-at-BLOCK divergence (issue #43); under the hold rule
359
+ // the implicit thread of a needs-exclusive task holds the slot from
360
+ // `enter_implicit_thread` to here, without exception.
361
+ assert_(this.inst.exclusiveThread === thread, "exit_implicit_thread without holding the exclusive thread");
362
+ this.inst.exclusiveThread = null;
363
+ }
364
+ }
365
+ /** definitions.py `Task.unregister_thread` (line 510). */
366
+ unregisterThread(thread) {
367
+ const i = this.threads.indexOf(thread);
368
+ assert_(i !== -1 && thread.task === this, "unregister of a foreign thread");
369
+ this.threads.splice(i, 1);
370
+ if (this.threads.length === 0) {
371
+ trapIf(this.state !== "resolved", "task finished all threads without resolving");
372
+ assert_(this.numBorrows === 0, "task exited with live borrows");
373
+ }
374
+ assert_(thread.index !== null, "unregister of an unindexed thread");
375
+ this.inst.threads.remove(thread.index);
376
+ thread.index = null;
377
+ }
378
+ /**
379
+ * definitions.py `Task.request_cancellation` (line 519). Delivered to a
380
+ * cancellable thread if one exists and the instance is enterable; otherwise
381
+ * recorded as pending, to be picked up at the next cancellable block point
382
+ * (`deliverPendingCancel`).
383
+ */
384
+ requestCancellation(caller) {
385
+ if (this.state === "initial") {
386
+ this.state = "cancel-delivered";
387
+ this.implicitThread.resume(CANCELLED_TRUE);
388
+ return;
389
+ }
390
+ assert_(this.state === "started", `request_cancellation in state ${this.state}`);
391
+ let candidates = this.threads.filter((t) => t.cancellable);
392
+ const excludeImplicit = this.ft.async === true && this.needsExclusive() &&
393
+ this.inst.exclusiveThread !== null &&
394
+ this.inst.exclusiveThread !== this.implicitThread;
395
+ if (excludeImplicit) {
396
+ candidates = candidates.filter((t) => t !== this.implicitThread);
397
+ }
398
+ // Suspension points of this task's activation are frames OF the implicit
399
+ // thread, so they obey the same exclusion (definitions.py line 526: with
400
+ // another thread holding the exclusive slot, the implicit thread may not
401
+ // run).
402
+ if (!excludeImplicit) {
403
+ const store = this.inst.store;
404
+ for (const w of store.waiting) {
405
+ if (w.task === this && w.cancellable === true &&
406
+ !candidates.includes(w)) {
407
+ candidates.push(w);
408
+ }
409
+ }
410
+ }
411
+ if (candidates.length > 0 && this.inst.mayEnterFrom(caller)) {
412
+ this.state = "cancel-delivered";
413
+ this.inst.enterFrom(caller);
414
+ try {
415
+ chooseCandidate(candidates).resume(CANCELLED_TRUE);
416
+ }
417
+ catch (e) {
418
+ // Deliberately NOT a `finally`, mirroring `Store.tick`'s
419
+ // bracket-break discipline (scheduler.ts): the reference wraps the
420
+ // delivery `resume(Cancelled.TRUE)` in no handler at all
421
+ // (definitions.py `Task.request_cancellation`, lines 519-532; the
422
+ // delivery is line 531), so a Trap escaping it never reaches
423
+ // `leave_to` on line 532 — the entered set stays locked, i.e. the
424
+ // Component Model's instance poisoning. A `finally` here would
425
+ // un-poison a half-unwound callee.
426
+ //
427
+ // Capability signals are the exception, exactly as in `tick`: they
428
+ // mark this RUNTIME incomplete, not the component faulted, and in
429
+ // the reference the blocking operation completes and `leave_to` IS
430
+ // reached.
431
+ if (e instanceof NeedsJspi || e instanceof PendingCapability) {
432
+ this.inst.leaveTo(caller);
433
+ }
434
+ else {
435
+ // The synthetic root is released so the poisoning stays
436
+ // per-instance (plan v3 amendment 4); for a guest caller the
437
+ // entering set is the leaf alone and the release is a no-op.
438
+ this.inst.releaseSyntheticRootOnPoison();
439
+ notifyInstancePoisoned(this.inst, e);
440
+ }
441
+ throw e;
442
+ }
443
+ this.inst.leaveTo(caller);
444
+ }
445
+ else {
446
+ this.state = "pending-cancel";
447
+ }
448
+ }
449
+ /** definitions.py `Task.deliver_pending_cancel` (line 536). */
450
+ deliverPendingCancel(cancellable) {
451
+ if (cancellable && this.state === "pending-cancel") {
452
+ this.state = "cancel-delivered";
453
+ return true;
454
+ }
455
+ return false;
456
+ }
457
+ /** definitions.py `Task.start` (line 542). */
458
+ start() {
459
+ assert_(this.state === "initial", "start on a started task");
460
+ this.state = "started";
461
+ return this.onStart();
462
+ }
463
+ /** definitions.py `Task.return_` (line 547). */
464
+ return_(result) {
465
+ trapIf(this.state === "resolved", "task.return on a resolved task");
466
+ // Wording parity with wasmtime's exit-time check, pinned by
467
+ // drop-cross-task-borrow.wast:309.
468
+ trapIf(this.numBorrows > 0, "borrow handles still remain at the end of the call");
469
+ this.onResolve(result);
470
+ this.state = "resolved";
471
+ }
472
+ /** definitions.py `Task.cancel` (line 554). */
473
+ cancel() {
474
+ trapIf(this.state !== "cancel-delivered", "task.cancel without a delivered cancellation request");
475
+ // Same definitions.py check as `return_` (num_borrows at exit); same
476
+ // call-end wording.
477
+ trapIf(this.numBorrows > 0, "borrow handles still remain at the end of the call");
478
+ this.onResolve(null);
479
+ this.state = "resolved";
480
+ }
481
+ }
482
+ /** Convenience re-exports so `../task/mod.ts` remains the single entry point. */
483
+ export { Store, Subtask, Thread, Waitable, WaitableSet };