@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
@@ -142,7 +142,7 @@ export class GuestBuffer {
142
142
  }
143
143
  this.progress += vs.length;
144
144
  }
145
- // --- A21 direct-access byte edges (embedder-api amendment A21, #128) ---
145
+ // --- direct-access byte edges (embedder-api.md §"Streams and futures" (polyengine#128)) ---
146
146
  //
147
147
  // `ByteWindow`, implemented for the `stream<u8>` case only. The two methods
148
148
  // together are the copy `read`/`write` would have done, split so that the
@@ -184,7 +184,7 @@ function isDirectBuffer(b) {
184
184
  * Collapses to definitions.py's `dst_buffer.write(src_buffer.read(n))`
185
185
  * whenever neither side is a direct session — which is every guest↔guest,
186
186
  * guest↔host-chunk and host-chunk↔host-chunk rendezvous, i.e. everything
187
- * that existed before A21.
187
+ * that existed before direct-access byte edge.
188
188
  */
189
189
  function rendezvousCopy(src, dst, n) {
190
190
  const srcDirect = isDirectBuffer(src);
@@ -199,12 +199,12 @@ function rendezvousCopy(src, dst, n) {
199
199
  ? src.runDirect(dst, n)
200
200
  : dst.runDirect(src, n);
201
201
  }
202
- /** The A21 rejection for a rendezvous of two direct sessions. */
202
+ /** The rejection for a rendezvous of two direct-access sessions. */
203
203
  function bothDirectError() {
204
204
  return new TypeError("at least one side of a host-to-host rendezvous must use the chunk " +
205
205
  "forms: two direct-access sessions cannot rendezvous with each other " +
206
206
  "because neither side owns the memory the other would write into " +
207
- "(embedder-api amendment A21, polyengine#128)");
207
+ `(embedder-api.md §"Streams and futures" ("Direct-access byte edges"), polyengine#128)`);
208
208
  }
209
209
  /**
210
210
  * definitions.py `none_or_number_type` (line 1070). Guards the "temporary"
@@ -243,7 +243,7 @@ export class SharedStreamImpl {
243
243
  * OUT of a guest table. The receiver — the host, or the destination of a
244
244
  * guest-to-guest hop, in which case the immediately following lower fires
245
245
  * `onLowered` — may now act on the transferred end. Host wrappers use it to
246
- * re-arm their activity (#162, embedder-api amendment A15). Guest-owned
246
+ * re-arm their activity (#162, contracts/embedder-api.md §"Streams and futures"). Guest-owned
247
247
  * objects leave it unset.
248
248
  */
249
249
  onLifted = null;
@@ -261,9 +261,9 @@ export class SharedStreamImpl {
261
261
  pendingOnCopyDone = null;
262
262
  /**
263
263
  * Observers fired once, when this stream becomes dropped — by EITHER
264
- * side, including the A7 teardown walk (`dropSharedForTeardown`). The
264
+ * side, including the loud component fault teardown walk (`dropSharedForTeardown`). The
265
265
  * embedder's producer pump uses this to cancel a producer parked on an
266
- * external event (amendment A13's cancellation companion: an
266
+ * external event (§"Streams and futures"'s cancellation companion: an
267
267
  * accept-shaped producer holds a live platform resource while parked,
268
268
  * and the reader dropping is its only stop signal). `null` = already
269
269
  * fired.
@@ -336,7 +336,7 @@ export class SharedStreamImpl {
336
336
  if (this.pendingBuffer.remain() > 0) {
337
337
  if (dstBuffer.remain() > 0) {
338
338
  const n = Math.min(dstBuffer.remain(), this.pendingBuffer.remain());
339
- // A21 seam (#128). `"chunk"` is the reference line verbatim.
339
+ // direct-access byte edge seam (#128). `"chunk"` is the reference line verbatim.
340
340
  const pendingIsDirect = isDirectBuffer(this.pendingBuffer);
341
341
  const out = rendezvousCopy(this.pendingBuffer, dstBuffer, n);
342
342
  if (out === "both-direct") {
@@ -374,7 +374,7 @@ export class SharedStreamImpl {
374
374
  if (this.pendingBuffer.remain() > 0) {
375
375
  if (srcBuffer.remain() > 0) {
376
376
  const n = Math.min(srcBuffer.remain(), this.pendingBuffer.remain());
377
- // A21 seam (#128). `"chunk"` is the reference line verbatim.
377
+ // direct-access byte edge seam (#128). `"chunk"` is the reference line verbatim.
378
378
  const pendingIsDirect = isDirectBuffer(this.pendingBuffer);
379
379
  const out = rendezvousCopy(srcBuffer, this.pendingBuffer, n);
380
380
  if (out === "both-direct") {
@@ -404,7 +404,7 @@ export class SharedStreamImpl {
404
404
  }
405
405
  }
406
406
  /**
407
- * A21 (#128): route a rendezvous whose direct session did NOT copy.
407
+ * direct-access byte edge (#128): route a rendezvous whose direct session did NOT copy.
408
408
  *
409
409
  * Two outcomes land here, and both share one invariant: the peer's parked
410
410
  * operation survives, no event is delivered, and the stream is not dropped
@@ -471,7 +471,7 @@ export class SharedFutureImpl {
471
471
  * OUT of a guest table. The receiver — the host, or the destination of a
472
472
  * guest-to-guest hop, in which case the immediately following lower fires
473
473
  * `onLowered` — may now act on the transferred end. Host wrappers use it to
474
- * re-arm their activity (#162, embedder-api amendment A15). Guest-owned
474
+ * re-arm their activity (#162, contracts/embedder-api.md §"Streams and futures"). Guest-owned
475
475
  * objects leave it unset.
476
476
  */
477
477
  onLifted = null;
@@ -511,9 +511,9 @@ export class SharedFutureImpl {
511
511
  pendingOnCopyDone = null;
512
512
  /**
513
513
  * Observers fired once, when this future becomes dropped — by EITHER side,
514
- * including the A7 teardown walk (`dropSharedForTeardown`). Streams grew
515
- * this for A13 producer cancellation; futures need it as the release hook
516
- * for a host wrapper's activity arm (#162, amendment A15): a guest dropping
514
+ * including the loud component fault teardown walk (`dropSharedForTeardown`). Streams grew
515
+ * this for resource stream producer cancellation; futures need it as the release hook
516
+ * for a host wrapper's activity arm (#162, §"Streams and futures"): a guest dropping
517
517
  * its end with no host operation parked, and the `readResult()`
518
518
  * already-dropped fast path, both bypass every other close site. `null` =
519
519
  * already fired.
@@ -677,8 +677,8 @@ export class WritableFutureEnd extends CopyEnd {
677
677
  /**
678
678
  * Failures recorded against shared stream/future objects whose peer end died
679
679
  * inside a trap-poisoned instance's handle table. The embedder layer consults
680
- * this to reject host operations loudly (contracts/embedder-api.md amendment
681
- * A7) instead of letting them hang forever or fake a clean end-of-stream.
680
+ * this to reject host operations loudly (contracts/embedder-api.md
681
+ * §"Streams and futures") instead of letting them hang forever or fake a clean end-of-stream.
682
682
  */
683
683
  const poisonFailures = new WeakMap();
684
684
  /** The recorded poisoning failure for a shared stream/future value, if any. */
@@ -732,51 +732,35 @@ export function abandonSharedFuture(shared, reason) {
732
732
  * **poisoned** guest instance is retired silently via `resetPending`.
733
733
  * Notifying it would queue a phantom event into the corpse's waitables, and
734
734
  * a later driving loop servicing it would resume machinery whose instance
735
- * can no longer be entered (`tick` asserts enterability). Host sentinels are
735
+ * can no longer be entered (`tick` excludes poisoned instances). Host sentinels are
736
736
  * not instances at all, so they are always notified.
737
737
  *
738
- * #100: THE HEALTH TEST IS "POISONED", NOT "`mayEnter === false`". The
739
- * original test used non-enterability as a proxy for deadness. The proxy is
740
- * unsound in one direction, and the unsoundness stranded healthy tasks:
741
- *
742
- * * (sound half, #84 audit) a healthy guest peer always parks with
743
- * `mayEnter === true`. Every park the callback ABI's waitable-set wait,
744
- * and equally a sync-lowered/JSPI peer blocked inside `finishCopy`'s
745
- * SITE 4 via `blockCurrentActivation` — yields the thread out of the
746
- * scheduler's enter/leave bracket, and the bracket's `leaveTo` runs on the
747
- * way out (task/scheduler.ts `Store.tick` :905-917, task/thread.ts
748
- * `Thread.resumeWith` :157-179, whose resume-side
749
- * `assert_(mayEnterFrom(null))` would fire otherwise). Blocking inside a
750
- * wasm frame does NOT hold the enter bracket.
751
- * * (unsound converse) `mayEnter === false` does not imply "poisoned". An
752
- * instance that is merely mid-call is also non-enterable, and a CALLER
753
- * instance stays non-enterable for the whole duration of a
754
- * cross-component (FACT) call into an instance that traps
755
- * (`ComponentInstanceState.enterFrom` clears `mayEnter` on the callee's
756
- * entering set only, task/mod.ts). A *different*, healthy task of that
757
- * caller, parked on an end of a stream/future the trapping callee also
758
- * held, was classified dead here and retired silently — stranded, the
759
- * exact outcome #66 exists to prevent.
738
+ * #100: THE HEALTH TEST IS "POISONED", NOT "BUSY". Deadness must be judged
739
+ * by the poison marker and nothing weaker: any liveness proxy that also
740
+ * covers a merely mid-call instance is unsound in one direction and strands
741
+ * healthy tasks. Under such a proxy a caller mid cross-component (FACT) call
742
+ * into an instance that trapped would drag its *other*, healthy tasks down
743
+ * with it one parked on an end of a stream/future the trapping callee also
744
+ * held would be classified dead here and retired silently, the exact outcome
745
+ * #66 exists to prevent.
760
746
  *
761
747
  * So the test consults the poison marker itself. It is per-instance and
762
- * recorded at the single seam every bracket-break site routes through
748
+ * recorded at the single seam every poisoning site routes through
763
749
  * (`notifyInstancePoisoned`, task/scheduler.ts: exec/boundary.ts `poison`,
764
750
  * `Store.tick`, `Thread.resumeWith`, the FACT cross-component catches in
765
751
  * intrinsics/fact_calls.ts, and cabi/handles.ts's gated destructor call),
766
752
  * and it is recorded *before* the retirement walk runs, so an instance's own
767
753
  * parked ends still see it during its own walk. `retiredInstances` is
768
754
  * consulted alongside it because the walk is also reachable directly (it is
769
- * set at walk entry, so the two agree); neither ever contains the synthetic
770
- * per-instantiation root, which every poison site skips or releases (plan v3
771
- * amendment 4, `releaseSyntheticRootOnPoison`).
755
+ * set at walk entry, so the two agree).
772
756
  *
773
757
  * Why this does not re-open review B2 (phantom events into a corpse): the
774
758
  * concern is that a DROPPED event queued onto a waitable of an instance that
775
759
  * can never be entered again would be serviced by a later driving loop and
776
- * resume machinery whose `tick` asserts enterability. "Can never be entered
777
- * again" is precisely poisoning — a mid-call instance's `mayEnter` is
778
- * restored by its own `leaveTo` when the call returns, and its parked task
779
- * then resumes normally and consumes the event. The narrowed predicate
760
+ * resume machinery `tick` deliberately excludes. "Can never be entered
761
+ * again" is precisely poisoning — a merely mid-call instance is entirely
762
+ * ordinary, and its parked task resumes normally and consumes the event.
763
+ * The narrowed predicate
780
764
  * therefore excludes exactly the population B2 is about, and admits only
781
765
  * peers that will run again.
782
766
  *
@@ -798,8 +782,8 @@ export function dropSharedForTeardown(shared) {
798
782
  }
799
783
  // The drop observers also fire on the teardown path: a stream producer
800
784
  // parked behind a trap-poisoned reader must be cancelled the same as behind
801
- // a cleanly-dropped one (A13), and a host wrapper's activity arm must be
802
- // released the same way (#162, amendment A15). Both classes carry the
785
+ // a cleanly-dropped one, and a host wrapper's activity arm must be
786
+ // released the same way (#162, §"Streams and futures"). Both classes carry the
803
787
  // observer machinery, so this is unconditional.
804
788
  shared.notifyDropped();
805
789
  }
@@ -807,8 +791,9 @@ export function dropSharedForTeardown(shared) {
807
791
  * Retire every live stream/future end in a trap-poisoned instance's handle
808
792
  * table (#66).
809
793
  *
810
- * Rationale: after a trap breaks the enter/leave bracket, `mayEnter` stays
811
- * false forever, so no task of this instance can ever rendezvous again. Its
794
+ * Rationale: a trapped instance is a corpse (polyengine's per-instance
795
+ * poisoning divergence), so no task of this instance can ever rendezvous
796
+ * again. Its
812
797
  * table's `CopyEnd`s are therefore unreachable-forever — leaving their shared
813
798
  * objects live strands the peers: a parked HOST operation never settles (its
814
799
  * promise hangs), and a LATER host operation would "succeed" against the
@@ -816,7 +801,7 @@ export function dropSharedForTeardown(shared) {
816
801
  * Dropping the shared object now converts both into the spec-shaped DROPPED
817
802
  * outcome, and the recorded failure lets the embedder layer brand it.
818
803
  *
819
- * Called from every bracket-break site — exec/boundary.ts `poison()` (the
804
+ * Called from every poisoning site — exec/boundary.ts `poison()` (the
820
805
  * sync-lift path), scheduler.ts `Store.tick` and thread.ts
821
806
  * `Thread.resumeWith` (traps during a resumed thread), and the FACT
822
807
  * cross-component catches (intrinsics/fact_calls.ts, callee side) — with the
@@ -885,7 +870,7 @@ export function retireInstanceAsyncEnds(inst, cause) {
885
870
  if (failed)
886
871
  throw first;
887
872
  }
888
- // `Store.tick`'s bracket-break site reaches the walk through this seam (its
873
+ // `Store.tick`'s poisoning site reaches the walk through this seam (its
889
874
  // module cannot import ours — see `setOnInstancePoisoned`); the sync-lift
890
875
  // site (exec/boundary.ts `poison`) imports it directly.
891
876
  setOnInstancePoisoned(retireInstanceAsyncEnds);
@@ -902,7 +887,7 @@ export class ErrorContext {
902
887
  this.debugMessage = debugMessage;
903
888
  }
904
889
  }
905
- // A9 brand (contracts/embedder-api.md §"Module identity"): error-contexts are
890
+ // module identity brand (contracts/embedder-api.md §"Module identity"): error-contexts are
906
891
  // STATEFUL — they live in a component instance's handle table — so the brand
907
892
  // exists to make a foreign one diagnosable at the lowering sites, never
908
893
  // usable. Both this internal class and the embedder-facing wrapper
@@ -72,8 +72,8 @@ export class Subtask extends Waitable {
72
72
  /**
73
73
  * Release a never-delivered subtask's lenders after its call broke off a
74
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).
75
+ * an abandoned park (contracts/intrinsics.md §A's trap-unwind/lender-release
76
+ * obligation; the park legs are #102/#106).
77
77
  *
78
78
  * The reference has no analogue because it never resumes after a trap:
79
79
  * the store dies with the lent handles inside it. The resolution state
@@ -107,64 +107,43 @@ export class Thread {
107
107
  this.awaiting = null;
108
108
  this.#store.awaiting.delete(this);
109
109
  this.#state = "suspended";
110
- // The reentrance bracket, exactly as `Store.tick` puts around `resume()`.
110
+ // Not a bracketed resumption: post-CM#705 (definitions.py @ 2f13265)
111
+ // `Store.tick` resumes a ready thread with no enter/leave bracket at all,
112
+ // and this path — the same thread body, woken by a Promise instead of a
113
+ // ready-condition — matches it.
111
114
  //
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
115
+ // What the catch preserves is polyengine's per-instance poisoning, which
116
+ // must be MARKER-recorded here specifically: a trap delivered as an
117
+ // `awaitValue` rejection is how EVERY guest trap in a suspended
118
+ // activation arrives under jspi (pin (e)), and if it unwound silently the
119
+ // second call of `builtin-trap-poisons-instance.wast` would re-run the
120
+ // guest and report "cannot drop busy stream" where the suite demands the
124
121
  // poisoned-instance "cannot enter component instance".
125
122
  //
126
- // Capability signals release the lock, for the same reason as in `tick`:
123
+ // Capability signals do not poison, for the same reason as in `tick`:
127
124
  // they mark the RUNTIME incomplete, not the component faulted.
128
125
  const inst = this.task.inst;
129
126
  // A poisoned instance's parked segments never run again: this settle
130
127
  // 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.
128
+ // trapped (#66 retired the handle tables). Resuming would re-enter the
129
+ // corpse, and asserting turned one legible trap into an assert cascade
130
+ // (the classic double-fault shape: `list too long`, then this assert as second
131
+ // victim). Retire quietly: the abandoned call's own driver reports, via
132
+ // its deadlock trap naming the export.
137
133
  if (isInstancePoisoned(inst))
138
134
  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
135
  try {
150
136
  this.#resumeInternal(value, failure);
151
137
  }
152
138
  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();
139
+ if (!(e instanceof NeedsJspi) && !(e instanceof PendingCapability)) {
140
+ // Retire the poisoned table's stream/future ends so parked host peers
141
+ // settle instead of hanging (#66), and record the marker — the whole
142
+ // entry-refusal mechanism since #251's re-key.
163
143
  notifyInstancePoisoned(inst, e);
164
144
  }
165
145
  throw e;
166
146
  }
167
- inst.leaveTo(null);
168
147
  }
169
148
  resume(cancelled = CANCELLED_FALSE) {
170
149
  assert_(!this.running() && !this.done(), "resume() on a running or finished thread");
@@ -13,7 +13,6 @@ export var EventCode;
13
13
  EventCode[EventCode["FUTURE_WRITE"] = 5] = "FUTURE_WRITE";
14
14
  EventCode[EventCode["TASK_CANCELLED"] = 6] = "TASK_CANCELLED";
15
15
  })(EventCode || (EventCode = {}));
16
- export const NO_EVENT = [EventCode.NONE, 0, 0];
17
16
  /**
18
17
  * definitions.py `class Waitable` (line 767).
19
18
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polyengine/runtime",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A WebAssembly Component Model host for JavaScript engines: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, and embedder API.",
5
5
  "homepage": "https://github.com/polymorph-components/polyengine#readme",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "access": "public"
56
56
  },
57
57
  "dependencies": {
58
- "@polyengine/protocol": "^0.2.3"
58
+ "@polyengine/protocol": "^0.3.0"
59
59
  },
60
60
  "_generatedBy": "dnt@0.43.2"
61
61
  }
@@ -2,8 +2,9 @@ import type { LiftLowerContext } from "./context.js";
2
2
  import type { BorrowType, OwnType, ValType } from "./types.js";
3
3
  import { ErrorContext, type SharedBase } from "../task/streams.js";
4
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).
5
+ * Diagnostic for a handle-table entry that carries the error-context brand
6
+ * without being one of THIS copy's `ErrorContext`s (contracts/embedder-api.md
7
+ * §"Module identity").
7
8
  *
8
9
  * A backstop, deliberately: the embedder's lowering site (embedder/values.ts)
9
10
  * refuses a foreign error-context before it can ever reach a handle table, so
@@ -1,8 +1,6 @@
1
1
  import { type MemInst } from "./memory.js";
2
2
  import type { ComponentValue } from "./types.js";
3
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
4
  /**
7
5
  * Bulk lift of `length` elements of `kind` at `ptr`. Returns `null` when the
8
6
  * kind is not handled here (caller falls back to the per-element loop) —
@@ -23,13 +23,24 @@ export declare function mkCanonicalOptions(partial?: Partial<CanonicalOptions>):
23
23
  export declare function requireMemory(opts: LiftOptions): MemInst;
24
24
  /**
25
25
  * Minimal component-instance stand-in for the value interpreter: a handle
26
- * table plus the `may_leave` gate. The full ComponentInstance (may_enter,
27
- * backpressure, threads, ...) belongs to the deferred task machinery.
26
+ * table plus the `may_leave` gate. The full ComponentInstance (backpressure,
27
+ * threads, ...) belongs to the task machinery, which cabi must not import.
28
28
  */
29
29
  export interface ComponentInstanceLike {
30
30
  handles: Table<unknown>;
31
31
  mayLeave: boolean;
32
32
  }
33
+ /**
34
+ * Brand marking a value as a REAL component instance (task/mod.ts
35
+ * `ComponentInstanceState`), as opposed to the many structural
36
+ * `ComponentInstanceLike` stand-ins — imported/host resources carry no
37
+ * instance at all, and test harnesses supply bare `{handles, mayLeave}`
38
+ * doubles. cabi must not depend on task/, so the symbol lives here and
39
+ * `ComponentInstanceState` declares it; cabi/handles.ts `isComponentInstance`
40
+ * is the only reader. `ComponentInstanceLike` stays deliberately structural:
41
+ * the brand is NOT part of it.
42
+ */
43
+ export declare const COMPONENT_INSTANCE: unique symbol;
33
44
  /**
34
45
  * Borrow scopes (definitions.py `LiftLowerContext.borrow_scope`):
35
46
  * - lifting a borrow requires the *subtask* side: `add_lender`.
@@ -53,9 +64,8 @@ export declare class LiftLowerContext {
53
64
  * with `may_leave` cleared, so a realloc that lowers an import traps
54
65
  * (`canon_lower`'s `trap_if(not ...may_leave)`, implemented here by
55
66
  * exec/boundary.ts `createLoweredImport`). That bracket is implemented
56
- * below. What remains deferred is only the reference's routing of the call
57
- * through `canon_lift`; upstream component-model PR #705 removes that
58
- * routing, leaving this bracket as the whole story. polyengine issue #147.
67
+ * below, and it is the whole story: the pinned reference does not route
68
+ * the call through `canon_lift` (CM#705). polyengine issue #147.
59
69
  */
60
70
  reallocate(old: number, oldByteLength: number, alignment: number, newByteLength: number): number;
61
71
  allocate(alignment: number, byteLength: number): number;
@@ -2,11 +2,11 @@ import { type CaseType, type CoreFuncType, type CoreType, type FieldType, type F
2
2
  import type { CanonicalOptions, LiftOptions } from "./context.js";
3
3
  export declare const MAX_FLAT_PARAMS = 16;
4
4
  export declare const MAX_FLAT_ASYNC_PARAMS = 4;
5
- export declare let MAX_FLAT_RESULTS: number;
6
- export declare function setMaxFlatResultsForTesting(n: number): number;
5
+ export declare const MAX_FLAT_RESULTS = 1;
7
6
  export type FlattenContext = "lift" | "lower";
8
7
  export declare function flattenFunctype(opts: CanonicalOptions, ft: FuncType, context: FlattenContext): CoreFuncType;
9
8
  export declare function flattenTypes(ts: ValType[], opts: LiftOptions): CoreType[];
9
+ export declare function flatCount(ts: ValType[], opts: LiftOptions): number;
10
10
  export declare function flattenType(t: ValType, opts: LiftOptions): CoreType[];
11
11
  export declare function flattenList(elemType: ValType, maybeLength: number | null, opts: LiftOptions): CoreType[];
12
12
  export declare function flattenRecord(fields: FieldType[], opts: LiftOptions): CoreType[];
@@ -23,11 +23,8 @@ export declare function lowerOwn(cx: LiftLowerContext, rep: number, t: OwnType):
23
23
  export declare function lowerBorrow(cx: LiftLowerContext, rep: number, t: BorrowType): number;
24
24
  export declare function canonResourceNew(inst: ComponentInstanceLike, rt: ResourceTypeInfo, rep: number): number;
25
25
  /**
26
- * Invoke a resource destructor through the reference's entry bracket.
27
- *
28
- * definitions.py `canon_resource_drop` (line 2319) does not call `rt.dtor`
29
- * directly. It builds the dtor into a function instance and calls it through
30
- * `Store.lift` / `Store.lower` (lines 2330-2333):
26
+ * Invoke a resource destructor, as definitions.py `canon_resource_drop`
27
+ * (@ 2f13265) does — through `Store.lift`/`Store.lower`:
31
28
  *
32
29
  * ```python
33
30
  * dtor = rt.dtor or (lambda rep: [])
@@ -36,34 +33,26 @@ export declare function canonResourceNew(inst: ComponentInstanceLike, rt: Resour
36
33
  * caller([h.rep])
37
34
  * ```
38
35
  *
39
- * so the dtor inherits `Store.lift`'s gate verbatim (lines 579-584):
40
- * `trap_if(not inst.may_enter_from(caller))`, `enter_from(caller)`, the call,
41
- * then `leave_to(caller)` — which a trap skips, leaving the *implementing*
42
- * instance permanently unenterable (poisoned).
43
- *
44
- * Two consequences that are easy to get wrong, both taken from the reference
45
- * rather than from intuition:
36
+ * That lift carries NO gate (CM#705): dropping a handle whose implementing
37
+ * instance is mid-execution is VALID, including the dtor-less case.
46
38
  *
47
- * - the bracket runs even when `rt.dtor is None` (the `or (lambda rep: [])`
48
- * above), so a dtor-less resource whose impl instance is mid-execution is
49
- * still a trap. `may_enter_from`/`enter_from` walk `entering_set(caller)`
50
- * (line 230), which is empty when the caller *is* the implementing
51
- * instance that, not a special case, is the same-instance exemption:
52
- * a component dropping a handle to its own resource never traps.
53
- * - poisoning applies to `rt.impl`, not to the dropping instance. The
54
- * dropper's own bracket (its `Store.lift` frame) is broken by the same
55
- * propagating trap at its own level; here only the callee is retired.
39
+ * What this adds is polyengine's per-instance poisoning divergence, and it
40
+ * applies to `rt.impl`, not to the dropping instance: a trap out of the dtor
41
+ * buries the implementing instance (refusal names the original trap,
42
+ * polyengine#145; its live stream/future ends are retired, #66). The
43
+ * dropper is poisoned, if at all, by the same trap propagating at its own
44
+ * level. `entryRefusal`'s `caller !== callee` guard keeps a component
45
+ * dropping a handle to its OWN resource admissible even against a marked
46
+ * instance.
56
47
  *
57
48
  * Capability signals (`NeedsJspi`, `PendingCapability`) are not traps — see
58
- * `isCapabilitySignal` in exec/boundary.ts — so they release the gate.
49
+ * `isCapabilitySignal` in exec/boundary.ts — so they do not poison.
59
50
  *
60
51
  * SCOPE (#160): this is the **guest-initiated** path only. A guest-initiated
61
52
  * drop must complete synchronously (the reference lifts the dtor with
62
53
  * `async_ = False`), so a thenable here is a trap. The host-initiated path
63
- * used to share this function with an `allowAsync` flag that held the entry
64
- * bracket across the dtor's promise; it now goes through the full lift
65
- * harness instead (`hostDtorCall` in exec/boundary.ts), which is what
66
- * definitions.py actually does and what unwedges #160.
54
+ * goes through the full lift harness instead (`hostDtorCall` in
55
+ * exec/boundary.ts), which is what definitions.py actually does.
67
56
  */
68
57
  export declare function callDtorGated(rt: ResourceTypeInfo, rep: number, caller: unknown): void;
69
58
  export declare function canonResourceDrop(inst: ComponentInstanceLike, rt: ResourceTypeInfo, i: number): void;
@@ -1,5 +1,26 @@
1
- import { type CaseType, type FieldType, type PtrType, type ValType } from "./types.js";
1
+ import { type CaseType, type DespecializedValType, type FieldType, type PtrType, type ValType } from "./types.js";
2
2
  export declare function alignTo(ptr: number, alignment: number): number;
3
+ /**
4
+ * Everything the lift/lower paths need to know about one type's byte layout,
5
+ * computed once and shared (issue #261).
6
+ *
7
+ * Deliberately ONE flat interface rather than a discriminated union of
8
+ * per-kind layouts: every `Layout` then has the same hidden class, so the hot
9
+ * property loads in `load`/`store` stay monomorphic. Fields that do not apply
10
+ * to a kind carry null/0 — please do not "improve" this into a union.
11
+ */
12
+ export interface Layout {
13
+ /** The despecialized type (definitions.py `despecialize`). */
14
+ readonly d: DespecializedValType;
15
+ readonly align: number;
16
+ readonly size: number;
17
+ /** record: field i occupies `base + fieldOffsets[i]`; null otherwise. */
18
+ readonly fieldOffsets: readonly number[] | null;
19
+ /** variant: discriminant width, and the offset of the case payload; 0 otherwise. */
20
+ readonly discSize: 0 | 1 | 2 | 4;
21
+ readonly payloadOffset: number;
22
+ }
23
+ export declare function layoutOf(t: ValType, ptrType: PtrType): Layout;
3
24
  export declare function alignment(t: ValType, ptrType: PtrType): number;
4
25
  export declare function alignmentList(elemType: ValType, maybeLength: number | null, ptrType: PtrType): number;
5
26
  export declare function alignmentRecord(fields: FieldType[], ptrType: PtrType): number;
@@ -6,8 +6,16 @@ export declare function convertIntToBool(i: number): boolean;
6
6
  export declare function loadList(cx: LiftLowerContext, ptr: number, elemType: ValType, maybeLength: number | null): ComponentValue;
7
7
  export declare function loadListFromRange(cx: LiftLowerContext, ptr: number | bigint, length: number | bigint, elemType: ValType): ComponentValue;
8
8
  export declare function loadListFromValidRange(cx: LiftLowerContext, ptr: number, length: number, elemType: ValType): ComponentValue;
9
- export declare function loadRecord(cx: LiftLowerContext, ptr: number, fields: FieldType[]): ComponentValue;
10
- export declare function loadVariant(cx: LiftLowerContext, ptr: number, cases: CaseType[]): ComponentValue;
9
+ /**
10
+ * `offsets[i]` is field i's byte offset from `ptr`, precomputed on the layout
11
+ * node, so the loop is an indexed read rather than the per-field
12
+ * `alignTo`/`alignment`/`elemSize` recomputation it used to be (issue #261).
13
+ * Taking the offsets rather than the whole `Layout` keeps the function
14
+ * self-consistent: its two arguments are the ones the result depends on, and
15
+ * there is no unchecked "these came from the same type" invariant to violate.
16
+ */
17
+ export declare function loadRecord(cx: LiftLowerContext, ptr: number, fields: FieldType[], offsets: readonly number[]): ComponentValue;
18
+ export declare function loadVariant(cx: LiftLowerContext, ptr: number, cases: CaseType[], discSize: 1 | 2 | 4, payloadOffset: number): ComponentValue;
11
19
  export declare function loadFlags(cx: LiftLowerContext, ptr: number, labels: string[]): ComponentValue;
12
20
  export declare function unpackFlagsFromInt(i: number, labels: string[]): {
13
21
  [label: string]: ComponentValue;
@@ -4,9 +4,11 @@ export declare function store(cx: LiftLowerContext, v: ComponentValue, t: ValTyp
4
4
  export declare function storeList(cx: LiftLowerContext, v: ArrayLike<ComponentValue>, ptr: number, elemType: ValType, maybeLength: number | null): void;
5
5
  export declare function storeListIntoRange(cx: LiftLowerContext, v: ArrayLike<ComponentValue>, elemType: ValType): [number, number];
6
6
  export declare function storeListIntoValidRange(cx: LiftLowerContext, v: ArrayLike<ComponentValue>, ptr: number, elemType: ValType): void;
7
- export declare function storeRecord(cx: LiftLowerContext, v: Record<string, ComponentValue>, ptr: number, fields: FieldType[]): void;
7
+ /** The store-side mirror of `loadRecord`: indexed offset writes, no per-field
8
+ * layout recomputation (issue #261). */
9
+ export declare function storeRecord(cx: LiftLowerContext, v: Record<string, ComponentValue>, ptr: number, fields: FieldType[], offsets: readonly number[]): void;
8
10
  /** definitions.py match_case: the value is a single-key object. */
9
11
  export declare function matchCase(v: Record<string, ComponentValue>, cases: CaseType[]): [number, ComponentValue];
10
- export declare function storeVariant(cx: LiftLowerContext, v: Record<string, ComponentValue>, ptr: number, cases: CaseType[]): void;
12
+ export declare function storeVariant(cx: LiftLowerContext, v: Record<string, ComponentValue>, ptr: number, cases: CaseType[], discSize: 1 | 2 | 4, payloadOffset: number): void;
11
13
  export declare function storeFlags(cx: LiftLowerContext, v: Record<string, ComponentValue>, ptr: number, labels: string[]): void;
12
14
  export declare function packFlagsIntoInt(v: Record<string, ComponentValue>, labels: string[]): number;
@@ -176,14 +176,33 @@ export type ComponentValue = AsyncValue | boolean | number | bigint | string | n
176
176
  export type CoreValue = number | bigint;
177
177
  export type DespecializedValType = Exclude<ValType, TupleType | EnumType | OptionType | ResultType | MapType>;
178
178
  export declare function despecialize(t: ValType): DespecializedValType;
179
+ /**
180
+ * The discriminant WIDTH — the single mirror of definitions.py
181
+ * `discriminant_type`'s arithmetic (line 1234), including its `assert(0 < n <
182
+ * (1 << 32))`. `math.ceil(log2(n)/8)`: 0|1 -> u8, 2 -> u16, 3 -> u32.
183
+ *
184
+ * The width is what the layout code actually wanted: on every variant lifted
185
+ * or lowered it used to build a `PrimType` purely to hand it to
186
+ * `alignment`/`elemSize` (issue #261). u8/u16/u32 each have alignment equal to
187
+ * their size, so this one number is both the discriminant's size and its
188
+ * alignment — which is why the variant layout kernels need nothing else.
189
+ */
190
+ export declare function discriminantSize(caseCount: number): 1 | 2 | 4;
191
+ /**
192
+ * definitions.py `discriminant_type`. The table itself lives in
193
+ * `discriminantSize`; this is only the width -> type mapping, so the bound
194
+ * check, the thresholds and the error string exist once. Flattening
195
+ * (flatten.ts:157) and layout (layout.ts `alignmentVariant`,
196
+ * `elemSizeVariant`) therefore cannot drift apart on the discriminant width.
197
+ */
179
198
  export declare function discriminantType(cases: CaseType[]): PrimType;
199
+ export declare function caseIndexOf(cases: CaseType[]): ReadonlyMap<string, number>;
180
200
  export declare function containsBorrow(t: ValType | null): boolean;
181
- export declare function containsAsyncValue(t: ValType | null): boolean;
182
201
  export declare function contains(t: ValType | null, p: (t: DespecializedValType) => boolean): boolean;
183
202
  /**
184
203
  * Structural `ValType` equality.
185
204
  *
186
- * CONTRACT (bugfix, TRACK C2-D; generalized during the #18 tls smoke): naive
205
+ * CONTRACT (bugfix, generalized during the #18 tls smoke): naive
187
206
  * `JSON.stringify(a) === JSON.stringify(b)` recurses into `own`/`borrow`'s
188
207
  * `ResourceTypeInfo` — a class whose `impl` field is documented "Compared by
189
208
  * object identity everywhere" (see `ResourceTypeInfo` above) and which cycles
@@ -191,7 +210,7 @@ export declare function contains(t: ValType | null, p: (t: DespecializedValType)
191
210
  * tables that reference their types), so `JSON.stringify` throws
192
211
  * `TypeError: Converting circular structure to JSON` on ANY type containing
193
212
  * `own<R>`/`borrow<R>` at any depth. First hit by `task.return` result types
194
- * (C2-D, polymorph-test's `list<own<test-case>>`), then by stream/future
213
+ * (polymorph-test's `list<own<test-case>>`), then by stream/future
195
214
  * element types (polymorph-tls streams carrying resource-bearing payloads).
196
215
  * Object-identity types (`ResourceTypeInfo`) are compared by reference, per
197
216
  * the documented invariant.
@@ -6,7 +6,7 @@
6
6
  * **This is not embedder API.** The entry point exists as a support surface
7
7
  * for bindgen-generated bindings — whose typed `instantiate` wrapper
8
8
  * verifies the digest before instantiating (contracts/embedder-api.md
9
- * amendment A17) — and for the runtime's own internals. No host program
9
+ * §"Module wiring and instantiation") — and for the runtime's own internals. No host program
10
10
  * should hand-write an import of this module.
11
11
  *
12
12
  * **Its contents are completely unstable: there is no compatibility promise