@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
@@ -38,6 +38,14 @@ function upperFirst(s) {
38
38
  const MANGLED = /^\[([a-z-]+)\](.*)$/;
39
39
  /**
40
40
  * Decode a mangled leaf name; unmangled names come back as `plain`.
41
+ *
42
+ * An unknown bracket form throws rather than falling back to `plain`
43
+ * (contracts/embedder-api.md §"Getters and setters (pre-ruling…)", final
44
+ * paragraph): "the runtime refuses unknown bracket forms in mangled names
45
+ * loudly at instantiation (rather than misbinding them as plain names — a
46
+ * `[get]foo` treated as a function named `[get]foo` would be wrong in both
47
+ * directions)". Getter/setter support (`[get]`/`[set]`, upstream
48
+ * WebAssembly/component-model#701) is tracked in polyengine#254.
41
49
  * @internal — leaf-name demangling, performed by the runtime and by
42
50
  * bindgen-generated code.
43
51
  */
@@ -48,20 +56,27 @@ export function parseLeafName(raw) {
48
56
  const [, tag, rest] = m;
49
57
  switch (tag) {
50
58
  case "constructor":
51
- return { form: "constructor", resource: rest };
59
+ if (!rest.includes("["))
60
+ return { form: "constructor", resource: rest };
61
+ break;
52
62
  case "method":
53
63
  case "static": {
54
64
  const dot = rest.indexOf(".");
55
65
  if (dot < 0)
56
66
  break;
57
- return {
58
- form: tag,
59
- resource: rest.slice(0, dot),
60
- member: rest.slice(dot + 1),
61
- };
67
+ const resource = rest.slice(0, dot);
68
+ // A resource name carrying a further bracket (`[method][get]r.p`, the
69
+ // exact getter/setter-on-instance spelling the pre-ruling names) is
70
+ // NOT a plain `[method]`/`[static]` leaf — it is one of the still-
71
+ // unimplemented forms, and must be refused the same way, not
72
+ // misparsed as a method whose resource is literally `[get]r`.
73
+ if (resource.includes("["))
74
+ break;
75
+ return { form: tag, resource, member: rest.slice(dot + 1) };
62
76
  }
63
77
  }
64
- // Unknown bracket forms (`[async]`, `[dtor]`, future spellings) are left
65
- // alone rather than guessed at: they surface verbatim, which is loud.
66
- return { form: "plain", name: raw };
78
+ throw new Error(`unrecognized mangled export/import name '${raw}': the bracket form is ` +
79
+ `not one this runtime understands (only [constructor]/[method]/` +
80
+ `[static] are implemented; getter/setter forms like [get]/[set] are ` +
81
+ `not yet implemented — polyengine#254)`);
67
82
  }
@@ -1,5 +1,5 @@
1
1
  // This runtime copy's identity (contracts/embedder-api.md §"Module identity
2
- // and @polyengine/protocol", amendment A9; issue #83).
2
+ // and @polyengine/protocol"; issue #83).
3
3
  //
4
4
  // One module owns the copy's URL so every diagnostic composes the same
5
5
  // message, and so lowering sites deep in the value adapters can name the copy
@@ -12,7 +12,7 @@
12
12
  import { copyCensus } from "@polyengine/protocol";
13
13
  /**
14
14
  * The URL of this copy of the runtime. Identity of the copy.
15
- * @internal — copy-identity constant for the A9 multi-copy diagnostics; not
15
+ * @internal — copy-identity constant for the module identity multi-copy diagnostics; not
16
16
  * host-facing.
17
17
  */
18
18
  export const COPY_URL = import.meta.url;
@@ -26,15 +26,15 @@ export const COPY_URL = import.meta.url;
26
26
  *
27
27
  * INVARIANT: keep in sync with `version` in runtime/deno.json — pinned by
28
28
  * runtime/tests/embedder/cross_copy_test.ts.
29
- * @internal — copy-identity constant for the A9 multi-copy diagnostics; not
29
+ * @internal — copy-identity constant for the module identity multi-copy diagnostics; not
30
30
  * host-facing.
31
31
  */
32
- export const RUNTIME_VERSION = "0.5.1";
32
+ export const RUNTIME_VERSION = "0.6.0";
33
33
  /**
34
34
  * Compose a cross-copy diagnostic: what was foreign, which copy is speaking,
35
35
  * the census of every copy in the graph, and the by-value remediation.
36
36
  *
37
- * Kept to one line but complete — the whole point of A9's stateful half is
37
+ * Kept to one line but complete — the whole point of module identity's stateful half is
38
38
  * that "recognized but foreign" is a NAMED failure, never a silent
39
39
  * adaptation (a foreign `Stream` pumped as an async iterable) and never a
40
40
  * misleading generic ("handle is not an error-context").
@@ -43,6 +43,6 @@ export function describeCrossCopy(what, remedy) {
43
43
  const census = copyCensus();
44
44
  return `${what} was minted by a DIFFERENT polyengine runtime copy and cannot ` +
45
45
  `be used through this one (this copy: ${COPY_URL}${census === "" ? "" : `; ${census}`}). Handles are stateful — their machinery lives in the copy that minted ` +
46
- `them (contracts/embedder-api.md amendment A9, issue #83)` +
46
+ `them (contracts/embedder-api.md §"Module identity")` +
47
47
  `${remedy === undefined ? "" : `. ${remedy}`}`;
48
48
  }
@@ -1,11 +1,11 @@
1
1
  // The embedder-facing error model (contracts/embedder-api.md §"Error model").
2
2
  //
3
- // The canonical definitions live in `@polyengine/protocol` since amendment A9
3
+ // The canonical definitions live in `@polyengine/protocol` since §"Module identity and @polyengine/protocol"
4
4
  // this module is the unchanged import path for them (every existing
5
5
  // `from "./errors.ts"` / `@polyengine/runtime/embedder` import keeps working) plus
6
6
  // the runtime-local `NameCollisionError`, which never crosses a copy boundary
7
7
  // (it is raised while BUILDING a facade, before any value exists) and
8
- // therefore carries no brand: the A9 table's omissions are deliberate.
8
+ // therefore carries no brand: the module identity table's omissions are deliberate.
9
9
  //
10
10
  // Recognition at the runtime's own boundaries is by BRAND, not class: use the
11
11
  // `is*` predicates re-exported below, never `instanceof`, for any value that
@@ -1,9 +1,9 @@
1
1
  // `requiredImports()` — the supported enumeration of a component's linkable
2
2
  // import leaves (contracts/embedder-api.md §"Module wiring and
3
- // instantiation"; C2 checklist item 2).
3
+ // instantiation").
4
4
  //
5
- // C0 finding #8: `plan.imports` proved the right authority, and every embedder
6
- // that needed it hand-rolled the same walk (tools/smoke-c0/wasi_stub.ts).
5
+ // `plan.imports` proved the right authority, and every embedder
6
+ // that needed it would otherwise hand-roll the same walk.
7
7
  // Blessing it removes that. It is also this layer's own input: the facade
8
8
  // builds its import wrappers from exactly this list.
9
9
  import { loadPlan } from "../plan/loader.js";
@@ -1,6 +1,6 @@
1
1
  // The conventions facade: `instantiate(artifacts, imports, opts)`.
2
2
  //
3
- // DESIGN (orchestrator ruling, C2): the facade is **runtime-driven**. Every
3
+ // DESIGN: the facade is **runtime-driven**. Every
4
4
  // camelCase name, every resource class and every import wrapper is built here,
5
5
  // at instantiate time, from the loaded plan's type tables — the plan already
6
6
  // carries names, kinds and function types. Bindgen emits compile-time *types*
@@ -11,7 +11,7 @@
11
11
  // contracts/plan-format.md for the wire shapes read here.
12
12
  import { loadEnvelope, loadPlan, PlanError } from "../plan/loader.js";
13
13
  import { Trap } from "../cabi/trap.js";
14
- import { CONSTRUCTOR_SYNC_ENTRY, hostResourceType, instantiateComponent, } from "../exec/mod.js";
14
+ import { SYNC_ENTRY, hostResourceType, instantiateComponent, } from "../exec/mod.js";
15
15
  import { camelCase, parseLeafName, pascalCase } from "./casing.js";
16
16
  import { abortable, deferCancel, isAbortable, isDeferCancel, isSuspending, suspending, } from "../jspi/suspending.js";
17
17
  import { Translator } from "../shim/mod.js";
@@ -23,14 +23,15 @@ import { buildGuestResourceClass, HostResourceRegistry, invalidateWrapper, lendW
23
23
  import { BorrowScope, describe, fromHost, toHost, } from "./values.js";
24
24
  import { ImportResolver } from "./version.js";
25
25
  import { Future, Stream } from "./streams.js";
26
+ import { markSyncCallable } from "./sync.js";
26
27
  /**
27
28
  * Relay the per-declaration host-import marks from the embedder's function
28
29
  * onto the wrapper the executor will actually receive, and return the
29
30
  * wrapper.
30
31
  *
31
32
  * Every `#dispatcher` arm re-wraps the embedder's function in a closure, so a
32
- * brand left on the original is INVISIBLE to `buildLoweredImport` — for A1
33
- * that surfaced as a `NeedsJspi`, for A23 (`deferCancel()`) it would be a
33
+ * brand left on the original is INVISIBLE to `buildLoweredImport` — for suspending mark
34
+ * that surfaced as a `NeedsJspi`, for cancellation discard (`deferCancel()`) it would be a
34
35
  * silently discarded commit, which is precisely the failure the brand exists
35
36
  * to prevent. Both marks are relayed by the same helper so a third one cannot
36
37
  * be added to one arm and forgotten in the other three.
@@ -58,7 +59,7 @@ function elementCodec(element, o) {
58
59
  * single-file JSON emitted by build-time translation (`tools/translate`,
59
60
  * or `Translator.translateRaw`), carrying the plan and the FACT adapter
60
61
  * modules. The production deploy set is `component.wasm` + its envelope +
61
- * the runtime: no translator ships (embedder-api.md amendment A4).
62
+ * the runtime: no translator ships (contracts/embedder-api.md §"Module wiring and instantiation").
62
63
  *
63
64
  * Pure and fetch-agnostic: acquire the two blobs however the platform
64
65
  * likes (HTTP, fs, bundler asset) and hand them over. The envelope embeds
@@ -76,7 +77,7 @@ export function artifactsFromEnvelope(envelopeJson, componentBytes) {
76
77
  * guest code runs: generated `instantiate` wrappers call this, verify the
77
78
  * plan, and only then delegate to `instantiate` below.
78
79
  * @internal — bindgen-generated code only — the digest handshake needs the
79
- * plan before instantiating (amendment A17).
80
+ * plan before instantiating (§"Module wiring and instantiation").
80
81
  */
81
82
  export async function resolveArtifacts(src) {
82
83
  if ("plan" in src)
@@ -126,7 +127,7 @@ export async function instantiate(source, imports = {}, opts = {}) {
126
127
  return instance;
127
128
  }
128
129
  /**
129
- * Alias matching the C2 dispatch's spelling.
130
+ * Alias matching bindgen's generated import spelling.
130
131
  * @internal — alias kept for bindgen-generated code only; hosts call
131
132
  * `instantiate`.
132
133
  */
@@ -175,7 +176,7 @@ class Facade {
175
176
  // guest `start` function can resolve resource types normally.
176
177
  //
177
178
  // One resource TYPE can be reached through several resource TABLES
178
- // (plan-format.md C2 amendment #1: a type export's index is a table
179
+ // (plan-format.md "Type exports index into `resourceTables`": a type export's index is a table
179
180
  // index, and the executor sets impl/dtor on every table whose `resource`
180
181
  // matches), hence index-keyed bindings with tokens as aliases.
181
182
  artifacts.plan.resourceTables.forEach((table, i) => {
@@ -188,8 +189,8 @@ class Facade {
188
189
  this.leaves = requiredImports(this.loaded);
189
190
  // A component that imports a resource TYPE cannot be wired without
190
191
  // `plan.importedResources`: that table is the only thing mapping the
191
- // import back to a `ResourceIndex` (plan-format.md v0.1 amendment #2 /
192
- // v0.2). Without it every own/borrow of that type would fail late, deep
192
+ // import back to a `ResourceIndex` (the `importedResources` field,
193
+ // contracts/plan-format.md schema). Without it every own/borrow of that type would fail late, deep
193
194
  // inside a call, as an unattributable `InvalidHandleError`.
194
195
  const resourceLeaves = this.leaves.filter((l) => l.kind === "resource");
195
196
  if (resourceLeaves.length > 0 &&
@@ -259,7 +260,7 @@ class Facade {
259
260
  #guestClass(b) {
260
261
  b.cls ??= buildGuestResourceClass({ name: b.name, ctor: null, ctorParams: null, methods: [], statics: [] },
261
262
  // The rt is supplied per wrapper, so an anonymous class needs none here.
262
- { impl: null, dtor: null }, () => Promise.reject(new TypeError("no methods")), () => []);
263
+ { impl: null, dtor: null }, () => () => Promise.reject(new TypeError("no methods")), () => []);
263
264
  return b.cls;
264
265
  }
265
266
  /**
@@ -270,7 +271,7 @@ class Facade {
270
271
  * construction — before instantiation, and therefore before a guest `start`
271
272
  * function can call an import that carries an `own`/`borrow` of one.
272
273
  * Imported resources occupy `ResourceIndex` 0..n-1 in `importedResources`
273
- * order (plan-format.md v0.1 amendment #2 / v0.2).
274
+ * order (the `importedResources` field, contracts/plan-format.md schema).
274
275
  */
275
276
  #bindHostResources() {
276
277
  const importedResources = this.artifacts.plan.importedResources ?? [];
@@ -349,7 +350,7 @@ class Facade {
349
350
  return rep;
350
351
  },
351
352
  dropOwn(rep, t) {
352
- // A13: a lowered `own` the guest will never take (an un-taken
353
+ // resource stream: a lowered `own` the guest will never take (an un-taken
353
354
  // stream element). Destroy it exactly as a guest-side drop would:
354
355
  // host-implemented R runs the instance's [Symbol.dispose] through
355
356
  // the registry; guest-implemented R runs the guest dtor via the
@@ -454,7 +455,7 @@ class Facade {
454
455
  }
455
456
  return impl(...raw);
456
457
  };
457
- // A1/A23 brand relay, layer 2 of 2 (see #dispatcher): the executor reads
458
+ // suspending mark/cancellation discard brand relay, layer 2 of 2 (see #dispatcher): the executor reads
458
459
  // the brands off this wrapper, which is what lands in its hostImports
459
460
  // record.
460
461
  return relayMarks(dispatch, wrapper);
@@ -493,11 +494,11 @@ class Facade {
493
494
  throw new PlanError(`host import '${label(leaf)}' missing or not a function (got ` +
494
495
  `${describe(fn)}); expected '${camelCase(m.name)}'`);
495
496
  }
496
- // A1/A23: the `suspending()` and `deferCancel()` brands ride the
497
+ // suspending mark/cancellation discard: the `suspending()` and `deferCancel()` brands ride the
497
498
  // dispatch closure so #wrapLeaf can relay them onto the value the
498
499
  // executor actually receives.
499
500
  //
500
- // A2 receiver rule: an interface member is invoked with its containing
501
+ // suspending mark receiver rule: an interface member is invoked with its containing
501
502
  // object as receiver (matching the static arm's `apply(cls)`), so a
502
503
  // class INSTANCE is a fully supported spelling of an interface
503
504
  // provider — methods reading instance state work. A world-level bare
@@ -522,12 +523,12 @@ class Facade {
522
523
  switch (m.form) {
523
524
  case "constructor":
524
525
  // Never markable: guest-driven construction of a host resource is
525
- // synchronous by the C2 amendment, and stage-3 reserves no
526
+ // synchronous (contracts/embedder-api.md §"Functions and async"), and stage-3 reserves no
526
527
  // constructor-decorator position.
527
528
  // deno-lint-ignore no-explicit-any
528
529
  return (args) => new cls(...args);
529
530
  case "method": {
530
- // A2: the brand authority for an instance method is the CLASS
531
+ // suspending mark: the brand authority for an instance method is the CLASS
531
532
  // PROTOTYPE, read at wrap time — the Suspending-wrap decision is
532
533
  // per-declaration and taken at instantiation, before any instance
533
534
  // exists. Instance-level method overrides do not change
@@ -539,7 +540,7 @@ class Facade {
539
540
  // `URLSearchParams.prototype.size`) brand-checks its receiver, and a
540
541
  // raw `prototype[member]` read runs it with `this` = the prototype —
541
542
  // an engine TypeError at instantiation, even for guests that never
542
- // call the member. Only a data-property function can carry the A2
543
+ // call the member. Only a data-property function can carry the suspending mark
543
544
  // mark (stage-3 method decorators install data properties), so an
544
545
  // accessor-backed member yields no wrap-time function here and stays
545
546
  // a call-time concern for the per-call lookup below.
@@ -561,7 +562,7 @@ class Facade {
561
562
  throw new PlanError(`host import '${label(leaf)}': ${clsName} has no static ` +
562
563
  `'${camelCase(m.member)}'`);
563
564
  }
564
- // A2: a static's brand sits on the function itself (a stage-3
565
+ // suspending mark: a static's brand sits on the function itself (a stage-3
565
566
  // static-method decorator marks the function value), readable here
566
567
  // at wrap time.
567
568
  const dispatch = (args) => fn.apply(cls, args);
@@ -596,7 +597,7 @@ class Facade {
596
597
  return fromHost(v, resultType, o);
597
598
  };
598
599
  const fail = (e, args) => {
599
- // Brand, not class (amendment A9): a `ComponentException` thrown by a host module
600
+ // Brand, not class (§"Module identity and @polyengine/protocol"): a `ComponentException` thrown by a host module
600
601
  // that resolved a DIFFERENT runtime copy — or hand-rolled with the
601
602
  // registry symbol — is the same value here (issue #83).
602
603
  if (isComponentException(e) && isResult) {
@@ -622,9 +623,9 @@ class Facade {
622
623
  `only a fallible import may signal an error value`);
623
624
  }
624
625
  // The #83 signature: in a graph with several copies, an UNBRANDED throw
625
- // is usually a pre-A9 copy's `ComponentException` (its brand rode class identity,
626
+ // is usually a pre-module identity copy's `ComponentException` (its brand rode class identity,
626
627
  // which does not survive the copy boundary). Say so rather than leaving
627
- // the latent puzzle that motivated amendment A9.
628
+ // the latent puzzle that motivated §"Module identity and @polyengine/protocol".
628
629
  const census = copyCensus();
629
630
  throw new Trap(`${where} threw ${describeThrow(e)}. An unbranded throw from a host ` +
630
631
  `import is a host bug and becomes a trap: signal a WIT error with ` +
@@ -632,13 +633,13 @@ class Facade {
632
633
  (census === ""
633
634
  ? ""
634
635
  : ` (${census} — an error carrying no polyengine brand in a ` +
635
- `multi-copy graph usually means a pre-A9 runtime copy threw ` +
636
+ `multi-copy graph usually means a pre-module identity runtime copy threw ` +
636
637
  `it, issue #83.)`));
637
638
  };
638
639
  return (...raw) => {
639
640
  const scope = new BorrowScope();
640
641
  const args = ft.params.map((p, i) => toHost(raw[i], p, o, scope));
641
- // CONTRACT (A24): anything the executor appended PAST the WIT-declared
642
+ // CONTRACT: anything the executor appended PAST the WIT-declared
642
643
  // params is a runtime-minted extra, not a component value — today
643
644
  // exactly the `abortable()` signal `createLoweredImport` adds for a
644
645
  // marked import. It is forwarded verbatim (no `toHost` conversion: it
@@ -658,7 +659,7 @@ class Facade {
658
659
  return fail(e, args);
659
660
  }
660
661
  if (isThenable(out)) {
661
- // Amendment A12: when the WIT result type is `future<T>`, a thenable
662
+ // When the WIT result type is `future<T>`, a thenable
662
663
  // return IS the future source ("for `future<T>`, a `Promise<T>` or
663
664
  // `Future<T>`" — §"Streams and futures"), not the call's async
664
665
  // completion. The import completes immediately with the lowered
@@ -753,8 +754,8 @@ class Facade {
753
754
  continue;
754
755
  }
755
756
  if (exp.kind === "module") {
756
- // Not WIT-expressible, digest-excluded (plan-format.md v4 amendment
757
- // 2): the WIT-shaped facade skips it, the type-export precedent. The
757
+ // Not WIT-expressible, digest-excluded (the `module` export kind,
758
+ // contracts/plan-format.md schema notes): the WIT-shaped facade skips it, the type-export precedent. The
758
759
  // raw executor export surface still carries the compiled module.
759
760
  continue;
760
761
  }
@@ -788,8 +789,8 @@ class Facade {
788
789
  const s = spec(member.resource);
789
790
  // Prefer the plain-entered variant in jspi mode: the JS `new`
790
791
  // cannot await the Promise a promising-wrapped entry returns
791
- // (exec/boundary.ts CONSTRUCTOR_SYNC_ENTRY).
792
- s.ctor = (fn[CONSTRUCTOR_SYNC_ENTRY] ?? fn);
792
+ // (exec/boundary.ts SYNC_ENTRY).
793
+ s.ctor = (fn[SYNC_ENTRY] ?? fn);
793
794
  s.ctorParams = ft.params;
794
795
  rtOf(ft.results[0], specRt, member.resource);
795
796
  break;
@@ -800,6 +801,7 @@ class Facade {
800
801
  raw: fn,
801
802
  params: ft.params,
802
803
  results: ft.results,
804
+ async: ft.async === true,
803
805
  });
804
806
  rtOf(ft.params[0], specRt, member.resource);
805
807
  break;
@@ -810,6 +812,7 @@ class Facade {
810
812
  raw: fn,
811
813
  params: ft.params,
812
814
  results: ft.results,
815
+ async: ft.async === true,
813
816
  });
814
817
  break;
815
818
  }
@@ -821,7 +824,7 @@ class Facade {
821
824
  throw new PlanError(`export '${id}': resource '${name}' has leaves but no own/borrow ` +
822
825
  `type to identify it by`);
823
826
  }
824
- const cls = buildGuestResourceClass(s, rt, (fn, params, results, where, args) => this.#wrapExportFn(fn, { params, results }, where)(...args), (args, params, where) => args.map((a, i) => fromHost(a, params[i], this.#opts(where))));
827
+ const cls = buildGuestResourceClass(s, rt, (raw, params, results, async, where) => this.#wrapExportFn(raw, { params, results, async }, where), (args, params, where) => args.map((a, i) => fromHost(a, params[i], this.#opts(where))));
825
828
  obj[claim(pascalCase(name), name)] = cls;
826
829
  const index = this.#tokenIndex.get(rt);
827
830
  if (index !== undefined) {
@@ -878,13 +881,14 @@ class Facade {
878
881
  #wrapExportFn(fn, ft, where) {
879
882
  const o = this.#opts(where);
880
883
  const resultType = ft.results.length === 0 ? null : ft.results[0];
884
+ let wrapper;
881
885
  if (resultType !== null && resultType.kind === "future") {
882
886
  // See `Future.deferred`: a `future<T>` result cannot be delivered
883
887
  // *through* a Promise, because promise resolution adopts thenables and
884
888
  // `Future<T>` is one. The handle is returned eagerly instead; it is
885
889
  // PromiseLike, so `await` still yields `T`.
886
890
  const element = resultType.element;
887
- return (...args) => {
891
+ wrapper = (...args) => {
888
892
  // Advisory 9: the generic branch checks arity; so must this one.
889
893
  if (args.length !== ft.params.length) {
890
894
  throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ` +
@@ -903,20 +907,111 @@ class Facade {
903
907
  return Future.deferred(pending, elementCodec(element, o));
904
908
  };
905
909
  }
906
- return async (...args) => {
910
+ else {
911
+ wrapper = async (...args) => {
912
+ if (args.length !== ft.params.length) {
913
+ throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ${args.length}`);
914
+ }
915
+ const { lowered, release } = this.#lowerParams(ft.params, args, o);
916
+ let raw;
917
+ try {
918
+ raw = await fn(...lowered);
919
+ }
920
+ finally {
921
+ // Call-scoped reps minted for `borrow<R>` arguments of a
922
+ // host-implemented resource live exactly as long as the call.
923
+ release();
924
+ }
925
+ if (resultType === null)
926
+ return undefined;
927
+ if (resultType.kind === "result") {
928
+ const v = raw;
929
+ if ("error" in v) {
930
+ throw new ComponentException(resultType.error === null
931
+ ? undefined
932
+ : toHost(v["error"], resultType.error, o));
933
+ }
934
+ return resultType.ok === null
935
+ ? undefined
936
+ : toHost(v["ok"], resultType.ok, o);
937
+ }
938
+ return toHost(raw, resultType, o);
939
+ };
940
+ }
941
+ // sync() brand (contracts/embedder-api.md §"Functions and async"): every
942
+ // returned wrapper is branded, additively — the default Promise-shaped
943
+ // surface above is unchanged either way.
944
+ if (ft.async === true) {
945
+ markSyncCallable(wrapper, { kind: "async" });
946
+ }
947
+ else {
948
+ markSyncCallable(wrapper, {
949
+ kind: "free",
950
+ fn: this.#buildSyncForm(fn, ft, where, o),
951
+ });
952
+ }
953
+ return wrapper;
954
+ }
955
+ /**
956
+ * The synchronous form of a sync-typed export (sync()'s `sync()` adapter),
957
+ * mirroring `#wrapExportFn`'s async form exactly minus the `await`:
958
+ * arity check, `#lowerParams`, the plain (`SYNC_ENTRY`) entry, result
959
+ * mapping.
960
+ *
961
+ * `SYNC_ENTRY` is the plain-entered variant `executor.ts` attaches to every
962
+ * sync-typed lifted export in jspi mode (exec/boundary.ts; in plain mode
963
+ * the lifted function itself already returns synchronously, so `fn` is
964
+ * used as-is — `fn[SYNC_ENTRY] ?? fn`).
965
+ */
966
+ #buildSyncForm(fn, ft, where, o) {
967
+ const resultType = ft.results.length === 0 ? null : ft.results[0];
968
+ const entry = fn[SYNC_ENTRY] ?? fn;
969
+ const unreachableThenable = (raw) => {
970
+ // Defensive (see the dispatch prompt / sync() failure ladder): a genuine
971
+ // park through a plain entry surfaces as a trap, `NeedsJspi`, or
972
+ // `SyncEntryBusy` — never a settled thenable VALUE. A silent
973
+ // Promise-as-value here would corrupt lifting rather than fail loudly,
974
+ // so this is a diagnostic backstop, not a documented outcome.
975
+ void raw;
976
+ throw new Error(`${where}: the sync entry returned a thenable, which should be ` +
977
+ `unreachable for a sync-typed WIT export (a genuine park surfaces ` +
978
+ `as a trap, NeedsJspi, or SyncEntryBusy instead) — this indicates ` +
979
+ `a runtime defect`);
980
+ };
981
+ if (resultType !== null && resultType.kind === "future") {
982
+ const element = resultType.element;
983
+ return (...args) => {
984
+ if (args.length !== ft.params.length) {
985
+ throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ` +
986
+ `${args.length}`);
987
+ }
988
+ const { lowered, release } = this.#lowerParams(ft.params, args, o);
989
+ let raw;
990
+ try {
991
+ raw = entry(...lowered);
992
+ }
993
+ finally {
994
+ release();
995
+ }
996
+ if (isThenable(raw))
997
+ unreachableThenable(raw);
998
+ return Future.fromLifted(raw, elementCodec(element, o));
999
+ };
1000
+ }
1001
+ return (...args) => {
907
1002
  if (args.length !== ft.params.length) {
908
1003
  throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ${args.length}`);
909
1004
  }
910
1005
  const { lowered, release } = this.#lowerParams(ft.params, args, o);
911
1006
  let raw;
912
1007
  try {
913
- raw = await fn(...lowered);
1008
+ raw = entry(...lowered);
914
1009
  }
915
1010
  finally {
916
- // Call-scoped reps minted for `borrow<R>` arguments of a
917
- // host-implemented resource live exactly as long as the call.
918
1011
  release();
919
1012
  }
1013
+ if (isThenable(raw))
1014
+ unreachableThenable(raw);
920
1015
  if (resultType === null)
921
1016
  return undefined;
922
1017
  if (resultType.kind === "result") {
@@ -974,7 +1069,7 @@ function pick(container, path, names) {
974
1069
  /**
975
1070
  * Read a DATA property from `obj` (walking its prototype chain, nearest own
976
1071
  * descriptor wins) without ever invoking accessors. Accessor-backed and
977
- * absent members both yield `undefined`. Used by the A2 wrap-time suspending
1072
+ * absent members both yield `undefined`. Used by the wrap-time suspending-mark
978
1073
  * probe, which must not run platform getters against a bare prototype.
979
1074
  */
980
1075
  function dataMember(obj, key) {
@@ -1,4 +1,4 @@
1
- // Embedder conventions layer (contracts/embedder-api.md; docs/milestones.md C2 / docs/consumers.md).
1
+ // Embedder conventions layer (contracts/embedder-api.md; docs/consumers.md).
2
2
  //
3
3
  // The host-facing surface: camelCase facades, resource classes on both sides,
4
4
  // stream/future handles, version-canonical import resolution and the branded
@@ -6,7 +6,7 @@
6
6
  // the layer works fully untyped. Bindgen (a separate track) emits compile-time
7
7
  // types that cast this facade; no generated code participates.
8
8
  // Copy registration (contracts/embedder-api.md §"Module identity and
9
- // @polyengine/protocol", amendment A9; issue #83). Runs at module evaluation, so
9
+ // @polyengine/protocol"; issue #83). Runs at module evaluation, so
10
10
  // merely importing the embedder surface puts this copy on the census — which
11
11
  // is what makes every cross-copy diagnostic below able to name both sides.
12
12
  // Multiple copies are DIAGNOSED, NEVER REFUSED: two isolated bundles on one
@@ -22,8 +22,8 @@ registerRuntimeCopy({
22
22
  protocolGeneration: PROTOCOL_GENERATION,
23
23
  });
24
24
  export { COPY_URL, RUNTIME_VERSION } from "./copy.js";
25
- // Amendment A22 (contracts/embedder-api.md §"The host-ABI surface and its
26
- // version"): the runtime's exported surface is application-only. The A9
25
+ // The host-ABI version (contracts/embedder-api.md §"The host-ABI surface and its
26
+ // version"): the runtime's exported surface is application-only. The
27
27
  // courtesy re-exports (error classes, predicates, brands, `suspending`,
28
28
  // realm crossing, the copy registry) are removed — host modules import that
29
29
  // vocabulary from `@polyengine/protocol` directly. The runtime still
@@ -34,9 +34,9 @@ export { requiredImports } from "./imports.js";
34
34
  // `NameCollisionError` is the one error class that stays here: it's raised
35
35
  // while building an instantiation facade, before any handle/value exists —
36
36
  // application machinery, not host-ABI vocabulary (contracts/embedder-api.md
37
- // §"The host-ABI surface and its version", amendment A22).
37
+ // §"The host-ABI surface and its version", §"The host-ABI surface and its version").
38
38
  export { NameCollisionError } from "./errors.js";
39
- // `createStream<T>()` — the A22 stream-pair factory (contracts/embedder-api.md
39
+ // `createStream<T>()` — the host-ABI version stream-pair factory (contracts/embedder-api.md
40
40
  // §"The host-ABI surface and its version" / §"Streams and futures"): the
41
41
  // `Stream.create()` static's application-surface spelling, since the
42
42
  // concrete `Stream`/`StreamWriter` classes are no longer exported. Handle
@@ -46,6 +46,7 @@ export function createStream() {
46
46
  return InternalStream.create();
47
47
  }
48
48
  export { GuestResource, HostResourceRegistry } from "./resources.js";
49
- export { camelCase, parseLeafName, pascalCase } from "./casing.js";
50
- export { asTrackKeySpelling, compareSemver, ImportRegistrationError, ImportResolutionError, ImportResolver, parseInterfaceId, parseSemver, trackKey, } from "./version.js";
49
+ export { camelCase, pascalCase } from "./casing.js";
50
+ export { ImportRegistrationError, ImportResolutionError, ImportResolver, } from "./version.js";
51
51
  export { BorrowScope, fromHost, toHost, } from "./values.js";
52
+ export { sync } from "./sync.js";