@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.
Files changed (81) hide show
  1. package/esm/cabi/async_values.js +6 -5
  2. package/esm/cabi/bulk_lists.js +0 -5
  3. package/esm/cabi/context.js +13 -3
  4. package/esm/cabi/flatten.js +41 -9
  5. package/esm/cabi/handles.js +57 -54
  6. package/esm/cabi/layout.js +113 -52
  7. package/esm/cabi/load.js +31 -23
  8. package/esm/cabi/store.js +33 -26
  9. package/esm/cabi/trap.js +2 -2
  10. package/esm/cabi/types.js +138 -25
  11. package/esm/cabi/values.js +25 -7
  12. package/esm/cache/core.js +2 -11
  13. package/esm/digest/digest.js +10 -8
  14. package/esm/digest/mod.js +1 -1
  15. package/esm/digest/verify.js +6 -86
  16. package/esm/embedder/casing.js +24 -9
  17. package/esm/embedder/copy.js +6 -6
  18. package/esm/embedder/errors.js +2 -2
  19. package/esm/embedder/imports.js +3 -3
  20. package/esm/embedder/instantiate.js +132 -37
  21. package/esm/embedder/mod.js +9 -8
  22. package/esm/embedder/resources.js +39 -16
  23. package/esm/embedder/streams.js +36 -37
  24. package/esm/embedder/sync.js +242 -0
  25. package/esm/embedder/values.js +84 -22
  26. package/esm/embedder/version.js +9 -9
  27. package/esm/exec/boundary.js +123 -161
  28. package/esm/exec/executor.js +37 -25
  29. package/esm/exec/host_streams.js +31 -31
  30. package/esm/intrinsics/async_builtins.js +15 -7
  31. package/esm/intrinsics/context.js +1 -1
  32. package/esm/intrinsics/errors.js +9 -9
  33. package/esm/intrinsics/fact_calls.js +37 -49
  34. package/esm/intrinsics/mod.js +54 -117
  35. package/esm/intrinsics/stream_builtins.js +2 -2
  36. package/esm/intrinsics/transcode.js +1 -1
  37. package/esm/jspi/bridge.js +4 -3
  38. package/esm/jspi/suspending.js +5 -5
  39. package/esm/plan/loader.js +5 -5
  40. package/esm/shim/translator.js +2 -2
  41. package/esm/task/mod.js +45 -182
  42. package/esm/task/scheduler.js +154 -185
  43. package/esm/task/streams.js +39 -54
  44. package/esm/task/subtask.js +2 -2
  45. package/esm/task/thread.js +20 -41
  46. package/esm/task/waitable.js +0 -1
  47. package/package.json +2 -2
  48. package/types/cabi/async_values.d.ts +3 -2
  49. package/types/cabi/bulk_lists.d.ts +0 -2
  50. package/types/cabi/context.d.ts +15 -5
  51. package/types/cabi/flatten.d.ts +2 -2
  52. package/types/cabi/handles.d.ts +15 -26
  53. package/types/cabi/layout.d.ts +22 -1
  54. package/types/cabi/load.d.ts +10 -2
  55. package/types/cabi/store.d.ts +4 -2
  56. package/types/cabi/types.d.ts +22 -3
  57. package/types/digest/mod.d.ts +1 -1
  58. package/types/digest/verify.d.ts +3 -19
  59. package/types/embedder/casing.d.ts +9 -1
  60. package/types/embedder/copy.d.ts +4 -4
  61. package/types/embedder/instantiate.d.ts +4 -4
  62. package/types/embedder/mod.d.ts +3 -2
  63. package/types/embedder/resources.d.ts +20 -7
  64. package/types/embedder/streams.d.ts +5 -6
  65. package/types/embedder/sync.d.ts +81 -0
  66. package/types/embedder/values.d.ts +2 -2
  67. package/types/exec/boundary.d.ts +55 -44
  68. package/types/exec/executor.d.ts +3 -2
  69. package/types/exec/host_streams.d.ts +8 -8
  70. package/types/intrinsics/errors.d.ts +3 -3
  71. package/types/intrinsics/mod.d.ts +1 -1
  72. package/types/intrinsics/stream_builtins.d.ts +2 -2
  73. package/types/jspi/bridge.d.ts +6 -5
  74. package/types/plan/format.d.ts +11 -10
  75. package/types/plan/loader.d.ts +2 -2
  76. package/types/shim/translator.d.ts +2 -2
  77. package/types/task/mod.d.ts +26 -97
  78. package/types/task/scheduler.d.ts +81 -62
  79. package/types/task/streams.d.ts +23 -38
  80. package/types/task/subtask.d.ts +2 -2
  81. package/types/task/waitable.d.ts +0 -1
