@polyengine/runtime 0.5.0 → 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 (82) 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 +170 -41
  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 +318 -165
  28. package/esm/exec/executor.js +51 -24
  29. package/esm/exec/host_streams.js +31 -31
  30. package/esm/intrinsics/async_builtins.js +19 -8
  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 +10 -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 +66 -42
  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/jspi/suspending.d.ts +1 -1
  75. package/types/plan/format.d.ts +11 -10
  76. package/types/plan/loader.d.ts +2 -2
  77. package/types/shim/translator.d.ts +2 -2
  78. package/types/task/mod.d.ts +26 -97
  79. package/types/task/scheduler.d.ts +81 -62
  80. package/types/task/streams.d.ts +23 -38
  81. package/types/task/subtask.d.ts +2 -2
  82. 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.0";
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,9 +11,9 @@
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
- import { isSuspending, suspending } from "../jspi/suspending.js";
16
+ import { abortable, deferCancel, isAbortable, isDeferCancel, isSuspending, suspending, } from "../jspi/suspending.js";
17
17
  import { Translator } from "../shim/mod.js";
18
18
  import { copyCensus, isTrap, isComponentException } from "@polyengine/protocol";
19
19
  import { NameCollisionError, ComponentException } from "./errors.js";
@@ -23,6 +23,28 @@ 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";
27
+ /**
28
+ * Relay the per-declaration host-import marks from the embedder's function
29
+ * onto the wrapper the executor will actually receive, and return the
30
+ * wrapper.
31
+ *
32
+ * Every `#dispatcher` arm re-wraps the embedder's function in a closure, so 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
35
+ * silently discarded commit, which is precisely the failure the brand exists
36
+ * to prevent. Both marks are relayed by the same helper so a third one cannot
37
+ * be added to one arm and forgotten in the other three.
38
+ */
39
+ function relayMarks(from, to) {
40
+ if (isSuspending(from))
41
+ suspending(to);
42
+ if (isDeferCancel(from))
43
+ deferCancel(to);
44
+ if (isAbortable(from))
45
+ abortable(to);
46
+ return to;
47
+ }
26
48
  /** Per-element codec for a `future<T>` returned in function-result position. */
