@polyengine/runtime 0.5.1 → 0.6.0
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/esm/cabi/async_values.js +6 -5
- package/esm/cabi/bulk_lists.js +0 -5
- package/esm/cabi/context.js +13 -3
- package/esm/cabi/flatten.js +41 -9
- package/esm/cabi/handles.js +57 -54
- package/esm/cabi/layout.js +113 -52
- package/esm/cabi/load.js +31 -23
- package/esm/cabi/store.js +33 -26
- package/esm/cabi/trap.js +2 -2
- package/esm/cabi/types.js +138 -25
- package/esm/cabi/values.js +25 -7
- package/esm/cache/core.js +2 -11
- package/esm/digest/digest.js +10 -8
- package/esm/digest/mod.js +1 -1
- package/esm/digest/verify.js +6 -86
- package/esm/embedder/casing.js +24 -9
- package/esm/embedder/copy.js +6 -6
- package/esm/embedder/errors.js +2 -2
- package/esm/embedder/imports.js +3 -3
- package/esm/embedder/instantiate.js +132 -37
- package/esm/embedder/mod.js +9 -8
- package/esm/embedder/resources.js +39 -16
- package/esm/embedder/streams.js +36 -37
- package/esm/embedder/sync.js +242 -0
- package/esm/embedder/values.js +84 -22
- package/esm/embedder/version.js +9 -9
- package/esm/exec/boundary.js +123 -161
- package/esm/exec/executor.js +37 -25
- package/esm/exec/host_streams.js +31 -31
- package/esm/intrinsics/async_builtins.js +15 -7
- package/esm/intrinsics/context.js +1 -1
- package/esm/intrinsics/errors.js +9 -9
- package/esm/intrinsics/fact_calls.js +37 -49
- package/esm/intrinsics/mod.js +54 -117
- package/esm/intrinsics/stream_builtins.js +2 -2
- package/esm/intrinsics/transcode.js +1 -1
- package/esm/jspi/bridge.js +4 -3
- package/esm/jspi/suspending.js +5 -5
- package/esm/plan/loader.js +5 -5
- package/esm/shim/translator.js +2 -2
- package/esm/task/mod.js +45 -182
- package/esm/task/scheduler.js +154 -185
- package/esm/task/streams.js +39 -54
- package/esm/task/subtask.js +2 -2
- package/esm/task/thread.js +20 -41
- package/esm/task/waitable.js +0 -1
- package/package.json +2 -2
- package/types/cabi/async_values.d.ts +3 -2
- package/types/cabi/bulk_lists.d.ts +0 -2
- package/types/cabi/context.d.ts +15 -5
- package/types/cabi/flatten.d.ts +2 -2
- package/types/cabi/handles.d.ts +15 -26
- package/types/cabi/layout.d.ts +22 -1
- package/types/cabi/load.d.ts +10 -2
- package/types/cabi/store.d.ts +4 -2
- package/types/cabi/types.d.ts +22 -3
- package/types/digest/mod.d.ts +1 -1
- package/types/digest/verify.d.ts +3 -19
- package/types/embedder/casing.d.ts +9 -1
- package/types/embedder/copy.d.ts +4 -4
- package/types/embedder/instantiate.d.ts +4 -4
- package/types/embedder/mod.d.ts +3 -2
- package/types/embedder/resources.d.ts +20 -7
- package/types/embedder/streams.d.ts +5 -6
- package/types/embedder/sync.d.ts +81 -0
- package/types/embedder/values.d.ts +2 -2
- package/types/exec/boundary.d.ts +55 -44
- package/types/exec/executor.d.ts +3 -2
- package/types/exec/host_streams.d.ts +8 -8
- package/types/intrinsics/errors.d.ts +3 -3
- package/types/intrinsics/mod.d.ts +1 -1
- package/types/intrinsics/stream_builtins.d.ts +2 -2
- package/types/jspi/bridge.d.ts +6 -5
- package/types/plan/format.d.ts +11 -10
- package/types/plan/loader.d.ts +2 -2
- package/types/shim/translator.d.ts +2 -2
- package/types/task/mod.d.ts +26 -97
- package/types/task/scheduler.d.ts +81 -62
- package/types/task/streams.d.ts +23 -38
- package/types/task/subtask.d.ts +2 -2
- package/types/task/waitable.d.ts +0 -1
package/esm/task/scheduler.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// stackless (callback-ABI) path never blocks *inside* a wasm frame — every
|
|
44
44
|
// wasm call returns a callback code before the host decides to wait. Blocking
|
|
45
45
|
// inside a wasm frame (stackful async lifts; a sync lower on an unresolved
|
|
46
|
-
// subtask) genuinely requires JSPI
|
|
46
|
+
// subtask) genuinely requires JSPI; those sites fail loudly
|
|
47
47
|
// rather than pretending (see `needsJspi`).
|
|
48
48
|
import { assert_, trapIf } from "../cabi/trap.js";
|
|
49
49
|
/** definitions.py `Cancelled` (line 248). */
|
|
@@ -60,7 +60,7 @@ export const CANCELLED_TRUE = true;
|
|
|
60
60
|
*/
|
|
61
61
|
export class NeedsJspi extends Error {
|
|
62
62
|
constructor(what) {
|
|
63
|
-
super(`needs JSPI
|
|
63
|
+
super(`needs JSPI: ${what}`);
|
|
64
64
|
this.name = "NeedsJspi";
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -68,8 +68,32 @@ export function needsJspi(what) {
|
|
|
68
68
|
throw new NeedsJspi(what);
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Failure raised
|
|
72
|
-
*
|
|
71
|
+
* Failure raised when a synchronous entry into an instance would race a
|
|
72
|
+
* pending lift (contracts/embedder-api.md §"Functions and async",
|
|
73
|
+
* failure-ladder arm 2).
|
|
74
|
+
*
|
|
75
|
+
* In jspi mode a promising-wrapped entry settles through a microtask hop even
|
|
76
|
+
* when nothing suspended, and the hop-quiescence gate (exec/boundary.ts)
|
|
77
|
+
* defers Promise-surface calls that would enter during that window. A
|
|
78
|
+
* synchronous caller — a resource constructor, or the embedder's `sync()`
|
|
79
|
+
* adapter — cannot be deferred, so it refuses instead.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately *not* a `Trap`, and deliberately raised BEFORE the instance is
|
|
82
|
+
* entered: nothing was entered, so there is nothing to poison. The refusal is
|
|
83
|
+
* transient — the instance stays enterable, and the call succeeds on retry
|
|
84
|
+
* once the in-flight activity settles, or immediately through the
|
|
85
|
+
* Promise-shaped surface, which defers rather than refusing.
|
|
86
|
+
*/
|
|
87
|
+
export class SyncEntryBusy extends Error {
|
|
88
|
+
constructor(what) {
|
|
89
|
+
super(`sync entry refused: ${what} (the component instance has activity in ` +
|
|
90
|
+
`flight; retry once it settles, or use the Promise-shaped call)`);
|
|
91
|
+
this.name = "SyncEntryBusy";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Failure raised where a not-yet-implemented capability is required. Same
|
|
96
|
+
* rationale as `NeedsJspi`: never a `Trap`.
|
|
73
97
|
*/
|
|
74
98
|
export class PendingCapability extends Error {
|
|
75
99
|
constructor(what) {
|
|
@@ -114,23 +138,6 @@ const poisonedInstances = new WeakMap();
|
|
|
114
138
|
export function isInstancePoisoned(inst) {
|
|
115
139
|
return poisonedInstances.has(inst);
|
|
116
140
|
}
|
|
117
|
-
/**
|
|
118
|
-
* May a settled activation tail for parked thread `t` be DISPATCHED now
|
|
119
|
-
* (issue #156)? True iff its instance is host-enterable — `Thread.resumeWith`
|
|
120
|
-
* brackets the resumption with `enterFrom(null)` — or POISONED, in which case
|
|
121
|
-
* `resumeWith`'s early return retires it and deferring would leak forever.
|
|
122
|
-
*
|
|
123
|
-
* CONTRACT: a parked entry without a reachable `task.inst` (the partial
|
|
124
|
-
* thread doubles the host-pump tests park in `Store.awaiting`) holds no
|
|
125
|
-
* reentrance state, so there is nothing to defer on: dispatchable.
|
|
126
|
-
*/
|
|
127
|
-
// deno-lint-ignore no-explicit-any
|
|
128
|
-
export function dispatchableTail(t) {
|
|
129
|
-
const inst = t?.task?.inst;
|
|
130
|
-
if (inst === undefined || inst === null)
|
|
131
|
-
return true;
|
|
132
|
-
return isInstancePoisoned(inst) || inst.mayEnterFrom(null);
|
|
133
|
-
}
|
|
134
141
|
/**
|
|
135
142
|
* The recorded cause of an instance's poisoning: the original trap that
|
|
136
143
|
* broke the enter/leave bracket (polyengine#145). `undefined` when the instance
|
|
@@ -142,12 +149,12 @@ export function instancePoisonCause(inst) {
|
|
|
142
149
|
}
|
|
143
150
|
/**
|
|
144
151
|
* Append the recorded poison cause to an entry-refusal trap message
|
|
145
|
-
* (polyengine#145 ask 1). "cannot enter component instance"
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* the
|
|
152
|
+
* (polyengine#145 ask 1). "cannot enter component instance" has exactly one
|
|
153
|
+
* cause — a permanently poisoned instance, the corpse of an earlier trap —
|
|
154
|
+
* and this
|
|
155
|
+
* suffix names the trap that made it one. The call is kept unconditional at
|
|
156
|
+
* the refusal sites (returning `base` unchanged for an unmarked instance) so
|
|
157
|
+
* the message construction stays in one place; the suffix is
|
|
151
158
|
* conformance-safe because the official suite matches trap messages by
|
|
152
159
|
* substring (harness/src/runner.ts).
|
|
153
160
|
*/
|
|
@@ -157,6 +164,34 @@ export function withPoisonCause(inst, base) {
|
|
|
157
164
|
const cause = describeCause(poisonedInstances.get(inst));
|
|
158
165
|
return `${base} — instance poisoned by: ${cause}`;
|
|
159
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The entry-refusal decision, in one place: may `caller` enter `callee` right
|
|
169
|
+
* now, and if not, what does the refusal trap say? Returns `null` when entry
|
|
170
|
+
* is allowed, otherwise the exact trap message for `base`.
|
|
171
|
+
*
|
|
172
|
+
* POISONING IS THE WHOLE MECHANISM (CM#705). There is no transient
|
|
173
|
+
* reentrance gate: at the pinned reference (definitions.py @ 2f13265)
|
|
174
|
+
* `may_enter`, `entering_set`, `enter_from`, `leave_to` and
|
|
175
|
+
* `ComponentInstance.parent` do not exist — `Store.lift` runs `canon_lift`
|
|
176
|
+
* with no gate at all, so host-mediated reentrance into a live instance is
|
|
177
|
+
* simply VALID.
|
|
178
|
+
*
|
|
179
|
+
* Against that, per-instance poisoning is polyengine's NAMED DIVERGENCE. A
|
|
180
|
+
* trapped instance is a corpse — entry is refused permanently, with the
|
|
181
|
+
* recorded cause appended (polyengine#145 ask 1) — where wasmtime instead
|
|
182
|
+
* kills the whole store. The reference never faces the question because a
|
|
183
|
+
* trap there is the end of the world.
|
|
184
|
+
*
|
|
185
|
+
* The `caller !== callee` guard keeps a self-call out of the refusal: a dtor
|
|
186
|
+
* invoked from inside its own instance (cabi/handles.ts) is the live case —
|
|
187
|
+
* it must not be refused by its own instance's marker.
|
|
188
|
+
*/
|
|
189
|
+
export function entryRefusal(callee, caller, base) {
|
|
190
|
+
if (caller !== callee && isInstancePoisoned(callee)) {
|
|
191
|
+
return withPoisonCause(callee, base);
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
160
195
|
function describeCause(cause) {
|
|
161
196
|
try {
|
|
162
197
|
// String(err) renders "Name: message" — for a `Trap`, exactly the
|
|
@@ -357,9 +392,9 @@ function activationOf() {
|
|
|
357
392
|
* The opposite shape — A settles B's suspension so B runs AFTER A — is
|
|
358
393
|
* deliberately NOT represented here: `SuspensionPoint.resume` pushes only when
|
|
359
394
|
* nothing is currently running, so B never shadows A. B is picked up by its
|
|
360
|
-
* own first `Suspending` call.
|
|
361
|
-
*
|
|
362
|
-
*
|
|
395
|
+
* own first `Suspending` call. A driver's settle-time claim would name B
|
|
396
|
+
* here, which is exactly why it is not an ambient tier — see `resolveAmbient`
|
|
397
|
+
* and `Store.pendingResumptions`.
|
|
363
398
|
*
|
|
364
399
|
* An activation leaves this stack when it parks again
|
|
365
400
|
* (`blockCurrentActivation`) or finishes (its `awaitValue` promise settles —
|
|
@@ -453,14 +488,11 @@ export function releaseActivationAmbient(t) {
|
|
|
453
488
|
// The resumed-but-not-yet-run gate (a SEPARATE concern from the ambient above)
|
|
454
489
|
// ---------------------------------------------------------------------------
|
|
455
490
|
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// per-Store SET semantics, not a global identity slot — see
|
|
462
|
-
// `Store.pendingResumptions` below, and `resolveAmbient` for the retirement
|
|
463
|
-
// evidence.
|
|
491
|
+
// The gate answers one question for the DRIVER: "was a suspension settled
|
|
492
|
+
// whose activation has not run yet — must I refrain from scheduling anything
|
|
493
|
+
// else?" That is per-Store SET semantics, not a global identity slot, and it
|
|
494
|
+
// is not an input to ambient resolution: see `Store.pendingResumptions`
|
|
495
|
+
// below.
|
|
464
496
|
const AMBIENT_TRACE = (() => {
|
|
465
497
|
try {
|
|
466
498
|
return Deno.env.get("CE_AMBIENT_TRACE") === "1";
|
|
@@ -498,48 +530,38 @@ export function ambientResidue() {
|
|
|
498
530
|
* is running outside our frames (a `Suspending` hop or a resumption).
|
|
499
531
|
* LIFO, because activations nest: an outer activation's built-in can
|
|
500
532
|
* synchronously enter an inner one's wasm.
|
|
501
|
-
* (There is no tier 3. A third tier — `resumingThread`, the driver's
|
|
502
|
-
* settle-time claim — existed from M3A-1 until 2026-08-22 and was
|
|
503
|
-
* RETIRED, see below.)
|
|
504
533
|
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* over 1395 commands), and the corpus pins the result.
|
|
534
|
+
* What tiers 1+2 state directly is "the innermost wasm activation currently
|
|
535
|
+
* executing, across the engine's hops and resumptions" -- the same quantity
|
|
536
|
+
* an async-context store written around the wasm entry would carry, without
|
|
537
|
+
* depending on the engine to restore it on every continuation captured inside
|
|
538
|
+
* that extent (M3A-1). The equivalence is not asserted from the armchair: it
|
|
539
|
+
* was established differentially, against such a store, over the whole
|
|
540
|
+
* conformance corpus, comparing at every read (zero disagreements over 1395
|
|
541
|
+
* commands), and the corpus pins the result.
|
|
514
542
|
*
|
|
515
|
-
* Having TWO readers with different orders is not a hypothetical
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* (`exit-sync-call with an empty sync-call stack`)
|
|
520
|
-
*
|
|
521
|
-
* Do not add a third reader; extend this one. (`activationOf` above is not a
|
|
543
|
+
* Having TWO readers with different precedence orders is not a hypothetical
|
|
544
|
+
* hazard: they disagree silently at exactly the sites that matter -- the FACT
|
|
545
|
+
* bracket sites read `maybeCurrentThread`, so a divergent order there
|
|
546
|
+
* attributes the bracket to the driver's claim instead of its own activation
|
|
547
|
+
* (`exit-sync-call with an empty sync-call stack`), and a precedence fix
|
|
548
|
+
* applied to the other reader measures as "no change" because the failing
|
|
549
|
+
* sites never call it. Do not add a third reader; extend this one. (`activationOf` above is not a
|
|
522
550
|
* second reader -- it answers a different question, "whose wasm frame are we
|
|
523
551
|
* running on behalf of", and is used only by
|
|
524
552
|
* `Store.consumePendingIfRunning`.)
|
|
525
553
|
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* sched-seeds, the shells (sm + node + jsc + bun, all "OK, matches
|
|
538
|
-
* expectation"), the browsers (chromium + firefox), smoke-tls and smoke-c0.
|
|
539
|
-
* The reading: post-#24 the sentinel discipline (tier 2's claim/release edges)
|
|
540
|
-
* always answers first, so the slot's attribution role was vestigial. Its
|
|
541
|
-
* other, live role -- the scheduling gate -- survives as the per-Store
|
|
542
|
-
* `Store.pendingResumptions` set.
|
|
554
|
+
* TWO TIERS ARE ENOUGH, and specifically a driver's settle-time claim is NOT
|
|
555
|
+
* a third: such a claim names whichever activation was settled or claimed
|
|
556
|
+
* across an await -- right for that one and wrong for every other in-flight
|
|
557
|
+
* activation. It is not needed, because the sentinel discipline (tier 2's
|
|
558
|
+
* claim/release edges, #24) always answers first: instrumented reads where
|
|
559
|
+
* tiers 1-2 were empty and a settle-time claim was live decided NOTHING
|
|
560
|
+
* across the conformance corpus, both seeded shuffles
|
|
561
|
+
* (`POLYENGINE_SCHED_SEED` 1 and 4242), test-runtime and the smoke-tls
|
|
562
|
+
* three-async-component #24 corpus. A driver's settle-time claim is a
|
|
563
|
+
* SCHEDULING gate only, and lives as the per-Store `Store.pendingResumptions`
|
|
564
|
+
* set (issue #158).
|
|
543
565
|
*/
|
|
544
566
|
function resolveAmbient() {
|
|
545
567
|
return threadStack[threadStack.length - 1] ??
|
|
@@ -703,22 +725,18 @@ export class Store {
|
|
|
703
725
|
* wedges the loops, because an activation that merely hopped legitimately
|
|
704
726
|
* holds an ambient while the scheduler is free to proceed.
|
|
705
727
|
*
|
|
706
|
-
* PER-STORE and MULTI-ENTRY
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* tier-3 attribution unambiguity, which no longer exists (see
|
|
715
|
-
* `resolveAmbient`), so it is gone with the slot; the entries and their
|
|
716
|
-
* release edges are otherwise unchanged, per entry.
|
|
728
|
+
* PER-STORE and MULTI-ENTRY (issues #158 mechanism B, #210), both load
|
|
729
|
+
* bearing. MULTI-ENTRY because two engine resumptions can legitimately be
|
|
730
|
+
* pending at once: a running activation X may deliver a resume to Z while
|
|
731
|
+
* Y's resumption is still outstanding, and a one-claimant gate cannot
|
|
732
|
+
* represent that. PER-STORE because a claim held store-wide makes every
|
|
733
|
+
* driver on EVERY store yield: an idle store's `driveStoreAsync` dies at
|
|
734
|
+
* the 10,000-hop assert (~311ms) while another store merely dwells on a
|
|
735
|
+
* slow host import.
|
|
717
736
|
*
|
|
718
737
|
* Cross-store de-serialization is safe by disjointness: an activation
|
|
719
|
-
* belongs to exactly one store. Same-store
|
|
720
|
-
*
|
|
721
|
-
* died, rather than crashing on the second.
|
|
738
|
+
* belongs to exactly one store. Same-store the set is conservative — the
|
|
739
|
+
* gate keeps refusing until EVERY pending entry has died.
|
|
722
740
|
*
|
|
723
741
|
* Release edges, per entry: the activation PARKS again
|
|
724
742
|
* (`blockCurrentActivation` -> `consumePendingIfRunning`), it FINISHES (its
|
|
@@ -860,30 +878,18 @@ export class Store {
|
|
|
860
878
|
* throw (trap unwinding); callers propagate or park it exactly as they do
|
|
861
879
|
* for `tick`.
|
|
862
880
|
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
* `resumeWith` brackets the resumption with `enterFrom(null)`, and under
|
|
866
|
-
* the shared synthetic per-instantiation root a host entry into ANY
|
|
867
|
-
* instance of the graph locks the root, so while one instance is entered a
|
|
868
|
-
* sibling's tail cannot be dispatched: dispatching it tripped
|
|
869
|
-
* `resumeWith`'s enterability assert (which, mutating before asserting,
|
|
870
|
-
* also stranded the thread and lost the settle).
|
|
881
|
+
* Every non-stale tail is dispatched immediately, in queue order: there is
|
|
882
|
+
* no enterability condition to defer on (CM#705).
|
|
871
883
|
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
* phantom-state gate
|
|
876
|
-
*
|
|
884
|
+
* The ordering discipline is therefore settle order, full stop — and it is
|
|
885
|
+
* the reason this queue exists rather than a direct resumption from the
|
|
886
|
+
* settle continuation: in definitions.py the tail runs atomically inside
|
|
887
|
+
* the entered bracket, so the phantom-state gate (`tick` refuses while an
|
|
888
|
+
* unserviced tail is queued, see `hasServiceableSettled`) is what keeps a
|
|
889
|
+
* parked activation's tail from being observed out of order.
|
|
877
890
|
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
* conforming schedule nondeterminism: in definitions.py the tail runs
|
|
881
|
-
* atomically inside the entered bracket, so a host entry admitted during a
|
|
882
|
-
* park necessarily orders before the parked activation's tail there.
|
|
883
|
-
*
|
|
884
|
-
* A POISONED instance's tail is still dispatched: `resumeWith`'s poison
|
|
885
|
-
* early-return retires it, and deferring it would leak forever — a
|
|
886
|
-
* poisoned leaf keeps its lock permanently.
|
|
891
|
+
* A POISONED instance's tail is dispatched like any other: `resumeWith`'s
|
|
892
|
+
* poison early-return retires it, so it drains rather than leaking.
|
|
887
893
|
*/
|
|
888
894
|
serviceSettled() {
|
|
889
895
|
let did = false;
|
|
@@ -899,8 +905,6 @@ export class Store {
|
|
|
899
905
|
this.settled.splice(i, 1);
|
|
900
906
|
continue scan;
|
|
901
907
|
}
|
|
902
|
-
if (!dispatchableTail(s.t))
|
|
903
|
-
continue;
|
|
904
908
|
this.settled.splice(i, 1);
|
|
905
909
|
s.t.resumeWith(s.value, s.failure);
|
|
906
910
|
did = true;
|
|
@@ -911,19 +915,16 @@ export class Store {
|
|
|
911
915
|
}
|
|
912
916
|
}
|
|
913
917
|
/**
|
|
914
|
-
* "Would a `serviceSettled` call make progress right now?" — i.e.
|
|
915
|
-
* entry
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
+
* "Would a `serviceSettled` call make progress right now?" — i.e. is any
|
|
919
|
+
* entry queued at all. Every entry either dispatches or is dropped as
|
|
920
|
+
* stale, so a non-empty queue always makes progress.
|
|
921
|
+
*
|
|
922
|
+
* It exists to gate `tick` (and to keep the driving loops from parking)
|
|
923
|
+
* behind unserviced tails: resuming some other thread while a settled tail
|
|
924
|
+
* waits would expose the out-of-order state the queue is there to prevent.
|
|
918
925
|
*/
|
|
919
926
|
hasServiceableSettled() {
|
|
920
|
-
|
|
921
|
-
if (!this.awaiting.has(s.t))
|
|
922
|
-
return true;
|
|
923
|
-
if (dispatchableTail(s.t))
|
|
924
|
-
return true;
|
|
925
|
-
}
|
|
926
|
-
return false;
|
|
927
|
+
return this.settled.length > 0;
|
|
927
928
|
}
|
|
928
929
|
/**
|
|
929
930
|
* "Does component instance `inst` still have runnable work?" — the
|
|
@@ -997,9 +998,9 @@ export class Store {
|
|
|
997
998
|
return false;
|
|
998
999
|
}
|
|
999
1000
|
/**
|
|
1000
|
-
* definitions.py `Store.tick` (
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1001
|
+
* definitions.py `Store.tick` (@ 2f13265): resume one ready thread. There
|
|
1002
|
+
* is no bracket and no gate — the reference body is exactly "pick a ready
|
|
1003
|
+
* thread, resume it" (CM#705).
|
|
1003
1004
|
*
|
|
1004
1005
|
* Returns false when no thread was ready, so callers can distinguish
|
|
1005
1006
|
* "made progress" from "stuck" without inspecting the queue themselves.
|
|
@@ -1024,83 +1025,51 @@ export class Store {
|
|
|
1024
1025
|
// Same discipline, other edge: a settled-but-unserviced activation tail
|
|
1025
1026
|
// (see `settled`) is mid-"atomic resume" from the reference's point of
|
|
1026
1027
|
// view; scheduling anything before servicing it acts on phantom state.
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
// because its instance is self-excluded from the candidate set by the
|
|
1032
|
-
// enterability filter below — the same predicate on the same instance —
|
|
1033
|
-
// so no thread of that instance can be resumed while its tail waits.
|
|
1028
|
+
// That is settle-order discipline and has nothing to do with reentrance.
|
|
1029
|
+
// `hasServiceableSettled` (rather than
|
|
1030
|
+
// "queue non-empty") only because a tail whose thread was already resumed
|
|
1031
|
+
// elsewhere must not wedge the store.
|
|
1034
1032
|
if (this.hasServiceableSettled())
|
|
1035
1033
|
return false;
|
|
1036
|
-
// Ready is
|
|
1037
|
-
//
|
|
1038
|
-
// thread
|
|
1039
|
-
//
|
|
1034
|
+
// Ready is sufficient — almost. Nothing filters this set for reentrance:
|
|
1035
|
+
// at the pinned reference (definitions.py @ 2f13265) `Store.tick` resumes
|
|
1036
|
+
// any ready thread with no gate and no bracket (CM#705), so a sibling
|
|
1037
|
+
// instance's thread going ready while another instance is entered from
|
|
1038
|
+
// the host is simply resumable.
|
|
1040
1039
|
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
// instance in the graph is host-enterable. A sibling instance whose
|
|
1047
|
-
// thread goes ready in that window (event-driven wakeups do this on every
|
|
1048
|
-
// clock turn) would then trip the assertion, and the failure escapes
|
|
1049
|
-
// through whatever host-import promise is in flight.
|
|
1050
|
-
//
|
|
1051
|
-
// So "ready but not enterable" is treated as no progress, exactly as the
|
|
1052
|
-
// sync driving loop already does by restricting its candidate set to the
|
|
1053
|
-
// callee instance (`driveSyncLift` below; definitions.py `canon_lift`).
|
|
1054
|
-
// This cannot livelock: the entered call's host import settles from host
|
|
1055
|
-
// JS independently of `tick`, and when that call returns, `leaveTo(null)`
|
|
1056
|
-
// unlocks the root and the skipped threads run on the next turn.
|
|
1057
|
-
const candidates = this.readyCandidates().filter((t) => t.task.inst.mayEnterFrom(null));
|
|
1040
|
+
// What is added is polyengine's per-instance poisoning divergence: a
|
|
1041
|
+
// poisoned instance is a corpse, its threads must never resume, and the
|
|
1042
|
+
// MARKER is the whole test. `Thread.resumeWith` makes the same call on
|
|
1043
|
+
// the tail path.
|
|
1044
|
+
const candidates = this.readyCandidates().filter((t) => !isInstancePoisoned(t.task.inst));
|
|
1058
1045
|
if (candidates.length === 0)
|
|
1059
1046
|
return false;
|
|
1060
1047
|
const thread = chooseCandidate(candidates);
|
|
1061
1048
|
const inst = thread.task.inst;
|
|
1062
|
-
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
// Capability signals are the exception, for the same reason as there: a
|
|
1070
|
-
// `NeedsJspi`/`PendingCapability` marks an operation this runtime cannot
|
|
1071
|
-
// perform, not a component fault. In the reference that operation blocks
|
|
1072
|
-
// and then completes, so `leave_to` *is* reached and the instance stays
|
|
1073
|
-
// enterable — poisoning here would turn one unsupported operation into a
|
|
1074
|
-
// permanently dead instance.
|
|
1049
|
+
// A trap out of the resumption poisons the instance (polyengine's named
|
|
1050
|
+
// divergence: a per-instance corpse where wasmtime kills the whole store).
|
|
1051
|
+
// Capability signals are the exception: a `NeedsJspi`/`PendingCapability`
|
|
1052
|
+
// marks an operation this runtime cannot perform, not a component fault —
|
|
1053
|
+
// in the reference that operation blocks and then completes, so poisoning
|
|
1054
|
+
// here would turn one unsupported operation into a permanently dead
|
|
1055
|
+
// instance.
|
|
1075
1056
|
try {
|
|
1076
1057
|
thread.resume();
|
|
1077
1058
|
}
|
|
1078
1059
|
catch (e) {
|
|
1079
|
-
if (e instanceof NeedsJspi
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
else {
|
|
1083
|
-
// The bracket stays broken (instance poisoned, comment above), so
|
|
1084
|
-
// its live stream/future ends can never rendezvous again — retire
|
|
1085
|
-
// them so parked host peers settle instead of hanging (#66).
|
|
1086
|
-
//
|
|
1087
|
-
// The synthetic root (plan v3 amendment 4) is released, though: it is
|
|
1088
|
-
// in this entry's entering set but must not turn per-instance
|
|
1089
|
-
// poisoning into store-wide poisoning. See
|
|
1090
|
-
// `ComponentInstanceState.releaseSyntheticRootOnPoison`.
|
|
1060
|
+
if (!(e instanceof NeedsJspi) && !(e instanceof PendingCapability)) {
|
|
1061
|
+
// Poisoned: its live stream/future ends can never rendezvous again —
|
|
1062
|
+
// retire them so parked host peers settle instead of hanging (#66).
|
|
1091
1063
|
//
|
|
1092
1064
|
// Routed through `notifyInstancePoisoned` (not the raw hook) so the
|
|
1093
1065
|
// poison MARKER is recorded too (polyengine#145): `Thread.resumeWith`'s
|
|
1094
|
-
// quiet-retire of late settled tails and `
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
// assert or defer forever.
|
|
1098
|
-
inst.releaseSyntheticRootOnPoison?.();
|
|
1066
|
+
// quiet-retire of late settled tails (#156) and `entryRefusal` both
|
|
1067
|
+
// read it; without the marker a settled tail of this dead instance
|
|
1068
|
+
// would be resumed as if healthy.
|
|
1099
1069
|
notifyInstancePoisoned(inst, e);
|
|
1100
1070
|
}
|
|
1101
1071
|
throw e;
|
|
1102
1072
|
}
|
|
1103
|
-
inst.leaveTo(null);
|
|
1104
1073
|
return true;
|
|
1105
1074
|
}
|
|
1106
1075
|
}
|