@@ -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 and is M2 phase 3; those sites fail loudly
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 (M2 phase 3): ${what}`);
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 where a capability scheduled for a later M2 phase is
72
- * required. Same rationale as `NeedsJspi`: never a `Trap`.
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" covers two states
146
- * that send an embedder down entirely different debugging paths a
147
- * transient reentrance overlap (retry later, look for caller-side call
148
- * overlap) and a permanently poisoned instance (the corpse of an earlier
149
- * trap, which this suffix names). Only the poisoned case gets the suffix:
150
- * the transient message stays byte-identical, and the suffix is
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. (Until 2026-08-22 a third ambient tier — the
361
- * driver's `resumingThread` slot also named B here; it was retired with the
362
- * slot, see `resolveAmbient` and `Store.pendingResumptions`.)
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
- // This used to be a module-global single slot, `resumingThread`, doing two
457
- // jobs: (1) the DRIVER's scheduling gate ("a suspension was settled and its
458
- // activation has not run yet do not schedule anything else"), and (2) tier 3
459
- // of ambient resolution. Job (2) was retired on 2026-08-22 (issue #158) after
460
- // measurement showed it never decided a read; job (1) is real, but it is
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
- * Tier 2 replaced an async-context store (M3A-1). The store held
506
- * precisely "the innermost wasm activation currently executing, across the
507
- * engine's hops and resumptions", because it was written by `withActivation`
508
- * around the wasm entry and the engine restored it on every continuation it
509
- * had captured inside that extent. Tiers 1+2 now state that directly. The
510
- * equivalence is not asserted from the armchair: it was established
511
- * differentially, by running the whole conformance corpus with both the store
512
- * and this queue live and comparing them at every read (zero disagreements
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 hazard: for
516
- * two rounds `currentThread` used store-first while `maybeCurrentThread` still
517
- * used slot-first, and since the FACT bracket sites read the latter, the
518
- * bracket was attributed to the driver's claim instead of its own activation
519
- * (`exit-sync-call with an empty sync-call stack`). Fixing the precedence in
520
- * one reader measured as "no change" because the failing sites used the other.
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
- * TIER 3 RETIRED, 2026-08-22 (issue #158). The bottom tier used to be
527
- * `resumingThread`, the driver's settle-time claim -- a last resort that named
528
- * whichever activation was settled or claimed across an await, right for that
529
- * one and wrong for every other in-flight activation. It was removed on the
530
- * strength of a re-run of the M3A-1 differential methodology: an instrumented
531
- * build counted every read where tiers 1-2 were empty and the slot was live,
532
- * and measured ZERO deciding reads across the conformance corpus (FIFO,
533
- * 1257/0), both seeded shuffles (`POLYENGINE_SCHED_SEED` 1 and 4242),
534
- * test-runtime (all jspi pins), and the smoke-tls three-async-component #24
535
- * corpus. A removal build then ran green on every engine lane we have:
536
- * test-runtime, test-protocol, conformance (1257/0, no expectation changes),
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 since 2026-08-22 (issues #158 mechanism B,
707
- * #210). It was one module-global slot with a one-claimant assert, which
708
- * (a) could not represent two legitimately-pending engine resumptions a
709
- * running activation X delivering a resume to Z while Y's resumption was
710
- * still pending crashed on the assert and (b) made every driver on every
711
- * store yield while ANY store held a claim, so an idle store's
712
- * `driveStoreAsync` died at the 10,000-hop assert (~311ms) while another
713
- * store merely dwelt on a slow host import. The assert's invariant was
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 it is strictly more conservative
720
- * than the old slot — the gate keeps refusing until EVERY pending entry has
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
- * A tail whose instance is NOT host-enterable is DEFERRED IN PLACE — left
864
- * in the queue, skipped here until the lock releases (issue #156).
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
- * Deferral is safe because `!inst.mayEnterFrom(null)` is EXACTLY `tick`'s
873
- * candidate-filter predicate on the same instance: while a tail of `inst`
874
- * is deferred, `tick` cannot resume any thread of `inst` either, so the
875
- * phantom-state gate the queue exists to enforce is preserved per-instance
876
- * by construction.
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
- * The ordering discipline is therefore per-instance settle order. Cross-
879
- * instance order relaxes only when enterability defers a tail, which is
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. some
915
- * entry is stale (would be removed) or serviceable (would be dispatched).
916
- * A queue holding ONLY deferred tails (issue #156) answers false: `tick`
917
- * must not be gated by them, and the driving loops must not spin on them.
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
- for (const s of this.settled) {
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` (line 597): resume one ready thread, bracketed
1001
- * by the reentrance gate for a host-initiated entry (`enter_from(None)` /
1002
- * `leave_to(None)`).
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
- // Only a SERVICEABLE tail gates: a tail DEFERRED on a non-enterable
1029
- // instance (issue #156) cannot be dispatched now, and gating on it would
1030
- // wedge the store (and hot-spin the drivers). It does not need to gate,
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 not sufficient: the thread's instance must also be enterable
1037
- // from the host. The reference *asserts* this in `Store.tick` — a waiting
1038
- // thread's instance is always re-enterable there, because its host entry
1039
- // has either left or is itself a waiting thread. That does not hold here.
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
- // Instances of one linked graph share a Store and, with it, the synthetic
1042
- // per-instantiation root (plan v3 amendment 4): `enterFrom(null)` locks
1043
- // the callee AND the root, so while ANY instance is entered from the host
1044
- // e.g. a sync export parked on an async host import, which in this
1045
- // runtime is a real suspension rather than a blocked OS thread — no
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
- inst.enterFrom(null);
1063
- // Deliberately NOT a `finally`: if the resumed thread traps, the reference
1064
- // never reaches `leave_to` either (definitions.py `Store.tick`, line 597,
1065
- // where a Trap propagates out of `thread.resume()`), so the instance stays
1066
- // locked the Component Model's instance poisoning. See the `poison`
1067
- // helper in exec/boundary.ts for the full rationale.
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 || e instanceof PendingCapability) {
1080
- inst.leaveTo(null);
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 `dispatchableTail`'s
1095
- // dispatch-or-defer decision (#156) both read it, and without the
1096
- // marker a settled tail of this instance would hit the backstop
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
  }