@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,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
|
+
}
|
package/esm/task/mod.js
ADDED
|
@@ -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 };
|