@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,135 @@
|
|
|
1
|
+
// definitions.py `### Subtask State` (line 858): one in-progress call from
|
|
2
|
+
// this component to an import.
|
|
3
|
+
import { assert_, trapIf } from "../cabi/trap.js";
|
|
4
|
+
import { EventCode, Waitable } from "./waitable.js";
|
|
5
|
+
/** definitions.py `Subtask.State` (line 859). */
|
|
6
|
+
export var SubtaskState;
|
|
7
|
+
(function (SubtaskState) {
|
|
8
|
+
SubtaskState[SubtaskState["STARTING"] = 0] = "STARTING";
|
|
9
|
+
SubtaskState[SubtaskState["STARTED"] = 1] = "STARTED";
|
|
10
|
+
SubtaskState[SubtaskState["RETURNED"] = 2] = "RETURNED";
|
|
11
|
+
SubtaskState[SubtaskState["CANCELLED_BEFORE_STARTED"] = 3] = "CANCELLED_BEFORE_STARTED";
|
|
12
|
+
SubtaskState[SubtaskState["CANCELLED_BEFORE_RETURNED"] = 4] = "CANCELLED_BEFORE_RETURNED";
|
|
13
|
+
})(SubtaskState || (SubtaskState = {}));
|
|
14
|
+
export class Subtask extends Waitable {
|
|
15
|
+
state = SubtaskState.STARTING;
|
|
16
|
+
onCancel = null;
|
|
17
|
+
cancellationRequested = false;
|
|
18
|
+
flatResults = [];
|
|
19
|
+
/**
|
|
20
|
+
* The callee TASK behind this subtask, when there is one (FACT
|
|
21
|
+
* cross-component calls; host-import subtasks have none). `subtask.cancel`
|
|
22
|
+
* needs it under jspi: a cancellation delivered to a suspended activation
|
|
23
|
+
* resumes it on a MICROTASK (the engine's, not ours), so the async form
|
|
24
|
+
* must wait until the callee's state is determinate before choosing
|
|
25
|
+
* between BLOCKED and the resolved state — the same determinacy question
|
|
26
|
+
* `async-start-call` answers, and it needs the same object to ask it of.
|
|
27
|
+
*/
|
|
28
|
+
// deno-lint-ignore no-explicit-any
|
|
29
|
+
calleeTask = null;
|
|
30
|
+
/**
|
|
31
|
+
* Handles lent to the callee for the duration of the call. `null` once
|
|
32
|
+
* `deliverResolve` has run — the reference uses exactly this
|
|
33
|
+
* `lenders is None` sentinel to mean "resolve delivered" (line 908), so the
|
|
34
|
+
* nullability is semantic, not an optimization.
|
|
35
|
+
*/
|
|
36
|
+
lenders = [];
|
|
37
|
+
/** definitions.py `Subtask.resolved` (line 880). */
|
|
38
|
+
resolved() {
|
|
39
|
+
switch (this.state) {
|
|
40
|
+
case SubtaskState.STARTING:
|
|
41
|
+
case SubtaskState.STARTED:
|
|
42
|
+
return false;
|
|
43
|
+
default:
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** definitions.py `Subtask.add_lender` (line 890). */
|
|
48
|
+
addLender(h) {
|
|
49
|
+
assert_(!this.resolveDelivered() && !this.resolved(), "addLender on a resolved subtask");
|
|
50
|
+
h.numLends += 1;
|
|
51
|
+
this.lenders.push(h);
|
|
52
|
+
}
|
|
53
|
+
/** definitions.py `Subtask.resolve` (line 895). */
|
|
54
|
+
resolve(state, flatResults) {
|
|
55
|
+
assert_(state === SubtaskState.RETURNED || flatResults.length === 0, "non-RETURNED subtask resolution carries results");
|
|
56
|
+
assert_(!this.resolved(), "resolve on an already-resolved subtask");
|
|
57
|
+
this.state = state;
|
|
58
|
+
this.flatResults = flatResults;
|
|
59
|
+
}
|
|
60
|
+
/** definitions.py `Subtask.deliver_resolve` (line 902). */
|
|
61
|
+
deliverResolve() {
|
|
62
|
+
assert_(!this.resolveDelivered() && this.resolved(), "deliverResolve on an unresolved or already-delivered subtask");
|
|
63
|
+
for (const h of this.lenders)
|
|
64
|
+
h.numLends -= 1;
|
|
65
|
+
this.lenders = null;
|
|
66
|
+
}
|
|
67
|
+
/** definitions.py `Subtask.resolve_delivered` (line 908). */
|
|
68
|
+
resolveDelivered() {
|
|
69
|
+
assert_(this.lenders !== null || this.resolved(), "lenders released on an unresolved subtask");
|
|
70
|
+
return this.lenders === null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Release a never-delivered subtask's lenders after its call broke off a
|
|
74
|
+
* non-poisoning exit — trap-rethrow past the CALLEE, capability bail, or
|
|
75
|
+
* an abandoned park (contracts/intrinsics.md v0.2 amendment 2, #91 scope
|
|
76
|
+
* clarification; the park legs are #102/#106).
|
|
77
|
+
*
|
|
78
|
+
* The reference has no analogue because it never resumes after a trap:
|
|
79
|
+
* the store dies with the lent handles inside it. The resolution state
|
|
80
|
+
* mirrors `canon_lower`'s `on_resolve(None)` branch (definitions.py
|
|
81
|
+
* line 2267): CANCELLED_BEFORE_STARTED if the callee never started,
|
|
82
|
+
* CANCELLED_BEFORE_RETURNED otherwise.
|
|
83
|
+
*
|
|
84
|
+
* Idempotent, and a no-op when the resolution was already delivered — a
|
|
85
|
+
* settled hook can call it unconditionally without disturbing the success
|
|
86
|
+
* path's own `deliverResolve`.
|
|
87
|
+
*/
|
|
88
|
+
unwindLenders() {
|
|
89
|
+
if (!this.resolved()) {
|
|
90
|
+
this.resolve(this.state === SubtaskState.STARTING
|
|
91
|
+
? SubtaskState.CANCELLED_BEFORE_STARTED
|
|
92
|
+
: SubtaskState.CANCELLED_BEFORE_RETURNED, []);
|
|
93
|
+
}
|
|
94
|
+
if (!this.resolveDelivered())
|
|
95
|
+
this.deliverResolve();
|
|
96
|
+
}
|
|
97
|
+
/** definitions.py `Subtask.drop` (line 912). */
|
|
98
|
+
drop() {
|
|
99
|
+
trapIf(!this.resolveDelivered(), "cannot drop a subtask which has not yet resolved");
|
|
100
|
+
super.drop();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* definitions.py `canon_lower`'s `on_progress`/`subtask_event` closure
|
|
104
|
+
* (lines 2297-2298). The event payload is computed **at delivery time** and
|
|
105
|
+
* delivering it is what runs `deliver_resolve` — so the lent handles are
|
|
106
|
+
* released exactly when the guest observes the resolution, not when it
|
|
107
|
+
* happens.
|
|
108
|
+
*
|
|
109
|
+
* The `!this.resolveDelivered()` guard has no reference analogue: it exists
|
|
110
|
+
* so this can coexist with `unwindLenders()` (which may itself have already
|
|
111
|
+
* delivered the resolve on an abandoned path). The reference's
|
|
112
|
+
* `subtask_event` calls `deliver_resolve()` unconditionally and would
|
|
113
|
+
* assert on a double delivery.
|
|
114
|
+
*/
|
|
115
|
+
setSubtaskPendingEvent(subtaski) {
|
|
116
|
+
this.setPendingEvent(() => {
|
|
117
|
+
if (this.resolved() && !this.resolveDelivered())
|
|
118
|
+
this.deliverResolve();
|
|
119
|
+
return [EventCode.SUBTASK, subtaski, this.state];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Pack a `canon_lower` async return value: `state | (subtaski << 4)`
|
|
125
|
+
* (definitions.py line 2306, with the accompanying asserts on the ranges).
|
|
126
|
+
*/
|
|
127
|
+
export function packSubtaskResult(state, subtaski) {
|
|
128
|
+
assert_(subtaski > 0 && subtaski <= 2 ** 28 - 1, "subtask index out of packing range");
|
|
129
|
+
assert_(state >= 0 && state < 2 ** 4, "subtask state out of packing range");
|
|
130
|
+
return (state | (subtaski << 4)) >>> 0;
|
|
131
|
+
}
|
|
132
|
+
/** Inverse of {@link packSubtaskResult}; used by tests mirroring the reference. */
|
|
133
|
+
export function unpackSubtaskResult(packed) {
|
|
134
|
+
return [(packed & 0xf), packed >>> 4];
|
|
135
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// definitions.py `class Thread` (line 317), reimplemented over JS generators.
|
|
2
|
+
//
|
|
3
|
+
// Mapping to the reference, state for state:
|
|
4
|
+
//
|
|
5
|
+
// reference here
|
|
6
|
+
// ------------------------------------------------------------------
|
|
7
|
+
// cont is None (running) #state === "running"
|
|
8
|
+
// cont set, ready_func None #state === "suspended"
|
|
9
|
+
// cont set, ready_func set #state === "waiting" (in store.waiting)
|
|
10
|
+
// thread.storage[2] storage: [0, 0] (context.{get,set})
|
|
11
|
+
// thread.index index (inst.threads table slot)
|
|
12
|
+
//
|
|
13
|
+
// The reference's `resume()` drives a chain of `switch_to` handoffs
|
|
14
|
+
// (`suspend_then_resume` and friends, lines 408-437). Those are the 🧵
|
|
15
|
+
// shared-everything-threads built-ins, which https://github.com/polymorph-components/polyengine/issues/12 defers along with
|
|
16
|
+
// memory64; `resume()` here therefore handles a single thread, and the
|
|
17
|
+
// switch-to variants are absent rather than approximated.
|
|
18
|
+
import { assert_ } from "../cabi/trap.js";
|
|
19
|
+
import { CANCELLED_FALSE, CANCELLED_TRUE, NeedsJspi, notifyInstancePoisoned, isInstancePoisoned, PendingCapability, popCurrentThread, pushCurrentThread, } from "./scheduler.js";
|
|
20
|
+
export class Thread {
|
|
21
|
+
task;
|
|
22
|
+
/**
|
|
23
|
+
* Per-thread context slots (definitions.py `Thread.storage`, line 323 —
|
|
24
|
+
* initialised `[0,0]`). `canon_context_{get,set}` (lines 2348/2358) read and
|
|
25
|
+
* write *this*, not per-task state: two threads of the same task have
|
|
26
|
+
* independent context. wit-bindgen 0.60 keeps its async task pointer in
|
|
27
|
+
* slot 0.
|
|
28
|
+
*
|
|
29
|
+
* Slots are plain JS numbers: a `context.set` of an i64 value above
|
|
30
|
+
* 2^53-1 would lose precision. Moot while memory64/threads support is
|
|
31
|
+
* deferred (issue #12) — revisit this when that issue's closure lands.
|
|
32
|
+
*/
|
|
33
|
+
storage = [0, 0];
|
|
34
|
+
/**
|
|
35
|
+
* The FACT sync-call bracket stack for THIS activation.
|
|
36
|
+
*
|
|
37
|
+
* `enter-sync-call` pushes and `exit-sync-call` pops; FACT emits both from
|
|
38
|
+
* the same activation, so the activation is the continuity that makes this a
|
|
39
|
+
* stack. See the note on `Task.syncCallStack` for why per-task was not
|
|
40
|
+
* enough.
|
|
41
|
+
*/
|
|
42
|
+
// deno-lint-ignore no-explicit-any
|
|
43
|
+
syncCallStack = [];
|
|
44
|
+
/** Slot in `inst.threads`, assigned by `Task.registerThread`. */
|
|
45
|
+
index = null;
|
|
46
|
+
/** definitions.py `Thread.cancellable` — set at each block point. */
|
|
47
|
+
cancellable = false;
|
|
48
|
+
#state = "suspended";
|
|
49
|
+
#body;
|
|
50
|
+
#readyFunc = null;
|
|
51
|
+
#store;
|
|
52
|
+
// deno-lint-ignore no-explicit-any
|
|
53
|
+
constructor(task, body) {
|
|
54
|
+
this.task = task;
|
|
55
|
+
this.#body = body;
|
|
56
|
+
this.#store = task.inst.store;
|
|
57
|
+
}
|
|
58
|
+
running() {
|
|
59
|
+
return this.#state === "running";
|
|
60
|
+
}
|
|
61
|
+
suspended() {
|
|
62
|
+
return this.#state === "suspended";
|
|
63
|
+
}
|
|
64
|
+
waiting() {
|
|
65
|
+
return this.#state === "waiting";
|
|
66
|
+
}
|
|
67
|
+
done() {
|
|
68
|
+
return this.#state === "done";
|
|
69
|
+
}
|
|
70
|
+
/** definitions.py `Thread.ready` (line 334). */
|
|
71
|
+
ready() {
|
|
72
|
+
return this.waiting() && this.#readyFunc !== null && this.#readyFunc();
|
|
73
|
+
}
|
|
74
|
+
/** definitions.py `Thread.start_waiting_internal` (line 350). */
|
|
75
|
+
#startWaiting(readyFunc) {
|
|
76
|
+
assert_(!this.waiting() && this.#readyFunc === null);
|
|
77
|
+
this.#readyFunc = readyFunc;
|
|
78
|
+
this.#state = "waiting";
|
|
79
|
+
this.#store.startWaiting(this);
|
|
80
|
+
}
|
|
81
|
+
/** definitions.py `Thread.stop_waiting_internal` (line 355). */
|
|
82
|
+
#stopWaiting(cancelled) {
|
|
83
|
+
assert_(this.waiting() && this.#readyFunc !== null);
|
|
84
|
+
assert_(cancelled || this.ready(), "stopWaiting on a thread that is neither ready nor cancelled");
|
|
85
|
+
this.#readyFunc = null;
|
|
86
|
+
this.#state = "suspended";
|
|
87
|
+
this.#store.stopWaiting(this);
|
|
88
|
+
}
|
|
89
|
+
/** definitions.py `Thread.resume_later` (line 361). */
|
|
90
|
+
resumeLater() {
|
|
91
|
+
assert_(this.suspended(), "resume_later on a non-suspended thread");
|
|
92
|
+
this.#startWaiting(() => true);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* definitions.py `Thread.resume` (line 366): run the body until it blocks
|
|
96
|
+
* again or finishes.
|
|
97
|
+
*
|
|
98
|
+
* The reference's loop over `switch_to` targets is omitted (see the module
|
|
99
|
+
* header). What remains is: leave the waiting list if we were on it, become
|
|
100
|
+
* the current thread, and step the generator with the cancelled flag.
|
|
101
|
+
*/
|
|
102
|
+
/** Pending `awaitValue` promise, if this thread is parked on one. */
|
|
103
|
+
awaiting = null;
|
|
104
|
+
/** Resume a promise-parked thread with the settled result. */
|
|
105
|
+
resumeWith(value, failure) {
|
|
106
|
+
assert_(this.awaiting !== null, "resumeWith on a thread that is not awaiting");
|
|
107
|
+
this.awaiting = null;
|
|
108
|
+
this.#store.awaiting.delete(this);
|
|
109
|
+
this.#state = "suspended";
|
|
110
|
+
// The reentrance bracket, exactly as `Store.tick` puts around `resume()`.
|
|
111
|
+
//
|
|
112
|
+
// Every thread resumption in the reference runs under the instance's
|
|
113
|
+
// entered lock, and a trap propagating out of the resumed thread skips
|
|
114
|
+
// `leave_to` — which is the Component Model's instance poisoning
|
|
115
|
+
// (definitions.py `Store.tick` line 597; see the matching comment in
|
|
116
|
+
// scheduler.ts). This path is a resumption too — the value arrived
|
|
117
|
+
// through a Promise instead of a ready-condition, but the thread body
|
|
118
|
+
// (and any wasm it runs) is the same — so it takes the same bracket.
|
|
119
|
+
// Without it, a trap delivered as an `awaitValue` rejection (how EVERY
|
|
120
|
+
// guest trap in a suspended activation arrives under jspi, pin (e))
|
|
121
|
+
// unwound cleanly and the instance stayed enterable: the second call of
|
|
122
|
+
// `builtin-trap-poisons-instance.wast` then re-ran the guest and
|
|
123
|
+
// reported "cannot drop busy stream" where the suite demands the
|
|
124
|
+
// poisoned-instance "cannot enter component instance".
|
|
125
|
+
//
|
|
126
|
+
// Capability signals release the lock, for the same reason as in `tick`:
|
|
127
|
+
// they mark the RUNTIME incomplete, not the component faulted.
|
|
128
|
+
const inst = this.task.inst;
|
|
129
|
+
// A poisoned instance's parked segments never run again: this settle
|
|
130
|
+
// belongs to an activation that was in flight when a SIBLING activation
|
|
131
|
+
// trapped (the trap kept the reentrance lock — CM poisoning — and #66
|
|
132
|
+
// retired the handle tables). Resuming would re-enter the corpse, and
|
|
133
|
+
// asserting turned one legible trap into an assert cascade (the
|
|
134
|
+
// wosh-M2 shape: `list too long`, then this assert as second victim).
|
|
135
|
+
// Retire quietly: the abandoned call's own driver reports, via its
|
|
136
|
+
// deadlock trap naming the export.
|
|
137
|
+
if (isInstancePoisoned(inst))
|
|
138
|
+
return;
|
|
139
|
+
// The enterability check below is an internal BACKSTOP, not a live gate:
|
|
140
|
+
// every dispatch site (`Store.serviceSettled`, `driveAsync`'s race-winner
|
|
141
|
+
// path) now guards enterable-or-poisoned before calling and DEFERS the
|
|
142
|
+
// tail otherwise (issue #156) — under the shared synthetic root, a host
|
|
143
|
+
// entry into any instance of the graph makes every sibling
|
|
144
|
+
// non-enterable, so this assert was reachable, and (mutating before
|
|
145
|
+
// asserting) it stranded the thread and lost the settle. It stays to
|
|
146
|
+
// protect the invariant for any future caller.
|
|
147
|
+
assert_(inst.mayEnterFrom(null), "resumeWith: parked thread's instance is not enterable from the host");
|
|
148
|
+
inst.enterFrom(null);
|
|
149
|
+
try {
|
|
150
|
+
this.#resumeInternal(value, failure);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (e instanceof NeedsJspi || e instanceof PendingCapability) {
|
|
154
|
+
inst.leaveTo(null);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// The bracket stays broken (instance poisoned, comment above) — same
|
|
158
|
+
// as `Store.tick`: retire the poisoned table's stream/future ends so
|
|
159
|
+
// parked host peers settle instead of hanging (#66), and release the
|
|
160
|
+
// synthetic root so the poisoning stays per-instance (plan v3
|
|
161
|
+
// amendment 4; `releaseSyntheticRootOnPoison`).
|
|
162
|
+
inst.releaseSyntheticRootOnPoison();
|
|
163
|
+
notifyInstancePoisoned(inst, e);
|
|
164
|
+
}
|
|
165
|
+
throw e;
|
|
166
|
+
}
|
|
167
|
+
inst.leaveTo(null);
|
|
168
|
+
}
|
|
169
|
+
resume(cancelled = CANCELLED_FALSE) {
|
|
170
|
+
assert_(!this.running() && !this.done(), "resume() on a running or finished thread");
|
|
171
|
+
assert_(this.cancellable || !cancelled, "cancelled resume of a non-cancellable block point");
|
|
172
|
+
if (this.waiting())
|
|
173
|
+
this.#stopWaiting(cancelled);
|
|
174
|
+
this.#resumeInternal(cancelled);
|
|
175
|
+
}
|
|
176
|
+
#resumeInternal(sendValue, failure) {
|
|
177
|
+
this.#state = "running";
|
|
178
|
+
pushCurrentThread(this);
|
|
179
|
+
let step;
|
|
180
|
+
try {
|
|
181
|
+
step = failure === undefined
|
|
182
|
+
? this.#body.next(sendValue)
|
|
183
|
+
// Throw the rejection *into* the body so a post-resume trap unwinds
|
|
184
|
+
// through the same `finally`s a synchronous one would (jspi pin (e)).
|
|
185
|
+
: this.#body.throw(failure.error);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
// The body threw (a trap, or one of our capability errors). The thread
|
|
189
|
+
// is finished either way; the exception propagates to whoever was
|
|
190
|
+
// driving the scheduler.
|
|
191
|
+
this.#state = "done";
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
popCurrentThread(this);
|
|
196
|
+
}
|
|
197
|
+
if (step.done) {
|
|
198
|
+
this.#state = "done";
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const req = step.value;
|
|
202
|
+
this.cancellable = req.cancellable;
|
|
203
|
+
if (req.awaitValue !== undefined) {
|
|
204
|
+
// Parked on a Promise, not on a scheduler condition. The driving loop
|
|
205
|
+
// owns it from here (exec/boundary.ts `drive`); parking through
|
|
206
|
+
// `noteAwaiting` arms the eager settle tracking the scheduler's
|
|
207
|
+
// phantom-state gate depends on (see `Store.settled`).
|
|
208
|
+
this.#state = "suspended";
|
|
209
|
+
this.awaiting = req.awaitValue;
|
|
210
|
+
this.#store.noteAwaiting(this, req.awaitValue);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (req.readyFunc === null) {
|
|
214
|
+
// `suspend`: resumable only by an explicit `resume`/`resumeLater`.
|
|
215
|
+
this.#state = "suspended";
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.#state = "suspended";
|
|
219
|
+
this.#startWaiting(req.readyFunc);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* definitions.py `Thread.wait_until` (line 396), as a generator-side helper.
|
|
224
|
+
*
|
|
225
|
+
* Call it from a thread body with `yield*`:
|
|
226
|
+
* `const cancelled = yield* thread.waitUntil(() => cond, true);`
|
|
227
|
+
*
|
|
228
|
+
* Deviation from the reference, deliberate: the reference may return
|
|
229
|
+
* immediately when `ready_func()` already holds
|
|
230
|
+
* (`if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1)`).
|
|
231
|
+
* We always take the blocking path, i.e. we behave as the reference's
|
|
232
|
+
* `DETERMINISTIC_PROFILE`. Blocking-then-immediately-ready is observably
|
|
233
|
+
* equivalent (the scheduler will find this thread ready on the next
|
|
234
|
+
* candidate scan) and it removes a coin flip from every wait.
|
|
235
|
+
*/
|
|
236
|
+
*waitUntil(readyFunc, cancellable = false) {
|
|
237
|
+
assert_(this.running(), "waitUntil on a non-running thread");
|
|
238
|
+
if (this.task.deliverPendingCancel(cancellable))
|
|
239
|
+
return CANCELLED_TRUE;
|
|
240
|
+
const cancelled = yield { readyFunc, cancellable };
|
|
241
|
+
return cancelled;
|
|
242
|
+
}
|
|
243
|
+
/** definitions.py `Thread.suspend` (line 390). */
|
|
244
|
+
*suspend(cancellable) {
|
|
245
|
+
assert_(this.running(), "suspend on a non-running thread");
|
|
246
|
+
if (this.task.deliverPendingCancel(cancellable))
|
|
247
|
+
return CANCELLED_TRUE;
|
|
248
|
+
const cancelled = yield { readyFunc: null, cancellable };
|
|
249
|
+
return cancelled;
|
|
250
|
+
}
|
|
251
|
+
/** definitions.py `Thread.yield_` (line 405): `wait_until(lambda: True)`. */
|
|
252
|
+
*yield_(cancellable) {
|
|
253
|
+
return yield* this.waitUntil(() => true, cancellable);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// definitions.py `### Waitable State` (line 754) — the event protocol shared
|
|
2
|
+
// by subtasks and (later) stream/future ends.
|
|
3
|
+
import { assert_, trapIf } from "../cabi/trap.js";
|
|
4
|
+
import { chooseCandidate } from "./scheduler.js";
|
|
5
|
+
/** definitions.py `EventCode` (line 756). */
|
|
6
|
+
export var EventCode;
|
|
7
|
+
(function (EventCode) {
|
|
8
|
+
EventCode[EventCode["NONE"] = 0] = "NONE";
|
|
9
|
+
EventCode[EventCode["SUBTASK"] = 1] = "SUBTASK";
|
|
10
|
+
EventCode[EventCode["STREAM_READ"] = 2] = "STREAM_READ";
|
|
11
|
+
EventCode[EventCode["STREAM_WRITE"] = 3] = "STREAM_WRITE";
|
|
12
|
+
EventCode[EventCode["FUTURE_READ"] = 4] = "FUTURE_READ";
|
|
13
|
+
EventCode[EventCode["FUTURE_WRITE"] = 5] = "FUTURE_WRITE";
|
|
14
|
+
EventCode[EventCode["TASK_CANCELLED"] = 6] = "TASK_CANCELLED";
|
|
15
|
+
})(EventCode || (EventCode = {}));
|
|
16
|
+
export const NO_EVENT = [EventCode.NONE, 0, 0];
|
|
17
|
+
/**
|
|
18
|
+
* definitions.py `class Waitable` (line 767).
|
|
19
|
+
*
|
|
20
|
+
* The pending event is a **thunk**, not a value: the reference computes the
|
|
21
|
+
* payload at delivery time (`get_pending_event` calls it), which is what lets
|
|
22
|
+
* a subtask report its *final* state even if it advanced between the event
|
|
23
|
+
* being set and being read. Keeping the thunk is load-bearing — see
|
|
24
|
+
* `Subtask.setPendingEvent`.
|
|
25
|
+
*/
|
|
26
|
+
export class Waitable {
|
|
27
|
+
pendingEvent = null;
|
|
28
|
+
wset = null;
|
|
29
|
+
hasSyncWaiter = false;
|
|
30
|
+
setPendingEvent(pendingEvent) {
|
|
31
|
+
this.pendingEvent = pendingEvent;
|
|
32
|
+
}
|
|
33
|
+
hasPendingEvent() {
|
|
34
|
+
return this.pendingEvent !== null;
|
|
35
|
+
}
|
|
36
|
+
inWaitableSet() {
|
|
37
|
+
return this.wset !== null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* definitions.py `Waitable.wait_for_pending_event` (line 786): a
|
|
41
|
+
* *non-cancellable* block until this waitable has an event, used by the
|
|
42
|
+
* synchronous `subtask.cancel` path.
|
|
43
|
+
*/
|
|
44
|
+
*waitForPendingEvent(thread) {
|
|
45
|
+
assert_(!this.inWaitableSet() && !this.hasSyncWaiter, "waitForPendingEvent on a joined or already-awaited waitable");
|
|
46
|
+
this.hasSyncWaiter = true;
|
|
47
|
+
yield* thread.waitUntil(() => this.hasPendingEvent(), false);
|
|
48
|
+
this.hasSyncWaiter = false;
|
|
49
|
+
}
|
|
50
|
+
getPendingEvent() {
|
|
51
|
+
const pendingEvent = this.pendingEvent;
|
|
52
|
+
assert_(pendingEvent !== null, "getPendingEvent with no pending event");
|
|
53
|
+
this.pendingEvent = null;
|
|
54
|
+
return pendingEvent();
|
|
55
|
+
}
|
|
56
|
+
/** definitions.py `Waitable.join` (line 797). */
|
|
57
|
+
join(wset) {
|
|
58
|
+
assert_(!this.hasSyncWaiter, "join on a waitable with a sync waiter");
|
|
59
|
+
if (this.wset) {
|
|
60
|
+
const i = this.wset.elems.indexOf(this);
|
|
61
|
+
assert_(i !== -1, "waitable not in its own waitable set");
|
|
62
|
+
this.wset.elems.splice(i, 1);
|
|
63
|
+
}
|
|
64
|
+
this.wset = wset;
|
|
65
|
+
if (wset)
|
|
66
|
+
wset.elems.push(this);
|
|
67
|
+
}
|
|
68
|
+
/** definitions.py `Waitable.drop` (line 805). */
|
|
69
|
+
drop() {
|
|
70
|
+
assert_(!this.hasPendingEvent(), "dropping a waitable with a pending event");
|
|
71
|
+
assert_(!this.hasSyncWaiter, "dropping a waitable with a sync waiter");
|
|
72
|
+
this.join(null);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const EV_TRACE = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return Deno.env.get("CE_EVENT_TRACE") === "1";
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
/** definitions.py `class WaitableSet` (line 810). */
|
|
84
|
+
export class WaitableSet {
|
|
85
|
+
elems = [];
|
|
86
|
+
numWaiting = 0;
|
|
87
|
+
hasPendingEvent() {
|
|
88
|
+
return this.elems.some((w) => w.hasPendingEvent());
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* definitions.py `WaitableSet.get_pending_event` (line 821). The reference
|
|
92
|
+
* shuffles `elems` before scanning; we scan in **join order** under the
|
|
93
|
+
* default FIFO policy (`chooseCandidate` over the ready elements), which is
|
|
94
|
+
* within the same allowed nondeterminism — see scheduler.ts's policy note.
|
|
95
|
+
*/
|
|
96
|
+
getPendingEvent() {
|
|
97
|
+
const ready = this.elems.filter((w) => w.hasPendingEvent());
|
|
98
|
+
assert_(ready.length > 0, "getPendingEvent on a set with no pending event");
|
|
99
|
+
const w = chooseCandidate(ready);
|
|
100
|
+
assert_(w.wset === this, "waitable/waitable-set back-reference mismatch");
|
|
101
|
+
const ev = w.getPendingEvent();
|
|
102
|
+
if (EV_TRACE) {
|
|
103
|
+
console.error(`[event] deliver code=${ev[0]} idx=${ev[1]} payload=${ev[2]} ` +
|
|
104
|
+
`readyCount=${ready.length} setSize=${this.elems.length} ` +
|
|
105
|
+
`chosenPos=${this.elems.indexOf(w)}`);
|
|
106
|
+
}
|
|
107
|
+
return ev;
|
|
108
|
+
}
|
|
109
|
+
/** definitions.py `WaitableSet.wait_for_event_and` (line 829). */
|
|
110
|
+
*waitForEventAnd(thread, readyFunc, cancellable) {
|
|
111
|
+
this.numWaiting += 1;
|
|
112
|
+
try {
|
|
113
|
+
const cancelled = yield* thread.waitUntil(() => readyFunc() && this.hasPendingEvent(), cancellable);
|
|
114
|
+
return cancelled
|
|
115
|
+
? [EventCode.TASK_CANCELLED, 0, 0]
|
|
116
|
+
: this.getPendingEvent();
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
this.numWaiting -= 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** definitions.py `WaitableSet.wait_for_event` (line 841). */
|
|
123
|
+
*waitForEvent(thread, cancellable) {
|
|
124
|
+
return yield* this.waitForEventAnd(thread, () => true, cancellable);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* definitions.py `WaitableSet.poll` (line 844). Never blocks, so it is a
|
|
128
|
+
* plain function rather than a generator.
|
|
129
|
+
*/
|
|
130
|
+
// deno-lint-ignore no-explicit-any
|
|
131
|
+
poll(task, cancellable) {
|
|
132
|
+
if (task.deliverPendingCancel(cancellable)) {
|
|
133
|
+
return [EventCode.TASK_CANCELLED, 0, 0];
|
|
134
|
+
}
|
|
135
|
+
if (!this.hasPendingEvent())
|
|
136
|
+
return [EventCode.NONE, 0, 0];
|
|
137
|
+
return this.getPendingEvent();
|
|
138
|
+
}
|
|
139
|
+
/** definitions.py `WaitableSet.drop` (line 852). */
|
|
140
|
+
drop() {
|
|
141
|
+
trapIf(this.elems.length > 0, "cannot drop waitable set with waitables");
|
|
142
|
+
trapIf(this.numWaiting > 0, "cannot drop waitable set with waiters");
|
|
143
|
+
}
|
|
144
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polyengine/runtime",
|
|
3
|
+
"version": "0.1.0-pre.g633468a",
|
|
4
|
+
"description": "A WebAssembly Component Model host for JavaScript engines: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, and embedder API.",
|
|
5
|
+
"homepage": "https://github.com/polymorph-components/polyengine#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/polymorph-components/polyengine.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/polymorph-components/polyengine/issues"
|
|
13
|
+
},
|
|
14
|
+
"module": "./esm/cabi/mod.js",
|
|
15
|
+
"types": "./types/cabi/mod.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
"./cabi": {
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./types/cabi/mod.d.ts",
|
|
20
|
+
"default": "./esm/cabi/mod.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"./cache": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./types/cache/mod.d.ts",
|
|
26
|
+
"default": "./esm/cache/mod.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"./plan": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./types/plan/mod.d.ts",
|
|
32
|
+
"default": "./esm/plan/mod.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./task": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./types/task/mod.d.ts",
|
|
38
|
+
"default": "./esm/task/mod.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"./exec": {
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./types/exec/mod.d.ts",
|
|
44
|
+
"default": "./esm/exec/mod.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"./embedder": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./types/embedder/mod.d.ts",
|
|
50
|
+
"default": "./esm/embedder/mod.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./digest": {
|
|
54
|
+
"import": {
|
|
55
|
+
"types": "./types/digest/mod.d.ts",
|
|
56
|
+
"default": "./esm/digest/mod.js"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"./intrinsics": {
|
|
60
|
+
"import": {
|
|
61
|
+
"types": "./types/intrinsics/mod.d.ts",
|
|
62
|
+
"default": "./esm/intrinsics/mod.js"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"./shim": {
|
|
66
|
+
"import": {
|
|
67
|
+
"types": "./types/shim/mod.d.ts",
|
|
68
|
+
"default": "./esm/shim/mod.js"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"./jspi": {
|
|
72
|
+
"import": {
|
|
73
|
+
"types": "./types/jspi/mod.d.ts",
|
|
74
|
+
"default": "./esm/jspi/mod.js"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"./package.json": "./package.json"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {},
|
|
80
|
+
"type": "module",
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=22.14.0"
|
|
83
|
+
},
|
|
84
|
+
"publishConfig": {
|
|
85
|
+
"access": "public"
|
|
86
|
+
},
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"@polyengine/protocol": "0.1.0-pre.g633468a"
|
|
89
|
+
},
|
|
90
|
+
"_generatedBy": "dnt@0.43.2"
|
|
91
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LiftLowerContext } from "./context.js";
|
|
2
|
+
import type { BorrowType, OwnType, ValType } from "./types.js";
|
|
3
|
+
import { ErrorContext, type SharedBase } from "../task/streams.js";
|
|
4
|
+
/**
|
|
5
|
+
* Diagnostic for a handle-table entry that carries the A9 error-context brand
|
|
6
|
+
* without being one of THIS copy's `ErrorContext`s (amendment A9, issue #83).
|
|
7
|
+
*
|
|
8
|
+
* A backstop, deliberately: the embedder's lowering site (embedder/values.ts)
|
|
9
|
+
* refuses a foreign error-context before it can ever reach a handle table, so
|
|
10
|
+
* this branch should be unreachable. It exists because "handle is not an
|
|
11
|
+
* error-context" is precisely the misleading generic that made #83 expensive
|
|
12
|
+
* to diagnose — if a path ever does get here, it says what happened.
|
|
13
|
+
*
|
|
14
|
+
* This layer is below `embedder/`, so it composes the census from
|
|
15
|
+
* `@polyengine/protocol` directly rather than importing `embedder/copy.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function errorContextTrapMessage(where: string, e: unknown): string;
|
|
18
|
+
export declare function liftStream(cx: LiftLowerContext, i: number, t: ValType & {
|
|
19
|
+
kind: "stream";
|
|
20
|
+
element: ValType | null;
|
|
21
|
+
}): SharedBase;
|
|
22
|
+
export declare function liftFuture(cx: LiftLowerContext, i: number, t: ValType & {
|
|
23
|
+
kind: "future";
|
|
24
|
+
element: ValType | null;
|
|
25
|
+
}): SharedBase;
|
|
26
|
+
/** definitions.py `lower_stream` (line 1828). */
|
|
27
|
+
export declare function lowerStream(cx: LiftLowerContext, v: SharedBase, t: ValType): number;
|
|
28
|
+
/** definitions.py `lower_future` (line 1833). */
|
|
29
|
+
export declare function lowerFuture(cx: LiftLowerContext, v: SharedBase, t: ValType): number;
|
|
30
|
+
/** definitions.py `lift_error_context` (line 1451). Does NOT remove the handle. */
|
|
31
|
+
export declare function liftErrorContext(cx: LiftLowerContext, i: number): ErrorContext;
|
|
32
|
+
/** definitions.py `lower_error_context` (line 1757). */
|
|
33
|
+
export declare function lowerErrorContext(cx: LiftLowerContext, v: ErrorContext): number;
|
|
34
|
+
/** Unused-import guards for the type-only imports above. */
|
|
35
|
+
export type _AsyncValueTypes = OwnType | BorrowType;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MemInst } from "./memory.js";
|
|
2
|
+
import type { ComponentValue } from "./types.js";
|
|
3
|
+
export declare const PLATFORM_LITTLE_ENDIAN: boolean;
|
|
4
|
+
/** Kinds these helpers handle (besides them, u8 has its own path). */
|
|
5
|
+
export declare function isBulkListKind(kind: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Bulk lift of `length` elements of `kind` at `ptr`. Returns `null` when the
|
|
8
|
+
* kind is not handled here (caller falls back to the per-element loop) —
|
|
9
|
+
* never for a handled kind on a little-endian platform with an aligned view.
|
|
10
|
+
* The caller has already trap-checked alignment and bounds.
|
|
11
|
+
*/
|
|
12
|
+
export declare function tryLoadNumericList(mem: MemInst, ptr: number, length: number, kind: string): ComponentValue[] | null;
|
|
13
|
+
/**
|
|
14
|
+
* Bulk store of `v` as elements of `kind` at `ptr`. Returns false when not
|
|
15
|
+
* handled (caller falls back). The caller has already trap-checked alignment
|
|
16
|
+
* and bounds.
|
|
17
|
+
*/
|
|
18
|
+
export declare function tryStoreNumericList(mem: MemInst, v: ArrayLike<ComponentValue>, ptr: number, kind: string): boolean;
|