@polyengine/runtime 0.5.1 → 0.6.1

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
@@ -1,9 +1,9 @@
1
1
  // Resources as classes on both sides of the boundary
2
- // (contracts/embedder-api.md §"Resources"; C2 checklist item 3).
2
+ // (contracts/embedder-api.md §"Resources").
3
3
  //
4
4
  // The raw boundary represents `own<R>` / `borrow<R>` as bare **reps**
5
5
  // (cabi/handles.ts `liftOwn` returns `rh.rep`; the host never holds a table
6
- // index). C0 findings 1-3 were embedders turning that into identity tables and
6
+ // index). Embedders otherwise turn that into identity tables and
7
7
  // hand-transcribed `[method]…` keys by hand. Both become runtime obligations
8
8
  // here.
9
9
  //
@@ -19,14 +19,15 @@ import { hostDtorCall } from "../exec/boundary.js";
19
19
  import { COPY_URL, describeCrossCopy } from "./copy.js";
20
20
  import { InvalidHandleError } from "./errors.js";
21
21
  import { camelCase, pascalCase } from "./casing.js";
22
+ import { markSyncCallable, syncPayloadOf } from "./sync.js";
22
23
  /**
23
24
  * Internal state of a guest-resource wrapper.
24
25
  *
25
- * The KEY is the process-global `polyengine.resourceState/1` brand since amendment
26
- * A9 (it used to be a module-local `Symbol(...)`, on the now-repealed
26
+ * The KEY is the process-global `polyengine.resourceState/1` brand
27
+ * (contracts/embedder-api.md §"Module identity": it used to be a module-local `Symbol(...)`, on the now-repealed
27
28
  * assumption that bundle and source runtimes are never mixed in one process —
28
29
  * issue #83 showed they routinely are). The state SHAPE stays strictly
29
- * runtime-internal, exactly as the A9 brand table notes: another copy may
30
+ * runtime-internal, exactly as the module identity brand table notes: another copy may
30
31
  * RECOGNIZE a wrapper, and must never read or write this object. `copyUrl` is
31
32
  * what lets this copy tell its own wrappers from a foreign copy's.
32
33
  */
@@ -34,7 +35,7 @@ const STATE = RESOURCE_STATE;
34
35
  /** Base of every runtime-built guest-resource class. */