27
49
  function elementCodec(element, o) {
28
50
  return {
@@ -37,7 +59,7 @@ function elementCodec(element, o) {
37
59
  * single-file JSON emitted by build-time translation (`tools/translate`,
38
60
  * or `Translator.translateRaw`), carrying the plan and the FACT adapter
39
61
  * modules. The production deploy set is `component.wasm` + its envelope +
40
- * the runtime: no translator ships (embedder-api.md amendment A4).
62
+ * the runtime: no translator ships (contracts/embedder-api.md §"Module wiring and instantiation").
41
63
  *
42
64
  * Pure and fetch-agnostic: acquire the two blobs however the platform
43
65
  * likes (HTTP, fs, bundler asset) and hand them over. The envelope embeds
@@ -55,7 +77,7 @@ export function artifactsFromEnvelope(envelopeJson, componentBytes) {
55
77
  * guest code runs: generated `instantiate` wrappers call this, verify the
56
78
  * plan, and only then delegate to `instantiate` below.
57
79
  * @internal — bindgen-generated code only — the digest handshake needs the
58
- * plan before instantiating (amendment A17).
80
+ * plan before instantiating (§"Module wiring and instantiation").
59
81
  */
60
82
  export async function resolveArtifacts(src) {
61
83
  if ("plan" in src)
@@ -105,7 +127,7 @@ export async function instantiate(source, imports = {}, opts = {}) {
105
127
  return instance;
106
128
  }
107
129
  /**
108
- * Alias matching the C2 dispatch's spelling.
130
+ * Alias matching bindgen's generated import spelling.
109
131
  * @internal — alias kept for bindgen-generated code only; hosts call
110
132
  * `instantiate`.
111
133
  */
@@ -154,7 +176,7 @@ class Facade {
154
176
  // guest `start` function can resolve resource types normally.
155
177
  //
156
178
  // One resource TYPE can be reached through several resource TABLES
157
- // (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
158
180
  // index, and the executor sets impl/dtor on every table whose `resource`
159
181
  // matches), hence index-keyed bindings with tokens as aliases.
160
182
  artifacts.plan.resourceTables.forEach((table, i) => {
@@ -167,8 +189,8 @@ class Facade {
167
189
  this.leaves = requiredImports(this.loaded);
168
190
  // A component that imports a resource TYPE cannot be wired without
169
191
  // `plan.importedResources`: that table is the only thing mapping the
170
- // import back to a `ResourceIndex` (plan-format.md v0.1 amendment #2 /
171
- // 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
172
194
  // inside a call, as an unattributable `InvalidHandleError`.
173
195
  const resourceLeaves = this.leaves.filter((l) => l.kind === "resource");
174
196
  if (resourceLeaves.length > 0 &&
@@ -238,7 +260,7 @@ class Facade {
238
260
  #guestClass(b) {
239
261
  b.cls ??= buildGuestResourceClass({ name: b.name, ctor: null, ctorParams: null, methods: [], statics: [] },
240
262
  // The rt is supplied per wrapper, so an anonymous class needs none here.
241
- { impl: null, dtor: null }, () => Promise.reject(new TypeError("no methods")), () => []);
263
+ { impl: null, dtor: null }, () => () => Promise.reject(new TypeError("no methods")), () => []);
242
264
  return b.cls;
243
265
  }
244
266
  /**
@@ -249,7 +271,7 @@ class Facade {
249
271
  * construction — before instantiation, and therefore before a guest `start`
250
272
  * function can call an import that carries an `own`/`borrow` of one.
251
273
  * Imported resources occupy `ResourceIndex` 0..n-1 in `importedResources`
252
- * order (plan-format.md v0.1 amendment #2 / v0.2).
274
+ * order (the `importedResources` field, contracts/plan-format.md schema).
253
275
  */
254
276
  #bindHostResources() {
255
277
  const importedResources = this.artifacts.plan.importedResources ?? [];
@@ -328,7 +350,7 @@ class Facade {
328
350
  return rep;
329
351
  },
330
352
  dropOwn(rep, t) {
331
- // 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
332
354
  // stream element). Destroy it exactly as a guest-side drop would:
333
355
  // host-implemented R runs the instance's [Symbol.dispose] through
334
356
  // the registry; guest-implemented R runs the guest dtor via the
@@ -433,9 +455,10 @@ class Facade {
433
455
  }
434
456
  return impl(...raw);
435
457
  };
436
- // A1 brand relay, layer 2 of 2 (see #dispatcher): the executor reads the
437
- // brand off this wrapper, which is what lands in its hostImports record.
438
- return isSuspending(dispatch) ? suspending(wrapper) : wrapper;
458
+ // suspending mark/cancellation discard brand relay, layer 2 of 2 (see #dispatcher): the executor reads
459
+ // the brands off this wrapper, which is what lands in its hostImports
460
+ // record.
461
+ return relayMarks(dispatch, wrapper);
439
462
  }
440
463
  /** A host-implemented resource type: register the class, own the mapping. */
441
464
  #wrapResourceType(leaf, importIndex, provider) {
@@ -471,10 +494,11 @@ class Facade {
471
494
  throw new PlanError(`host import '${label(leaf)}' missing or not a function (got ` +
472
495
  `${describe(fn)}); expected '${camelCase(m.name)}'`);
473
496
  }
474
- // A1: the `suspending()` brand rides the dispatch closure so #wrapLeaf
475
- // can relay it onto the value the executor actually receives.
497
+ // suspending mark/cancellation discard: the `suspending()` and `deferCancel()` brands ride the
498
+ // dispatch closure so #wrapLeaf can relay them onto the value the
499
+ // executor actually receives.
476
500
  //
477
- // A2 receiver rule: an interface member is invoked with its containing
501
+ // suspending mark receiver rule: an interface member is invoked with its containing
478
502
  // object as receiver (matching the static arm's `apply(cls)`), so a
479
503
  // class INSTANCE is a fully supported spelling of an interface
480
504
  // provider — methods reading instance state work. A world-level bare
@@ -485,7 +509,7 @@ class Facade {
485
509
  // liberal-acceptance failure the contract forbids.)
486
510
  const receiver = leaf.path.length === 0 ? undefined : provider;
487
511
  const dispatch = (args) => fn.apply(receiver, args);
488
- return isSuspending(fn) ? suspending(dispatch) : dispatch;
512
+ return relayMarks(fn, dispatch);
489
513
  }
490
514
  const clsName = pascalCase(m.resource);
491
515
  // World-level member leaves resolved the class itself (`#provider`);
@@ -499,12 +523,12 @@ class Facade {
499
523
  switch (m.form) {
500
524
  case "constructor":
501
525
  // Never markable: guest-driven construction of a host resource is
502
- // synchronous by the C2 amendment, and stage-3 reserves no
526
+ // synchronous (contracts/embedder-api.md §"Functions and async"), and stage-3 reserves no
503
527
  // constructor-decorator position.
504
528
  // deno-lint-ignore no-explicit-any
505
529
  return (args) => new cls(...args);
506
530
  case "method": {
507
- // A2: the brand authority for an instance method is the CLASS
531
+ // suspending mark: the brand authority for an instance method is the CLASS
508
532
  // PROTOTYPE, read at wrap time — the Suspending-wrap decision is
509
533
  // per-declaration and taken at instantiation, before any instance
510
534
  // exists. Instance-level method overrides do not change
@@ -516,7 +540,7 @@ class Facade {
516
540
  // `URLSearchParams.prototype.size`) brand-checks its receiver, and a
517
541
  // raw `prototype[member]` read runs it with `this` = the prototype —
518
542
  // an engine TypeError at instantiation, even for guests that never
519
- // 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
520
544
  // mark (stage-3 method decorators install data properties), so an
521
545
  // accessor-backed member yields no wrap-time function here and stays
522
546
  // a call-time concern for the per-call lookup below.
@@ -530,7 +554,7 @@ class Facade {
530
554
  }
531
555
  return fn.apply(self, rest);
532
556
  };
533
- return isSuspending(protoFn) ? suspending(dispatch) : dispatch;
557
+ return relayMarks(protoFn, dispatch);
534
558
  }
535
559
  case "static": {
536
560
  const fn = cls[camelCase(m.member)];
@@ -538,11 +562,11 @@ class Facade {
538
562
  throw new PlanError(`host import '${label(leaf)}': ${clsName} has no static ` +
539
563
  `'${camelCase(m.member)}'`);
540
564
  }
541
- // 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
542
566
  // static-method decorator marks the function value), readable here
543
567
  // at wrap time.
544
568
  const dispatch = (args) => fn.apply(cls, args);
545
- return isSuspending(fn) ? suspending(dispatch) : dispatch;
569
+ return relayMarks(fn, dispatch);
546
570
  }
547
571
  }
548
572
  }
@@ -573,7 +597,7 @@ class Facade {
573
597
  return fromHost(v, resultType, o);
574
598
  };
575
599
  const fail = (e, args) => {
576
- // 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
577
601
  // that resolved a DIFFERENT runtime copy — or hand-rolled with the
578
602
  // registry symbol — is the same value here (issue #83).
579
603
  if (isComponentException(e) && isResult) {
@@ -599,9 +623,9 @@ class Facade {
599
623
  `only a fallible import may signal an error value`);
600
624
  }
601
625
  // The #83 signature: in a graph with several copies, an UNBRANDED throw
602
- // 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,
603
627
  // which does not survive the copy boundary). Say so rather than leaving
604
- // the latent puzzle that motivated amendment A9.
628
+ // the latent puzzle that motivated §"Module identity and @polyengine/protocol".
605
629
  const census = copyCensus();
606
630
  throw new Trap(`${where} threw ${describeThrow(e)}. An unbranded throw from a host ` +
607
631
  `import is a host bug and becomes a trap: signal a WIT error with ` +
@@ -609,12 +633,23 @@ class Facade {
609
633
  (census === ""
610
634
  ? ""
611
635
  : ` (${census} — an error carrying no polyengine brand in a ` +
612
- `multi-copy graph usually means a pre-A9 runtime copy threw ` +
636
+ `multi-copy graph usually means a pre-module identity runtime copy threw ` +
613
637
  `it, issue #83.)`));
614
638
  };
615
639
  return (...raw) => {
616
640
  const scope = new BorrowScope();
617
641
  const args = ft.params.map((p, i) => toHost(raw[i], p, o, scope));
642
+ // CONTRACT: anything the executor appended PAST the WIT-declared
643
+ // params is a runtime-minted extra, not a component value — today
644
+ // exactly the `abortable()` signal `createLoweredImport` adds for a
645
+ // marked import. It is forwarded verbatim (no `toHost` conversion: it
646
+ // has no `ValType` and must reach the host as the platform object it
647
+ // is). Without this the facade would silently drop the signal and a
648
+ // marked import's `signal` parameter would be forever `undefined` —
649
+ // the failure the mark exists to prevent. The slice is empty for every
650
+ // unmarked import, so no existing path changes shape.
651
+ for (let i = ft.params.length; i < raw.length; i++)
652
+ args.push(raw[i]);
618
653
  let out;
619
654
  try {
620
655
  out = dispatch(args);
@@ -624,7 +659,7 @@ class Facade {
624
659
  return fail(e, args);
625
660
  }
626
661
  if (isThenable(out)) {
627
- // Amendment A12: when the WIT result type is `future<T>`, a thenable
662
+ // When the WIT result type is `future<T>`, a thenable
628
663
  // return IS the future source ("for `future<T>`, a `Promise<T>` or
629
664
  // `Future<T>`" — §"Streams and futures"), not the call's async
630
665
  // completion. The import completes immediately with the lowered
@@ -719,8 +754,8 @@ class Facade {
719
754
  continue;
720
755
  }
721
756
  if (exp.kind === "module") {
722
- // Not WIT-expressible, digest-excluded (plan-format.md v4 amendment
723
- // 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
724
759
  // raw executor export surface still carries the compiled module.
725
760
  continue;
726
761
  }
@@ -754,8 +789,8 @@ class Facade {
754
789
  const s = spec(member.resource);
755
790
  // Prefer the plain-entered variant in jspi mode: the JS `new`
756
791
  // cannot await the Promise a promising-wrapped entry returns
757
- // (exec/boundary.ts CONSTRUCTOR_SYNC_ENTRY).
758
- s.ctor = (fn[CONSTRUCTOR_SYNC_ENTRY] ?? fn);
792
+ // (exec/boundary.ts SYNC_ENTRY).
793
+ s.ctor = (fn[SYNC_ENTRY] ?? fn);
759
794
  s.ctorParams = ft.params;
760
795
  rtOf(ft.results[0], specRt, member.resource);
761
796
  break;
@@ -766,6 +801,7 @@ class Facade {
766
801
  raw: fn,
767
802
  params: ft.params,
768
803
  results: ft.results,
804
+ async: ft.async === true,
769
805
  });
770
806
  rtOf(ft.params[0], specRt, member.resource);
771
807
  break;
@@ -776,6 +812,7 @@ class Facade {
776
812
  raw: fn,
777
813
  params: ft.params,
778
814
  results: ft.results,
815
+ async: ft.async === true,
779
816
  });
780
817
  break;
781
818
  }
@@ -787,7 +824,7 @@ class Facade {
787
824
  throw new PlanError(`export '${id}': resource '${name}' has leaves but no own/borrow ` +
788
825
  `type to identify it by`);
789
826
  }
790
- 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))));
791
828
  obj[claim(pascalCase(name), name)] = cls;
792
829
  const index = this.#tokenIndex.get(rt);
793
830
  if (index !== undefined) {
@@ -844,13 +881,14 @@ class Facade {
844
881
  #wrapExportFn(fn, ft, where) {
845
882
  const o = this.#opts(where);
846
883
  const resultType = ft.results.length === 0 ? null : ft.results[0];
884
+ let wrapper;
847
885
  if (resultType !== null && resultType.kind === "future") {
848
886
  // See `Future.deferred`: a `future<T>` result cannot be delivered
849
887
  // *through* a Promise, because promise resolution adopts thenables and
850
888
  // `Future<T>` is one. The handle is returned eagerly instead; it is
851
889
  // PromiseLike, so `await` still yields `T`.
852
890
  const element = resultType.element;
853
- return (...args) => {
891
+ wrapper = (...args) => {
854
892
  // Advisory 9: the generic branch checks arity; so must this one.
855
893
  if (args.length !== ft.params.length) {
856
894
  throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ` +
@@ -869,20 +907,111 @@ class Facade {
869
907
  return Future.deferred(pending, elementCodec(element, o));
870
908
  };
871
909
  }
872
- 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) => {
873
1002
  if (args.length !== ft.params.length) {
874
1003
  throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ${args.length}`);
875
1004
  }
876
1005
  const { lowered, release } = this.#lowerParams(ft.params, args, o);
877
1006
  let raw;
878
1007
  try {
879
- raw = await fn(...lowered);
1008
+ raw = entry(...lowered);
880
1009
  }
881
1010
  finally {
882
- // Call-scoped reps minted for `borrow<R>` arguments of a
883
- // host-implemented resource live exactly as long as the call.
884
1011
  release();
885
1012
  }
1013
+ if (isThenable(raw))
1014
+ unreachableThenable(raw);
886
1015
  if (resultType === null)
887
1016
  return undefined;
888
1017
  if (resultType.kind === "result") {
@@ -940,7 +1069,7 @@ function pick(container, path, names) {
940
1069
  /**
941
1070
  * Read a DATA property from `obj` (walking its prototype chain, nearest own
942
1071
  * descriptor wins) without ever invoking accessors. Accessor-backed and
943
- * 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
944
1073
  * probe, which must not run platform getters against a bare prototype.
945
1074
  */
946
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";