35
36
  export class GuestResource {
36
37
  constructor() {
37
- // A20 (contracts/embedder-api.md §"Realm boundaries and
38
+ // realm boundary (contracts/embedder-api.md §"Realm boundaries and
38
39
  // structured-clone-safe forms"; issue #131): guest-resource wrappers are
39
40
  // realm-local by principle (their machinery lives in the minting
40
41
  // copy's tables, issue #129's identity rule) — the pill makes a raw
@@ -130,7 +131,7 @@ export function initWrapper(w, state) {
130
131
  *
131
132
  * A wrapper minted by ANOTHER copy carries the same (process-global) brand key
132
133
  * but its state belongs to that copy — reading it here would be reading a
133
- * foreign copy's private shape (A9). So it is not a state: it is
134
+ * foreign copy's private shape. So it is not a state: it is
134
135
  * `undefined` here, and `requireLive` turns that into the named cross-copy
135
136
  * error rather than a misleading "not a resource handle" / "not live".
136
137
  */
@@ -141,9 +142,9 @@ export function wrapperState(w) {
141
142
  return s.copyUrl === COPY_URL ? s : undefined;
142
143
  }
143
144
  /**
144
- * True iff `w` carries the A9 resource-state key but is not one of ours.
145
+ * True iff `w` carries the module identity resource-state key but is not one of ours.
145
146
  *
146
- * Note the resource brand is the odd one out in the A9 table: its value is the
147
+ * Note the resource brand is the odd one out in the module identity table: its value is the
147
148
  * state OBJECT, not `true`, so `hasBrand` does not apply — presence of the key
148
149
  * is the recognition. Only meaningful once `wrapperState` has returned
149
150
  * `undefined`, i.e. presence here means "another copy's wrapper".
@@ -265,7 +266,7 @@ export function takeRep(w, own, what) {
265
266
  * synchronously is reported as such rather than silently returning a
266
267
  * half-built object (see the report's contract-friction list).
267
268
  */
268
- export function buildGuestResourceClass(spec, rt, adapt, lowerArgs) {
269
+ export function buildGuestResourceClass(spec, rt, wrapExport, lowerArgs) {
269
270
  const className = pascalCase(spec.name);
270
271
  const cls = class extends GuestResource {
271
272
  constructor(...args) {
@@ -298,22 +299,44 @@ export function buildGuestResourceClass(spec, rt, adapt, lowerArgs) {
298
299
  for (const m of spec.methods) {
299
300
  const js = camelCase(m.member);
300
301
  const where = `${className}.${js}`;
302
+ // Built ONCE at class-build time (sync(): "prototype methods and statics
303
+ // must carry the brand at class-build time, not per call") — every
304
+ // instance's method call goes through this same wrapper, receiver
305
+ // (`self`) prepended.
306
+ const wrapped = wrapExport(m.raw, m.params, m.results, m.async, where);
307
+ const methodFn = function (...args) {
308
+ // params[0] is the `borrow<R>`/`own<R>` self.
309
+ return wrapped(this, ...args);
310
+ };
311
+ const payload = syncPayloadOf(wrapped);
312
+ if (payload !== undefined) {
313
+ // A resource method's sync form takes `self` as its first argument —
314
+ // exactly `wrapped`'s own synchronous form (params[0] IS self), so the
315
+ // "method" brand's `fn` is `payload.fn` verbatim, just re-tagged so
316
+ // `sync()` knows this one needs `sync(instance)` rather than being
317
+ // callable bare.
318
+ markSyncCallable(methodFn, payload.kind === "free"
319
+ ? { kind: "method", fn: payload.fn }
320
+ : payload);
321
+ }
301
322
  Object.defineProperty(cls.prototype, js, {
302
323
  configurable: true,
303
324
  writable: true,
304
- value: function (...args) {
305
- // params[0] is the `borrow<R>`/`own<R>` self.
306
- return adapt(m.raw, m.params, m.results, where, [this, ...args]);
307
- },
325
+ value: methodFn,
308
326
  });
309
327
  }
310
328
  for (const s of spec.statics) {
311
329
  const js = camelCase(s.member);
312
330
  const where = `${className}.${js} (static)`;
331
+ const wrapped = wrapExport(s.raw, s.params, s.results, s.async, where);
332
+ const staticFn = (...args) => wrapped(...args);
333
+ const payload = syncPayloadOf(wrapped);
334
+ if (payload !== undefined)
335
+ markSyncCallable(staticFn, payload);
313
336
  Object.defineProperty(cls, js, {
314
337
  configurable: true,
315
338
  writable: true,
316
- value: (...args) => adapt(s.raw, s.params, s.results, where, args),
339
+ value: staticFn,
317
340
  });
318
341
  }
319
342
  return cls;
@@ -323,7 +346,7 @@ export function makeWrapper(
323
346
  // deno-lint-ignore no-explicit-any
324
347
  cls, rep, rt, owns) {
325
348
  const w = Object.create(cls.prototype);
326
- // A20: `Object.create` bypasses `GuestResource`'s constructor, so the
349
+ // realm boundary: `Object.create` bypasses `GuestResource`'s constructor, so the
327
350
  // realm-local pill is installed explicitly here (see that constructor).
328
351
  defineRealmLocal(w);
329
352
  initWrapper(w, {
@@ -1,5 +1,5 @@
1
1
  // Stream / future conventions (contracts/embedder-api.md §"Streams and
2
- // futures"; C2 checklist item 4).
2
+ // futures").
3
3
  //
4
4
  // The low-level seam is `exec/host_streams.ts` — `HostStream`/`HostFuture`
5
5
  // over the shared rendezvous object. This file is the *handle* layer named by
@@ -14,7 +14,7 @@ import { defineBrand, defineRealmLocal, ERROR_CONTEXT, FUTURE, hasBrand, isStrea
14
14
  import { describeCrossCopy } from "./copy.js";
15
15
  import { DroppedError, PeerTrappedError } from "./errors.js";
16
16
  // `StreamProducerError`'s canonical definition moved to `@polyengine/protocol`
17
- // with amendment A9 (it is an embedder-contract value: recognition must
17
+ // with §"Module identity and @polyengine/protocol" (it is an embedder-contract value: recognition must
18
18
  // survive multiple runtime copies, issue #83). Re-exported here so every
19
19
  // existing import path is unchanged.
20
20
  export { StreamProducerError } from "@polyengine/protocol";
@@ -45,7 +45,7 @@ const producerFailures = new WeakMap();
45
45
  * shared object and raised on the next interaction with the handle.
46
46
  */
47
47
  function reportProducerFailure(host, where, cause) {
48
- // Brand, not class (A9): a producer failure raised by another runtime copy
48
+ // Brand, not class: a producer failure raised by another runtime copy
49
49
  // must not be re-wrapped into a second layer of the same error.
50
50
  const err = isStreamProducerError(cause)
51
51
  ? cause
@@ -68,8 +68,8 @@ function throwIfFailed(value, where = "stream") {
68
68
  throwIfPeerTrapped(value, where);
69
69
  }
70
70
  /**
71
- * @internal — raise the recorded poisoning failure, if any (#66, amendment
72
- * A7). Pre-op: an operation started after the peer's instance trapped must
71
+ * @internal — raise the recorded poisoning failure, if any (#66,
72
+ * contracts/embedder-api.md §"Streams and futures"). Pre-op: an operation started after the peer's instance trapped must
73
73
  * reject rather than park forever. Post-await (with the op's outcome in
74
74
  * hand): an operation the retirement walk settled DROPPED-shaped must reject
75
75
  * rather than fake a clean end — but an op that genuinely COMPLETED before
@@ -86,12 +86,12 @@ export function isU8Element(element) {
86
86
  return element !== null && despecialize(element).kind === "u8";
87
87
  }
88
88
  /**
89
- * A21 (#128): the direct-access byte edges are `stream<u8>` only. A
89
+ * direct-access byte edge (#128): the direct-access byte edges are `stream<u8>` only. A
90
90
  * zero-width element type (`t === null`) is not u8 either.
91
91
  */
92
92
  function requireU8Direct(codec, who) {
93
93
  if (codec === null || !isU8Element(codec.element)) {
94
- throw new TypeError(`${who} is available on stream<u8> only (embedder-api amendment A21, ` +
94
+ throw new TypeError(`${who} is available on stream<u8> only (embedder-api.md §"Streams and futures" ("Direct-access byte edges"), ` +
95
95
  `polyengine#128); use write()/read() for other element types`);
96
96
  }
97
97
  }
@@ -112,7 +112,7 @@ export class Stream {
112
112
  constructor(host, codec) {
113
113
  this.#host = host;
114
114
  this.#codec = codec;
115
- // A20 (contracts/embedder-api.md §"Realm boundaries and
115
+ // realm boundary (contracts/embedder-api.md §"Realm boundaries and
116
116
  // structured-clone-safe forms"; issue #131): the realm-local pill —
117
117
  // stateful handles must fail loud (DataCloneError) at a raw
118
118
  // structuredClone/postMessage instead of husking silently.
@@ -183,14 +183,14 @@ export class Stream {
183
183
  "passed to a guest yet, so it has no element type; pass it first, " +
184
184
  "or use the writer, which parks until then");
185
185
  }
186
- // Post-transfer refusal (#162, embedder-api amendment A15). Lifting
186
+ // Post-transfer refusal (#162, contracts/embedder-api.md §"Streams and futures"). Lifting
187
187
  // removes the handle from the source table and lowering installs it in
188
188
  // the destination's (definitions.py `lift_async_value` line 1530,
189
189
  // `lower_stream` line 1828): once this handle's shared object has been
190
190
  // passed to a guest, the guest owns the readable end and a host read here
191
191
  // would operate a phantom duplicate of it. Refuse loudly instead.
192
192
  // `StreamWriter` is deliberately unaffected — the host retains the
193
- // writable end, and writing after the pass is the normal A5 pattern —
193
+ // writable end, and writing after the pass is the normal stream/future round-trip pattern —
194
194
  // and `drop()`/`cancelRead()` stay permissive.
195
195
  if (this.#consumed) {
196
196
  throw new TypeError("this Stream handle has already been passed to a guest; the guest " +
@@ -207,7 +207,7 @@ export class Stream {
207
207
  const raw = await host.readable.read(max);
208
208
  // An empty chunk normally means clean end-of-stream; when the peer's
209
209
  // instance trapped it means the retirement walk settled us — reject
210
- // instead of faking EOS (amendment A7). A non-empty chunk was really
210
+ // instead of faking EOS (§"Streams and futures"). A non-empty chunk was really
211
211
  // copied before the trap and is delivered; the next read rejects.
212
212
  if (raw.length === 0)
213
213
  throwIfPeerTrapped(host.value, where);
@@ -215,7 +215,7 @@ export class Stream {
215
215
  }
216
216
  /**
217
217
  * Consume the writer's bytes in place, without an intermediate chunk
218
- * (`stream<u8>` only — contracts/embedder-api.md amendment A21,
218
+ * (`stream<u8>` only — contracts/embedder-api.md §"Streams and futures" ("Direct-access byte edges"),
219
219
  * polyengine#128).
220
220
  *
221
221
  * At every rendezvous with a writer of nonzero capacity, `consume` runs
@@ -232,7 +232,7 @@ export class Stream {
232
232
  * writer's parked operation survives and the stream stays alive.
233
233
  *
234
234
  * Refusals mirror `read`: an unbound `Stream.create()` handle and a handle
235
- * already passed to a guest (the A15 transfer guard) both throw, as does a
235
+ * already passed to a guest (the deadlock-verdict suppression transfer guard) both throw, as does a
236
236
  * non-`u8` element type.
237
237
  */
238
238
  async readDirect(consume) {
@@ -242,7 +242,7 @@ export class Stream {
242
242
  requireU8Direct(this.#codec, "readDirect");
243
243
  const info = { endedByVerdict: false };
244
244
  const n = await host.readable.readDirect(consume, info);
245
- // A7 precision, `read`'s rule adapted: a session the CONSUMER itself
245
+ // loud component fault precision, `read`'s rule adapted: a session the CONSUMER itself
246
246
  // ended with `"done"` genuinely completed and keeps its resolution. Any
247
247
  // other way out (the writer dropped, the session was cancelled, the
248
248
  // retirement walk settled us) is a settle-path this consumer did not
@@ -298,7 +298,7 @@ export class Stream {
298
298
  * DROPPED event into the trapping instance's waitables, and a later
299
299
  * driving loop asserted on the corpse).
300
300
  *
301
- * The arm is released on this path too (#162, amendment A15): the wrapper's
301
+ * The arm is released on this path too (#162, §"Streams and futures"): the wrapper's
302
302
  * `HostActivity` now closes through the shared object's drop observers,
303
303
  * which `dropSharedForTeardown` fires unconditionally — so a teardown with
304
304
  * nothing parked no longer leaves the arm outliving the stream. (This
@@ -349,7 +349,7 @@ export class StreamWriter {
349
349
  #stream;
350
350
  constructor(stream) {
351
351
  this.#stream = stream;
352
- // A20 realm-local pill (see Stream's constructor above for rationale).
352
+ // realm boundary realm-local pill (see Stream's constructor above for rationale).
353
353
  defineRealmLocal(this);
354
354
  }
355
355
  /**
@@ -370,7 +370,7 @@ export class StreamWriter {
370
370
  const n = await host.writable.write(packChunk(values, this.#stream.codec));
371
371
  // A short take normally means "re-offer later" / "reader done"; when the
372
372
  // reader's instance trapped it means the retirement walk settled us —
373
- // reject, carrying the delivered count (amendment A7). A full take
373
+ // reject, carrying the delivered count (§"Streams and futures"). A full take
374
374
  // genuinely completed before the trap and stays a success.
375
375
  if (n < values.length)
376
376
  throwIfPeerTrapped(host.value, where, n);
@@ -378,7 +378,7 @@ export class StreamWriter {
378
378
  }
379
379
  /**
380
380
  * Fill the reader's landing zone in place, without an intermediate chunk
381
- * (`stream<u8>` only — contracts/embedder-api.md amendment A21,
381
+ * (`stream<u8>` only — contracts/embedder-api.md §"Streams and futures" ("Direct-access byte edges"),
382
382
  * polyengine#128).
383
383
  *
384
384
  * At every rendezvous with a reader of nonzero capacity, `produce` runs
@@ -406,7 +406,7 @@ export class StreamWriter {
406
406
  requireU8Direct(this.#stream.codec, "writeDirect");
407
407
  const info = { endedByVerdict: false };
408
408
  const n = await host.writable.writeDirect(produce, info);
409
- // A7 precision, `write`'s short-take rule adapted: a session the PRODUCER
409
+ // loud component fault precision, `write`'s short-take rule adapted: a session the PRODUCER
410
410
  // itself ended with `"done"` keeps its resolution; every other way out is
411
411
  // a settle-path the producer did not cause, so a trapped peer rejects
412
412
  // here carrying the delivered count.
@@ -470,7 +470,7 @@ export class Future {
470
470
  this.#host = host;
471
471
  this.#hostP = hostP;
472
472
  this.#codec = codec;
473
- // A20 realm-local pill (see Stream's constructor above for rationale).
473
+ // realm boundary realm-local pill (see Stream's constructor above for rationale).
474
474
  defineRealmLocal(this);
475
475
  }
476
476
  static fromLifted(value, codec) {
@@ -494,7 +494,6 @@ export class Future {
494
494
  * is itself PromiseLike, so `await` still works and still yields `T`), which
495
495
  * keeps `drop()`/`cancel()` reachable for a caller that does not await. The
496
496
  * alternative — resolving a Promise *to* the handle — is not expressible.
497
- * Flagged in the C2 report.
498
497
  */
499
498
  static deferred(pending, codec) {
500
499
  const hostP = pending.then((v) => {
@@ -530,7 +529,7 @@ export class Future {
530
529
  return this.#host.value;
531
530
  }
532
531
  #read() {
533
- // Post-transfer refusal (#162, amendment A15), the `Stream.read` mirror:
532
+ // Post-transfer refusal (#162, §"Streams and futures"), the `Stream.read` mirror:
534
533
  // once this handle was passed to a guest, the guest owns the readable end
535
534
  // and a host read would operate a phantom duplicate. A read MEMOIZED
536
535
  // before the transfer keeps resolving — it genuinely happened while the
@@ -546,7 +545,7 @@ export class Future {
546
545
  const { value, result } = await host.readResult();
547
546
  if (result !== CopyResult.COMPLETED) {
548
547
  // A drop caused by the writer's instance trapping is a fault, not a
549
- // "no value" outcome — brand it (#66, amendment A7).
548
+ // "no value" outcome — brand it (#66, §"Streams and futures").
550
549
  throwIfPeerTrapped(host.value, this.#codec.where ?? "future read");
551
550
  throw new DroppedError(result === CopyResult.CANCELLED
552
551
  ? "the future read was cancelled"
@@ -623,7 +622,7 @@ export class ErrorContext {
623
622
  constructor(internal) {
624
623
  this.internal = internal;
625
624
  this.message = internal.debugMessage;
626
- // A20 realm-local pill (see Stream's constructor above for rationale).
625
+ // realm boundary realm-local pill (see Stream's constructor above for rationale).
627
626
  // Note: envelope-encodable brands take precedence over the pill at
628
627
  // toCloneable time (ErrorContext carries both ERROR_CONTEXT and the
629
628
  // pill; it encodes) — the pill here is only the backstop for raw
@@ -631,11 +630,11 @@ export class ErrorContext {
631
630
  defineRealmLocal(this);
632
631
  }
633
632
  }
634
- // A9 brands (contracts/embedder-api.md §"Module identity"): the STATEFUL
633
+ // module identity brands (contracts/embedder-api.md §"Module identity"): the STATEFUL
635
634
  // embedder-facing handle classes. Their machinery lives in the copy that
636
635
  // minted them, so the brand never makes a foreign handle usable — it makes
637
636
  // it DIAGNOSABLE, at the lowering sites below. `StreamWriter` gains its
638
- // brand with amendment A22 (§"The host-ABI surface and its version"):
637
+ // brand with §"The host-ABI surface and its version" (§"The host-ABI surface and its version"):
639
638
  // writers carried none before because nothing needed to recognize one, and
640
639
  // `isStreamWriter` now does.
641
640
  defineBrand(Stream.prototype, STREAM);
@@ -651,13 +650,13 @@ defineBrand(ErrorContext.prototype, ERROR_CONTEXT);
651
650
  * footgun.
652
651
  */
653
652
  export function lowerStreamSource(src, codec) {
654
- // Order matters (amendment A9). Same-copy handle: the fast path, unchanged.
653
+ // Order matters (§"Module identity and @polyengine/protocol"). Same-copy handle: the fast path, unchanged.
655
654
  if (src instanceof Stream) {
656
655
  return src.takeValue(codec);
657
656
  }
658
657
  // Branded but not ours: a `Stream` minted by ANOTHER runtime copy. Without
659
658
  // this check it would fall through to producer adaptation below and be
660
- // pumped by its async iterator — a silent downgrade that quietly voids A5's
659
+ // pumped by its async iterator — a silent downgrade that quietly voids stream/future round-trip's
661
660
  // identity guarantees. Refused, loudly, naming both copies (issue #83).
662
661
  if (hasBrand(src, STREAM)) {
663
662
  throw new TypeError(describeCrossCopy("this stream handle", "To pipe it by value, pass `src.readable()` instead."));
@@ -695,11 +694,11 @@ async function pump(src, host, codec) {
695
694
  const where = codec.where ?? "stream producer";
696
695
  let failure;
697
696
  let produced = 0;
698
- // A13 cancellation companion: the pump learns of the reader dropping
697
+ // resource stream cancellation companion: the pump learns of the reader dropping
699
698
  // through short writes, but a producer PARKED on an external event (an
700
699
  // accept-shaped source holding a live platform resource) offers no write
701
700
  // to shorten — this notification is its only stop signal. It also fires
702
- // on the A7 teardown walk and on our own end-of-pump drop (harmless: the
701
+ // on the loud component fault teardown walk and on our own end-of-pump drop (harmless: the
703
702
  // loop has exited by then).
704
703
  const gone = new Promise((resolve) => host.writable.onDropped(() => resolve(READER_GONE)));
705
704
  try {
@@ -712,7 +711,7 @@ async function pump(src, host, codec) {
712
711
  n = await host.writable.writeAll(lowered);
713
712
  }
714
713
  catch (e) {
715
- // A13: elements past the fault's progress point were lowered but
714
+ // resource stream: elements past the fault's progress point were lowered but
716
715
  // will never be taken — destroy them (an `own` element may hold a
717
716
  // live platform resource). `PeerTrappedError.progress` reports
718
717
  // delivered-before-the-fault; anything else delivered nothing.
@@ -722,7 +721,7 @@ async function pump(src, host, codec) {
722
721
  produced += n;
723
722
  if (n < lowered.length) {
724
723
  // The reader went away: a clean end — but the un-taken tail of this
725
- // chunk was already lowered and must be destroyed, not leaked (A13).
724
+ // chunk was already lowered and must be destroyed, not leaked.
726
725
  releaseUntaken(lowered, n, codec);
727
726
  break;
728
727
  }
@@ -744,7 +743,7 @@ async function pump(src, host, codec) {
744
743
  // been recorded on the store, so the call fails rather than resolving.
745
744
  host.writable.drop();
746
745
  }
747
- /** A13: destroy `lowered[taken..]` when a codec's elements hold resources. */
746
+ /** resource stream: destroy `lowered[taken..]` when a codec's elements hold resources. */
748
747
  function releaseUntaken(lowered, taken, codec) {
749
748
  const release = codec.release;
750
749
  if (release === undefined || lowered instanceof Uint8Array)
@@ -754,13 +753,13 @@ function releaseUntaken(lowered, taken, codec) {
754
753
  }
755
754
  /**
756
755
  * Normalize every accepted producer shape to an async iterator of batches,
757
- * racing each pull against `gone` (A13 cancellation): when the stream dies
756
+ * racing each pull against `gone` (resource stream cancellation): when the stream dies
758
757
  * with the producer parked, a `ReadableStream` source is `cancel()`ed
759
758
  * through its reader, and an (async-)iterable source gets its optional
760
759
  * `cancel()` method invoked — the documented producer-cancellation hook —
761
760
  * then its pending pull is drained so a straggler element the producer
762
761
  * already minted still reaches the caller's release path. A source with no
763
- * cancel hook keeps the pre-A13 behavior: the pump stays parked until the
762
+ * cancel hook keeps the pre-resource stream behavior: the pump stays parked until the
764
763
  * producer's next element (or forever — the documented embedder-negligence
765
764
  * hang class).
766
765
  */
@@ -835,10 +834,10 @@ function isReadableStream(v) {
835
834
  export function lowerFutureSource(src, codec) {
836
835
  if (src instanceof Future)
837
836
  return src.takeValue();
838
- // Branded but not ours (amendment A9). This one is the sharpest edge in the
837
+ // Branded but not ours (§"Module identity and @polyengine/protocol"). This one is the sharpest edge in the
839
838
  // family: `Future` is a `PromiseLike`, so a foreign future would otherwise
840
839
  // be adopted as a plain thenable and appear to work — exactly the silent
841
- // path A9 bans, since the awaited value would ride the OTHER copy's
840
+ // path module identity bans, since the awaited value would ride the OTHER copy's
842
841
  // machinery with no handle transfer at all.
843
842
  if (hasBrand(src, FUTURE)) {
844
843
  throw new TypeError(describeCrossCopy("this future handle", "To pipe it by value, pass `Promise.resolve(f)` instead."));
@@ -0,0 +1,242 @@
1
+ // `sync()` — the explicit synchronous view of a WIT-sync export (contracts/
2
+ // embedder-api.md §"Functions and async", §"Functions and async", 2026-08-30).
3
+ //
4
+ // Placement: application machinery exported from
5
+ // `@polyengine/runtime/embedder`, like `createStream` — only an instantiating
6
+ // application holds export functions, so this is deliberately NOT host-module
7
+ // vocabulary and does not touch `@polyengine/protocol`.
8
+ //
9
+ // Recognition is by brand (`polyengine.syncCallable/1`, a registry symbol per
10
+ // module identity) so views work across mixed runtime copies. Unlike the boolean brands in
11
+ // `@polyengine/protocol`'s `brands.ts` (whose payload is always `true`), this
12
+ // brand carries a PAYLOAD describing the callable's synchronous form — the
13
+ // dispatch shapes below are what `instantiate.ts` / `resources.ts` attach at
14
+ // wrap time and what this module reads back.
15
+ /** The registry symbol. `Symbol.for` per module identity: N runtime copies agree on it
16
+ * without sharing modules. */
17
+ export const SYNC_CALLABLE = Symbol.for("polyengine.syncCallable/1");
18
+ /**
19
+ * Stamp `payload` on `target` under the brand: non-enumerable, non-writable,
20
+ * matching `@polyengine/protocol`'s `defineBrand` (protocol/src/brands.ts) —
21
+ * implemented locally since the runtime does not add application-tier
22
+ * vocabulary to the protocol package.
23
+ *
24
+ * @internal — written by `instantiate.ts` and `resources.ts` at wrap/
25
+ * class-build time; not part of the public `sync()` surface.
26
+ */
27
+ export function markSyncCallable(target, payload) {
28
+ Object.defineProperty(target, SYNC_CALLABLE, {
29
+ value: payload,
30
+ enumerable: false,
31
+ writable: false,
32
+ configurable: false,
33
+ });
34
+ }
35
+ /**
36
+ * Read the brand payload off `target`, or `undefined` if unbranded.
37
+ * Structural, like `hasBrand`: accepts a payload minted by any copy.
38
+ * @internal
39
+ */
40
+ export function syncPayloadOf(target) {
41
+ if (target === null)
42
+ return undefined;
43
+ const t = typeof target;
44
+ if (t !== "object" && t !== "function")
45
+ return undefined;
46
+ return target[SYNC_CALLABLE];
47
+ }
48
+ /** Own, function-valued, branded members of `proto`'s prototype chain
49
+ * (stopping at `Object.prototype`), nearest wins. Used to recognize a
50
+ * guest-resource INSTANCE: its class's prototype carries `"method"`-branded
51
+ * data properties (`resources.ts` `buildGuestResourceClass`). */
52
+ function protoBrandedMembers(proto) {
53
+ const out = new Map();
54
+ for (let o = proto; o !== null && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
55
+ for (const key of Object.getOwnPropertyNames(o)) {
56
+ if (out.has(key) || key === "constructor")
57
+ continue;
58
+ const d = Object.getOwnPropertyDescriptor(o, key);
59
+ if (d === undefined || typeof d.value !== "function")
60
+ continue;
61
+ const p = syncPayloadOf(d.value);
62
+ if (p !== undefined)
63
+ out.set(key, p);
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ /** Own, function-valued, branded static members of a guest-resource class. */
69
+ function ownBrandedStatics(cls) {
70
+ const out = new Map();
71
+ for (const key of Object.getOwnPropertyNames(cls)) {
72
+ if (key === "prototype" || key === "name" || key === "length")
73
+ continue;
74
+ const d = Object.getOwnPropertyDescriptor(cls, key);
75
+ if (d === undefined || typeof d.value !== "function")
76
+ continue;
77
+ const p = syncPayloadOf(d.value);
78
+ if (p !== undefined)
79
+ out.set(key, p);
80
+ }
81
+ return out;
82
+ }
83
+ function isResourceInstance(v) {
84
+ if (typeof v === "function")
85
+ return false; // a class, not an instance
86
+ const proto = Object.getPrototypeOf(v);
87
+ if (proto === null || proto === Object.prototype)
88
+ return false;
89
+ return protoBrandedMembers(proto).size > 0;
90
+ }
91
+ // deno-lint-ignore ban-types
92
+ function isResourceClass(v) {
93
+ return ownBrandedStatics(v).size > 0;
94
+ }
95
+ function asyncMessage(name) {
96
+ return `sync(): '${name}' is an async-typed WIT export; async exports ` +
97
+ `have no synchronous form`;
98
+ }
99
+ function methodMessage(name) {
100
+ return `sync(): '${name}' is a resource method; call sync(instance) ` +
101
+ `instead of sync(fn) — a bare method function has no receiver to bind`;
102
+ }
103
+ /** Views are stable: `sync(x) === sync(x)` for the same target. */
104
+ const views = new WeakMap();
105
+ function memoView(key, build) {
106
+ const cached = views.get(key);
107
+ if (cached !== undefined)
108
+ return cached;
109
+ const view = build();
110
+ views.set(key, view);
111
+ return view;
112
+ }
113
+ /** A view member that reports its real reason (async) only when accessed —
114
+ * so an unrelated sync member of the same record/class/instance stays usable
115
+ * (see the CONTRACT note on record recursion below). */
116
+ function throwingMember(view, key, message) {
117
+ Object.defineProperty(view, key, {
118
+ enumerable: true,
119
+ configurable: true,
120
+ get() {
121
+ throw new TypeError(message);
122
+ },
123
+ });
124
+ }
125
+ function instanceView(instance) {
126
+ return memoView(instance, () => {
127
+ const proto = Object.getPrototypeOf(instance);
128
+ const members = protoBrandedMembers(proto);
129
+ const view = {};
130
+ for (const [key, p] of members) {
131
+ if (p.kind === "method") {
132
+ const fn = p.fn;
133
+ view[key] = (...a) => fn(instance, ...a);
134
+ }
135
+ else if (p.kind === "async") {
136
+ throwingMember(view, key, asyncMessage(key));
137
+ }
138
+ // A "free"-kind branded proto member should not occur (methods are
139
+ // always branded "method" by `buildGuestResourceClass`); nothing to do
140
+ // if it somehow did — the instance view only ever exposes methods
141
+ // (statics are not reachable from an instance; §"Functions and async").
142
+ }
143
+ return view;
144
+ });
145
+ }
146
+ function classView(cls) {
147
+ return memoView(cls, () => {
148
+ const statics = ownBrandedStatics(cls);
149
+ const view = {};
150
+ for (const [key, p] of statics) {
151
+ if (p.kind === "free") {
152
+ view[key] = p.fn;
153
+ }
154
+ else if (p.kind === "async") {
155
+ throwingMember(view, key, asyncMessage(key));
156
+ }
157
+ }
158
+ return view;
159
+ });
160
+ }
161
+ /**
162
+ * Map one record MEMBER by the `sync(record)` recursion rule: a branded
163
+ * function or a nested resource class/instance/record maps recursively;
164
+ * anything else (including an unbranded function) passes through unchanged.
165
+ *
166
+ * CONTRACT (contracts/embedder-api.md §"Functions and async" sync(), the
167
+ * `sync(record)` bullet): the bullet says a record's members are "mapped by
168
+ * these same rules, recursively" — read most literally, an async-typed
169
+ * member nested in a record should behave exactly as `sync(asyncFn)` does at
170
+ * top level, i.e. throw. But applying that EAGERLY while building the
171
+ * parent's view would make one unrelated async export in a real component's
172
+ * exports record (a normal mix — see contracts/embedder-api.md's own async +
173
+ * sync export examples) poison `sync(exports)` entirely, defeating the
174
+ * per-use adapter's whole purpose. The conservative reading kept here defers
175
+ * that failure to the point the caller actually reaches for the async
176
+ * member (`throwingMember`), never for members the caller never touches —
177
+ * every failure the contract mandates still happens, just lazily.
178
+ */
179
+ function mapMember(v) {
180
+ if (typeof v === "function") {
181
+ const p = syncPayloadOf(v);
182
+ if (p !== undefined) {
183
+ if (p.kind === "free")
184
+ return p.fn;
185
+ if (p.kind === "method")
186
+ throw new TypeError(methodMessage(v.name));
187
+ throw new TypeError(asyncMessage(v.name));
188
+ }
189
+ if (isResourceClass(v))
190
+ return classView(v);
191
+ return v; // unbranded function: pass through unchanged
192
+ }
193
+ if (v !== null && typeof v === "object") {
194
+ if (isResourceInstance(v))
195
+ return instanceView(v);
196
+ return recordView(v); // a nested (interface) record
197
+ }
198
+ return v; // primitives, null: pass through unchanged
199
+ }
200
+ function recordView(rec) {
201
+ return memoView(rec, () => {
202
+ const view = {};
203
+ for (const key of Object.keys(rec)) {
204
+ const d = Object.getOwnPropertyDescriptor(rec, key);
205
+ if (d === undefined)
206
+ continue;
207
+ const value = d.value;
208
+ // Lazy: `mapMember` runs (and can throw, for an async member) only
209
+ // when the caller actually reads this key — see the CONTRACT note on
210
+ // `mapMember` above.
211
+ Object.defineProperty(view, key, {
212
+ enumerable: true,
213
+ configurable: true,
214
+ get: () => mapMember(value),
215
+ });
216
+ }
217
+ return view;
218
+ });
219
+ }
220
+ export function sync(target) {
221
+ if (typeof target === "function") {
222
+ const p = syncPayloadOf(target);
223
+ if (p !== undefined) {
224
+ if (p.kind === "free")
225
+ return p.fn;
226
+ if (p.kind === "method") {
227
+ throw new TypeError(methodMessage(target.name || "<anonymous>"));
228
+ }
229
+ throw new TypeError(asyncMessage(target.name || "<anonymous>"));
230
+ }
231
+ if (isResourceClass(target))
232
+ return classView(target);
233
+ throw new TypeError(`sync(): '${target.name || "<anonymous>"}' is not a sync-callable export (unbranded function)`);
234
+ }
235
+ if (target === null || typeof target !== "object") {
236
+ throw new TypeError(`sync(): expected a lifted export function, guest-resource instance/` +
237
+ `class, or exports record; got ${target === null ? "null" : typeof target}`);
238
+ }
239
+ if (isResourceInstance(target))
240
+ return instanceView(target);
241
+ return recordView(target);
242
+ }