From ea9f1d23434e98ca77145aca2444f37709316113 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Wed, 6 May 2026 16:17:54 -0500 Subject: [PATCH 1/7] Initial design sketch --- design/mvp/Explainer.md | 2 + design/mvp/Web.md | 655 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 657 insertions(+) create mode 100644 design/mvp/Web.md diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index b0cee058..412d2fa1 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -3084,6 +3084,8 @@ In particular, the Component Model maintains the following invariants: ### JS API +***NOTE: This will be replaced by Web.md*** + The [JS API] currently provides `WebAssembly.compile(Streaming)` which take raw bytes from an `ArrayBuffer` or `Response` object and produces `WebAssembly.Module` objects that represent decoded and validated modules. To diff --git a/design/mvp/Web.md b/design/mvp/Web.md new file mode 100644 index 00000000..ac734271 --- /dev/null +++ b/design/mvp/Web.md @@ -0,0 +1,655 @@ +# Web API for Components + +This explainer describes how WebAssembly Components (hereafter 'components') can be used in a web engine. It could also be used in non-web engines (such as Node) that support the subset of WebIDL used in this document. + +This spec would be layered on a future component embedder interface (similar to how the JS-API is layered on the core spec embedder interface). + +**This is a draft and is not complete. Major details are unresolved, and there are bugs. See "Open Questions" at the end for a sampling of them.** + +## Goals + +1. Components can import and use most web and JS API's +2. Components can export an API useable by JS +3. Components interact with the web platform in similar ways to JS: + a. Components can feature test whether API's are present + b. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill + c. Components are tolerant of web API evolution + d. Components misuse of a web API's result in failure at that call-site, not link time errors +4. Components have improved performance when calling web API's compared to today + +## Non-goals + +1. Components importing every kind of web API +1. Components exporting any kind of JS API + +## Design + +To meet our goals, we need to define interactions (also known as 'bindings') between components, web API's, and JS. + +The scripting interface for web API's is handled (almost but not entirely) by WebIDL, so bindings for web API's effectively means bindings for WebIDL. WebIDL already has a "JavaScript Bindings" section which defines how JS interacts with WebIDL. There are no other bindings yet supported by WebIDL. + +There are roughly three paths forward here: + +1. Define bindings between components and JS - components transitively have access to web API's through the pre-existing JS-WebIDL bindings. +2. (1) and also define bindings between components and WebIDL - components get a separate direct path to web API's. +3. Define bindings between components and WebIDL - components transitively have access to JS through the pre-existing JS-WebIDL bindings. + +There are pros/cons to each. Let's go through them. + +### A. Define only bindings between Components and JS + +This is the smallest step from where we are today. A component's imports and exports are described in terms of JS values, and the web platform is reached the same way JS reaches it. + +Goals #1, #2 and #3 mostly fall out for free. Web API's are already exposed to JS, so importing one is just importing the JS function that reflects it, and exporting to JS is given by the binding. Feature testing, polyfilling and API evolution are all properties the WebIDL-JS binding already supports, so they keep working without us specifying anything new. + +The problem is goal #4. Once a call into a web API is defined as a call through JS, JS semantics are observable at every step. Lookups on the global object and on prototypes can be intercepted, argument coercion can run user code through `valueOf`, `toString` and iterators, and the callee may be a Proxy. An engine can speculate and fast-path the common case, but it cannot skip those steps in general. TODO(elaborate). + +JS (specifically ECMA-262) also is missing many concepts that components require. Components have resources, streams, sized integers and guaranteed-valid unicode strings. WebIDL has interface types, `ReadableStream`, sized integer types and `USVString`. JS just has objects and doubles. Going through JS means lowering all of those concepts down to their JS representations so that the JS-WebIDL bindings can immediately raise them back up. Both conversions still have to be specified, and information can be lost in the middle. + +### B. Define bindings between Components and JS and also Components and WebIDL + +This is a superset of option A, so it inherits the pros/cons of that. In addition, we add a parallel binding between components and WebIDL to get goal #4 as well. Components that only need to talk to JS use the JS binding, and components that use web API's use the WebIDL binding. + +The cost is that we write and maintain two bindings, and they have to harmonize. + +### C. Define only bindings between Components and WebIDL + +JS already has well-defined bindings to WebIDL. If we define bindings from components to WebIDL, we get direct and efficient access to web API's (goal #4) and transitively get access to JS (goals #1 and #2). + +Like #1 we only have one specification to draft and maintain. + +The open question is goal #3. We need to decide how feature testing, polyfills and API evolution work in the direct WebIDL binding. This is new conceptual ground that needs careful design. + +### Conclusion + +We should take option C. Option A has too many cons, while option B is twice the work to implement and maintain. Option C has the potential to get us everything we want at the smallest conceptual burden. + +## Walkthrough + +Let's walk through how this all works in practice. After this will be an in-depth explainer of the exact proposed rules. + +### A greeter + +Start with a component that imports nothing: + +```wit +package example:greeter; + +world greeter { + export greet: func(name: string) -> string; +} +``` + +Exports are converted to canonical WebIDL which is then exposed to JS through the existing WebIDL-to-JS machinery. A component `string` is a sequence of unicode scalar values, which is exactly what WebIDL calls a `USVString`, so this component is described as: + +```webidl +namespace { + USVString greet(USVString name); +}; +``` + +What JS gets is an ordinary object with an ordinary method on it: + +```js +const { instance } = await WebAssembly.instantiate(bytes); +instance.exports.greet("world"); // "hello, world" +``` + +The JS caller interacts with greet like any normal WebIDL operation. For example, `greet(42)` converts the number to a string and passes `"42"`, and `greet()` throws a `TypeError` for the missing argument. + +### A logger + +Now a component that imports: + +```wit +package example:logger; + +world logger { + import log: func(message: string); + export run: func(); +} +``` + +The obvious thing to pass is `console.log`: + +```js +const { instance } = await WebAssembly.instantiate(bytes, { + log: console.log, +}); +``` + +`console.log` is a web API, so the engine already knows its [WebIDL signature](https://console.spec.whatwg.org/#console-namespace): +``` +undefined log(any... data); +``` + +it takes any number of arguments of any type. The component's `message` is a string, and a string is one of the things it can take, so there is nothing to convert and nothing to check. + +#### Polyfilling it + +Now suppose `console.log` isn't available, or we want to capture the output. Pass a plain JS function instead: + +```js +const lines = []; +const log = (message) => { lines.push(message); }; +const { instance } = await WebAssembly.instantiate(bytes, { + log, +}); +``` + +A plain JS function has no WebIDL signature, so we treat it as one that takes anything and returns anything, and convert the component's values to JS values on the way in. + +Since both work, the choice can be made in JS before the component is instantiated: + +```js +const log = globalThis.console?.log ?? myPolyfill; +``` + +### Searching the DOM + +Now let's import a resource type and a more complex API. + +```wit +package example:search; + +interface dom { + resource element { + query-selector: func(selectors: string) -> option; + get-attribute: func(name: string) -> option; + scroll-into-view: func(align-to-top: bool); + } +} + +world search { + import dom; + export find: func(root: borrow, selectors: string) -> option; +} +``` + +To satisfy all of that, you can just import `Element` itself: + +```js +const { instance } = await WebAssembly.instantiate(bytes, { element: Element }); + +instance.exports.find(document.body, "h1"); // "page-title" or null +``` + +One import value covers the resource and all three of its methods. `Element` names the interface, and the methods are found on `Element.prototype`, which is where JS finds them too. Component names are kebab-case and JS names are camelCase, so `query-selector` is matched with `querySelector`. + +Binding the resource to `Element` also influences how the component's own exports look. Its `find` takes an element, so what JS sees is: + +```webidl +namespace { + USVString? find(Element root, USVString selectors); +}; +``` + +JS must pass a real element or else it gets a `TypeError`. + +#### When the API evolves + +`scroll-into-view` is interesting here, because `scrollIntoView` has evolved over time. It used to take a single boolean, but now it takes either a boolean or an options dictionary. The component above was written against the old version and still asks for a `bool`. + +This is okay. When an argument is allowed to be one of several types, we try the component's value against each of them and use the first one that fits, and a boolean still fits. + +Mismatched argument counts get a similar treatment. Extra arguments are dropped, and arguments the component doesn't pass behave as if a JS caller had left them out. + +Arguments that don't actually match do fail, but they fail at the call rather than at load. If a component asks for `scroll-into-view: func(align-to-top: string)`, a string is neither a boolean nor an options dictionary, so that call traps. Instantiation still succeeds, `find` still works, and a component that never calls `scroll-into-view` never traps. + +## The WebAssembly Namespace + +Extend the imperative WebAssembly JS-API interfaces to also allow validation/compilation/instantiation of components in addition to modules. + +```webidl +interface Component { + constructor([AllowResizable] AllowSharedBufferSource bytes); +} + +interface ComponentInstance { + constructor(Component component, object args); +} + +typedef (Component or Module) InstantiateSource; + +[Exposed=*] +namespace WebAssembly { + // Same as before, but now will detect if the bytes are a component or module and dispatch differently. + boolean validate([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise compile([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise instantiate( + [AllowResizable] AllowSharedBufferSource bytes, optional object importObject, optional WebAssemblyCompileOptions options = {}); + + // Now takes an InstantiateSource instead of just a Module. + Promise instantiate( + InstantiateSource moduleObject, optional object importObject); +} +``` + +## WebAssembly ESM-Integration + +TODO. + +## Names + +Component names are [`label`s](Explainer.md#import-and-export-names) and must be transformed when looking up what JS/Web interface they refer to. + +TODO: Define `pascal case`(|name|) +TODO: Define `camel case`(|name|) + +## Types and values + +Components and WebIDL maintain separate type systems, so any value crossing the boundary needs a defined translation in both directions. + +This section specifies that translation as four [abstract operations](https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations): + 1. CanonicalWebIDLType - pick the WebIDL type that best represents a given component value type + 2. ToCanonicalWebIDLValue - infallibly convert from a component value to a canonical WebIDL value + 3. FromCanonicalWebIDLValue - infallibly convert from a canonical WebIDL value to a component value + 4. CoerceWebIDLValue - convert from one WebIDL type to another + +### Resource types + +A component resource type in the web embedding is a [WebIDL object type](https://webidl.spec.whatwg.org/#dfn-object-type). Resource defined in a component are given a WebIDL interface that represents them as WebIDL object types. + +When a component imports a resource type, if a WebIDL [interface object](https://webidl.spec.whatwg.org/#dfn-interface-object) is given then the type of the interface it represents is used. Otherwise the generic `object` type is used instead. + +The interface object is what identifies the interface, not its constructor. Most interfaces on the platform are not constructible, since `new Element()` throws and `Element` has no `constructor` operation at all, but `Element` is still the value a JS author reaches for to name the type, and it is still the object carrying the prototype that `[method]` imports are resolved from. Keying on constructibility instead would make nearly every DOM interface unimportable. + +### CanonicalWebIDLType + +`CanonicalWebIDLType(componentValType)` computes the canonical WebIDL type used to represent a component value type. Specialized component types are handled directly rather than being despecialized first, since many have natural WebIDL counterparts. + +| Component type | Canonical WebIDL type | +|---|---| +| `bool` | `boolean` | +| `s8` / `u8` | `byte` / `octet` | +| `s16` / `u16` | `short` / `unsigned short` | +| `s32` / `u32` | `long` / `unsigned long` | +| `s64` / `u64` | `long long` / `unsigned long long` | +| `f32` / `f64` | `unrestricted float` / `unrestricted double` | +| `char` | `USVString` (length 1, asserted at conversion time) | +| `string` | `USVString` | +| `list` | `sequence` | +| `list` (fixed-length) | `sequence` (length-N invariant) | +| `record { f: T, ... }` | an anonymous `dictionary` type with required member `camel case(f)` of `CanonicalWebIDLType(T)` per field | +| `tuple` | `sequence` | +| `flags "L"+` | an anonymous `dictionary` type with optional `boolean` member `camel case(L)` per label, default `false` | +| `enum "L"+` | an anonymous `enumeration` with the same label set | +| `option` where T is not option<_> | `CanonicalWebIDLType(T)?` | +| `option` where T is option<_> | fallthrough to generic variant case below | +| `result` | fallthrough to generic variant case below. This is also special cased elsewhere when used as return value of a function. | +| `variant (case "L" T?)+` | an anonymous `dictionary { KindEnum kind; (union of case payload types)? value; }` and an anonymous `enumeration KindEnum` with the same label set | +| `own` / `borrow` | the WebIDL type chosen for `R` by `read the component type import` | +| `future` | `Promise` | +| `stream` | `ReadableStream`? | +| `error-context` | TODO | + +Notes: + - `f32`/`f64` map to `unrestricted float`/`unrestricted double` rather than the restricted forms because the component model permits NaN values, while restricted WebIDL float types forbid NaN and infinity. + - For `stream`, the element type `T` is not encoded into the WebIDL type; element-level conversion occurs at read time. + - `record` fields and `flags` labels are dictionary members, so they are `camel case`d. `enum` and `variant` case labels are enumeration values, which JS sees as strings, so they are used verbatim. See "Names". Two fields or labels of the same type that `camel case` to the same string are a link-time error, as elsewhere. + +### ToCanonicalWebIDLValue + +`ToCanonicalWebIDLValue(componentValue)` converts a component value to the canonical WebIDL value of type `CanonicalWebIDLType(componentValType)`. This algorithm is infallible. + +Dispatch on the component value type: +- `bool` → IDL `boolean` +- Integer types → IDL number of the matching IDL integer type +- `f32` / `f64` → IDL `unrestricted float` / `unrestricted double` +- `char` → `USVString` of length 1 from the Unicode scalar value +- `string` → `USVString` +- `list` → `sequence` with each element recursively converted by `ToCanonicalWebIDLValue` +- `record { f: T, ... }` → a `dictionary` value with each field recursively converted +- `tuple` → a `sequence` value with each field recursively converted +- `flags` → a `dictionary` value with each set label `true`, each unset label `false` +- `enum` → the `enumeration` value matching the label +- `option` where T is not option<_> → `null` for `none`; else `ToCanonicalWebIDLValue` the inner value. +- `variant` → `{ kind: label, value: ToCanonicalWebIDLValue(payload) }` (omit `value` for cases which don't have a payload) +- `own` / `borrow` → the host interface object wrapping the handle; `own` resources use a `FinalizationRegistry` to invoke the destructor; `borrow` wrappers are invalidated after the call returns +- `future` → an IDL `Promise` wrapping the future (TODO) +- `stream` → an IDL `ReadableStream` wrapping the stream (TODO) +- `error-context` → TODO + +### FromCanonicalWebIDLValue + +`FromCanonicalWebIDLValue(webIDLValue, targetComponentType)` converts a canonical WebIDL value back to a component value of `targetComponentType`. The algorithm is driven by `targetComponentType` and assumes `webIDLValue` is of type `CanonicalWebIDLType(targetComponentType)`. This algorithm is infallible. + +Each case is the inverse of the corresponding `ToCanonicalWebIDLValue` rule above. + +### CoerceWebIDLValue + +`CoerceWebIDLValue(fromWebIDLValue, toWebIDLType)` coerces a WebIDL value to a different WebIDL type. This algorithm is defined entirely over IDL values without invoking JavaScript semantics. It may throw `TypeError` (or `RangeError` under `[EnforceRange]`). + +Coercions are restricted to within the same [WebIDL overload type class](https://webidl.spec.whatwg.org/#idl-overloading) — numeric types coerce only to other numeric types, string types only to other string types, and so on. This gives the following invariant: if `CoerceWebIDLValue(v, t1)` and `CoerceWebIDLValue(v, t2)` both succeed, then `t1` and `t2` fall in the same overload type class and therefore are not distinguishable. Coercing a value will not change which overload should be selected. This is in contrast to JS, which performs two-step overload selection first comparing the JS value kind to find a candidate and then performing more permissive coercions to try and call the candidate. + +`fromWebIDLValue` may itself be `undefined` — e.g. a missing WebIDL operation argument with no declared default (see "create a component function for WebIDL operation" below). Each dispatch case below calls out its `undefined`-source behavior where it differs from throwing; where a rule mirrors a well-known ECMAScript abstract operation's behavior on `undefined` (`ToBoolean`, `ToNumber`, `ToString`), that's a description of the resulting value, not an invocation — the algorithm still never runs JavaScript semantics. + +Dispatch on `toWebIDLType`: + +- **`any`** — return `fromWebIDLValue` unchanged. +- **`undefined`** — accept only `undefined`; else throw `TypeError`. +- **`boolean`** — + - source `boolean`: identity. + - source `undefined`: `false` (matches `ToBoolean(undefined)`). + - other sources: throw `TypeError`. +- **Integer types** (`byte`, `octet`, `short`, `unsigned short`, `long`, `unsigned long`, `long long`, `unsigned long long`) — + - source any integer or float type: apply the IDL integer-conversion rules (modular reduction by default, clamping under `[Clamp]`, range check under `[EnforceRange]`) on the source's mathematical value. + - source `undefined`: treated as `NaN` (matches `ToNumber(undefined)`), then the same integer-conversion rules apply to that `NaN` — so `[EnforceRange]` throws (non-finite), `[Clamp]` clamps to `0`, and the default rule modularly reduces to `0`. + - source `bigint`: range-checked; valid only for `long long` and `unsigned long long`. + - other sources: throw `TypeError`. +- **Float types** (`float`, `unrestricted float`, `double`, `unrestricted double`) — + - source any integer or float type: convert by IEEE-754 round-to-nearest-even; restricted forms (`float`, `double`) throw `TypeError` for `NaN` or `±Infinity`. + - source `undefined`: treated as `NaN` (matches `ToNumber(undefined)`); as above, restricted forms throw and unrestricted forms keep the `NaN`. + - other sources: throw `TypeError`. +- **`bigint`** — + - source `bigint`: identity. + - source integer type: exact conversion. + - source float: the value must be a finite integer; else throw `TypeError`. + - other sources (including `undefined`, matching `BigInt(undefined)` throwing in JS): throw `TypeError`. +- **`DOMString`** — + - source `DOMString`, `USVString`, or `ByteString`: identity (re-typed). + - source `enumeration`: the label string. + - source `undefined`: the literal string `"undefined"` (matches `ToString(undefined)`). + - source `null` under `[LegacyNullToEmptyString]`: the empty string. + - other sources: throw `TypeError`. +- **`USVString`** — + - source `USVString`: identity. + - source `DOMString` or `ByteString`: replace lone surrogates with U+FFFD; reinterpret otherwise. + - source `enumeration`: the label string, then apply surrogate replacement. + - source `undefined`: the literal string `"undefined"` (already valid USV; no replacement needed). + - other sources: throw `TypeError`. +- **`ByteString`** — + - source `ByteString`: identity. + - source `DOMString` or `USVString`: each code unit must be `≤ U+00FF`; else throw `TypeError`. + - source `enumeration`: the label string, then check the range. + - source `undefined`: the literal string `"undefined"` (already valid ByteString). + - other sources: throw `TypeError`. +- **`object`** — accept any non-primitive IDL value (interface, dictionary, sequence, record, callback, Promise); else throw `TypeError` (including for `undefined`). +- **`symbol`** — accept only `symbol`; else throw `TypeError`. +- **Interface `I`** — accept iff the source is an interface value whose type is `I` or a derived interface of `I`; else throw `TypeError`. +- **Callback function** — accept iff the source is a callback; else throw `TypeError`. +- **`dictionary D`** — accept iff the source is a dictionary value (or a record whose entry set covers all required members of `D`). For each declared member `m: T` of `D`: retrieve `m` from the source and recurse with `CoerceWebIDLValue(srcM, T)`. Missing required member: throw `TypeError`. Extra members in the source are ignored. TODO: per real WebIDL, an `undefined` source should build an all-defaults dictionary instead of throwing, once dictionary coercion itself is specified in more detail. +- **Enumeration `E`** — accept iff the source is a string value (any string type, or another enumeration whose label is in `E`'s label set); else throw `TypeError` (an `undefined` source is therefore rejected unless a label is literally `"undefined"`). +- **`sequence`** — accept iff the source is a sequence (or frozen/observable array). Convert each element via `CoerceWebIDLValue(elem, T)`. +- **`record`** — accept iff the source is a `record. Convert each key via `CoerceWebIDLValue(k, K) and value via `CoerceWebIDLValue(v, V)`. +- **`T?` (nullable)** — if the source is `null` or `undefined`, return `null`; else `CoerceWebIDLValue(source, T)`. (A deliberate simplification: an `undefined` source could instead recurse into `T`'s own `undefined`-handling, but a missing nullable-typed value is simpler to just treat as `null` outright.) +- **Union types** — try each member type in declaration order; return the result of the first `CoerceWebIDLValue` call that does not throw. If all throw, throw `TypeError`. (An `undefined` source therefore succeeds against whichever member type accepts it, e.g. the first numeric or string member in declaration order.) +- **Buffer source types** — identity if the source is the same buffer-source kind; else throw `TypeError`. `[AllowShared]` and `[AllowResizable]` gate acceptance. +- **`FrozenArray`** / **`ObservableArray`** — as `sequence`, but produce a frozen or observable array. +- **`Promise`** — TODO. +- **`ReadableStream`** — TODO. + +Notes: +- `[Clamp]` and `[EnforceRange]` are properties of the target parameter or member site. They parameterize the integer-conversion rules above. +- This algorithm does not invoke any JavaScript abstract operation. All source values are fully-typed IDL values (including `undefined`, which is itself a valid IDL value, not a JS one). + +## Validation/Compilation + +Validation and compilation of components just defer to the underlying component embedding interface. This explainer adds nothing to it. + +## Instantiation + +Instantiating a component is a two step process: +1. `Read the imports object` to translate from web/js values to component values +1. `Create the exports object` to translate from component values to web/js values + +This is the core of the web embedding and where most of the logic lives. + +### Read the imports object + +The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the Core JS-API's algorithm of the same name. The resolved JS values are then handed to the per-kind algorithms (`read the component function import`, `read the component type import`, `read the component value import`) to produce the component definitions used during instantiation. + +While walking, the algorithm recognizes the common pattern of a resource type import accompanied by `[constructor]`, `[static]`, and `[method]` function imports tied to it. A resource type import is read first and should be given a WebIDL interface object (see "Resource Types"). The tagged function imports then read from that interface object and its prototype directly. This allows the common case of importing an interface to be satisfied by just passing the interface object. + +Every name looked up on a JS object is `camel case`d first (see "Names"). + +To `read the imports` given |component| and |importsObject|: +1. If |component| has no imports: + 1. Return an empty list. +1. If `Type`(|importsObject|) is not Object: + 1. Throw a `TypeError`. +1. If two names within any of the following groups `camel case` to the same string, throw a `TypeError`: + 1. The names of the imports that are resolved on |importsObject| (that is, every import except a `[constructor]`, `[method]` or `[static]` function import whose resource type is itself imported). + 1. For each resource type import R, the `[method]` names tied to R. + 1. For each resource type import R, the `[static]` names tied to R. +1. Let |resourceInterfaceObjects| be a new empty map keyed by resource type. +1. Let |imports| be a new empty list. + +1. For each |import| of |component|.Imports, in declaration order: + 1. If |import| is a type import: + 1. TODO: handle non-resource type imports. + 1. Let |importValue| be ? `GetV`(|importsObject|, `camel case`(|import|.Name)). + 1. Set |resourceInterfaceObjects|[|import|.ResourceType] to |importValue|. + 1. Else if |import| is a function import: + 1. If |import| is tagged `[constructor]`: + 1. Let R be the resource whose `own` type is the function's return type. + 1. Else if |import| is tagged `[method]`: + 1. Let R be the resource whose `borrow` type is the function's first parameter (the `self` position). + 1. Else if |import| is tagged `[static]`: + 1. Let R be the resource named in the `[static].` tag. + 1. Else: + 1. Let R be undefined. + + 1. If R is defined and |resourceInterfaceObjects|[R] exists: + 1. Let |interfaceObject| be |resourceInterfaceObjects|[R]. + 1. If tagged `[constructor]`: + 1. Let |importValue| be |interfaceObject|. + 1. Else if tagged `[static]`: + 1. Let |importValue| be ? `GetV`(|interfaceObject|, `camel case`(|import|.StaticName)). + 1. Else if tagged `[method]`: + 1. Let |prototype| be ? `GetV`(|interfaceObject|, "prototype"). + 1. If `Type`(|prototype|) is not Object, throw a `TypeError`. + 1. Let |importValue| be ? `GetV`(|prototype|, `camel case`(|import|.MethodName)). + 1. Else: + 1. Let |importValue| be ? `GetV`(|importsObject|, `camel case`(|import|.Name)). + 1. Else: + 1. Let |importValue| be ? `GetV`(|importsObject|, `camel case`(|import|.Name)). + + 1. Let |resolved| be `read a component import` given |import| and |importValue|. + 1. Append |resolved| to |imports|. +1. Return |imports|. + +To `read a component import` given |import| and |importValue|: +1. Match |import|.Kind: + 1. **Instance**: TODO. + 1. **Function**: return `read the component function import` given |import|.Type and |importValue|. + 1. **Type**: return `read the component type import` given |import|.TypeBound and |importValue|. + 1. **Value**: return `read the component value import` given |import|.Type and |importValue|. + +To `read the component function import` given |componentFuncType| and |importValue|: +1. If |importValue| is not callable: + 1. Throw TypeError. +1. If |importValue| is an exported component function: + 1. Return the wrapped component function. +1. If |importValue| is a WebIDL interface object: + 1. If the interface it represents has a constructor operation: + 1. Let |importValue| be that constructor operation. + 1. Else: + 1. Let |importValue| be an operation that throws a `TypeError` when invoked, matching what calling the interface object does. +1. Else if |importValue| is not a WebIDL operation: + 1. Let |importValue| = `create a WebIDL operation for a JS callable`. +1. Return `create a component function for WebIDL operation` for |importValue| + +To `read the component type import` given |componentTypeBound| and |importValue|: +1. If |componentTypeBound| is not `(sub resource)`: + 1. TODO. +1. If |importValue| is not a WebIDL interface object: + 1. Return WebIDL `object`. +1. Return the interface type that |importValue| represents. + +To `read the component value import` given |componentValType| and |importValue|: +1. Let |canonicalType| be `CanonicalWebIDLType`(|componentValType|). +1. Let |canonicalValue| be the result of converting |importValue| to IDL type |canonicalType| using WebIDL's [convert an ECMAScript value to an IDL value](https://webidl.spec.whatwg.org/#js-type-mapping) algorithm. If that algorithm throws, propagate the exception. +1. Return `FromCanonicalWebIDLValue`(|canonicalValue|, |componentValType|). + +Notes: + - Unlike function imports, value import conversion failures surface at instantiation, not at first use. + +To `create a WebIDL operation for a JS callable` given |callable|: +1. TODO: sketch this out more. +1. Return an operation with a `any (any...)` WebIDL signature that immediately invokes |callable|. + +To `create a component function for WebIDL operation` given |operation| and |componentFuncType|: +1. Let |paramComponentTypes| be |componentFuncType|.Params. +1. Let |returnComponentType| be |componentFuncType|.Return. +1. If |returnComponentType| is `result`: + 1. Let |okComponentType| = T. + 1. Let |errorComponentType| = E. + 1. Let |throwing| = true. +1. Else: + 1. Let |okComponentType| = |returnComponentType|. + 1. Let |throwing| = false. +1. If |operation| is an overload set: + 1. Compute |canonicalParamType_i| = `CanonicalWebIDLType`(|paramComponentTypes|[i]) for each i. + 1. Look for the unique overload whose declared parameter type at the distinguishing argument index has the same WebIDL overload type class as |canonicalParamType_i| at that index, considering only positions present in both. + 1. If an overload was found: + 1. Let |selectedOperation| be that overload. + 1. Else: + 1. let |selectedOperation| be a placeholder that traps when invoked. +1. Else: + 1. Let |selectedOperation| = |operation|. +1. Let |result| = Construct a component host function with type |componentFuncType| whose body, given component args [|v_0|, ..., |v_{N_c - 1}|]: + 1. If |selectedOperation| is the trap placeholder, trap. + 1. Let |declaredParamTypes| = |selectedOperation|.Params + 1. Let |N_o| = |declaredParamTypes|.length. + 1. If |selectedOperation|'s final declared parameter is variadic: + 1. Let |fixedCount| = |N_o| - 1. + 1. Let |variadicElemType| be that parameter's element type/ + 1. Else: + 1. Let |fixedCount| = |N_o|. + 1. Let |variadicElemType| be undefined. + 1. For each i in [0, |fixedCount|): + 1. If i < |N_c|: + 1. Let |args|[i] = `CoerceWebIDLValue`(`ToCanonicalWebIDLValue`(|v_i|), |declaredParamTypes|[i]). If this throws, trap. + 1. Else if the i-th declared parameter has a default value expression (WebIDL's `optional T x = defaultExpr`): + 1. Let |args|[i] be that default value, already of type |declaredParamTypes|[i]. + 1. Else: + 1. Let |args|[i] = `CoerceWebIDLValue`(`undefined`, |declaredParamTypes|[i]). If this throws, trap. + 1. Let |variadicArgs| be a fresh empty IDL sequence with element type |variadicElemType|. + 1. If |variadicElemType| is defined: + 1. For each j in [|fixedCount|, |N_c|): + 1. Append `CoerceWebIDLValue`(`ToCanonicalWebIDLValue`(|v_j|), |variadicElemType|) to |variadicArgs|. If this throws, trap. + 1. Pass |variadicArgs| as the variadic invocation arguments to |selectedOperation|. + 1. Else: + 1. Component args |v_{|fixedCount|}|, ..., |v_{N_c - 1}| are ignored when |N_c| > |fixedCount|. + 1. Invoke |selectedOperation|(|args|). + 1. If the invocation throws |error|: + 1. If the function is marked throwing: + 1. Let |canonicalError| = `CoerceWebIDLValue`(|error|, `CanonicalWebIDLType`(|errorComponentType|)). If this throws, trap. + 1. Return `result.error(`FromCanonicalWebIDLValue`(|canonicalError|, |errorComponentType|))`. + 1. Else: trap. + 1. Else: let |webIDLResult| = the returned WebIDL value. + 1. Let |canonicalReturn| = `CoerceWebIDLValue`(|webIDLResult|, `CanonicalWebIDLType`(|okComponentType|)). If this throws, trap. + 1. Let |componentResult| = `FromCanonicalWebIDLValue`(|canonicalReturn|, |okComponentType|). + 1. If |throwing|: + 1. Return `result.ok(|componentResult|)`. + 1. Else: + 1. Return |componentResult|. +1. Return |result|. + +Notes: +- Construction always succeeds. Type and arity mismatches surface as runtime traps when the function is invoked; not at instantiation time. +- Pre-resolved overload selection runs once at instantiation. The component import has a fixed function type that is used to select the closest overload. +- Param-length mismatches are JS-permissive: a missing arg uses its declared default value if the parameter has one, else falls back to `undefined` (subject to per-param `CoerceWebIDLValue` rules, including its `undefined`-source cases above); extras are dropped. +- Variadic operations are spread one-per-element from the component caller's trailing args. +- TODO: should we special case a list passed as the final argument to a variadic overload? +- TODO: can we get away with only ever having static overload selection? + +### Create the exports object + +The `create the exports object` algorithm analyzes the component's exports, builds a set of WebIDL fragments (interfaces, namespace members, dictionaries, enumerations) describing them, and then defers to WebIDL's existing [JS binding](https://webidl.spec.whatwg.org/#javascript-binding) to materialize JS values for those fragments. The returned object is a fresh JS object whose properties are the materialized exports. + +Tagged function exports are mapped to interface members just as in `read the imports`: +- `[constructor]`: The operation becomes the interface `R`'s constructor. By strong-uniqueness, there can only be one for an interface, and we don't have to worry about overloading a constructor. +- `[method].`: The operation becomes a regular interface member named `camel case`(|name|) on `R`. +- `[static].`: The operation becomes a static interface member named `camel case`(|name|) on `R`. + +Resource types become interfaces named `pascal case`(|name|), and everything else becomes a member named `camel case`(|name|); see "Names". + +To `create the exports object` given a |componentInstance|: +1. Let |fragments| be a new empty set of WebIDL fragments. +1. Let |resourceInterfaces| be a new empty map keyed by component resource type. +1. Let |namespace| be an fresh anonymous WebIDL `namespace` fragment that will host plain function and value exports. Add it to |fragments|. +1. For each |export| of |componentInstance|.|component|.Exports, in declaration order: + 1. Match |export|.Kind: + 1. **Type (resource)**: + 1. If the resource is re-exported from imports: + 1. Let |interface| be the WebIDL interface that was selected for that resource by `read the component type import` at instantiation. + 1. Else (resource defined in the component): + 1. Let |interface| be a fresh WebIDL `interface` fragment named `pascal case`(|export|.Name). + 1. Add a `[LegacyNamespace=|namespace|]` extended attribute to |interface|. + 1. Add |interface| to |fragments|. + 1. If no `[constructor]` export targets this resource: + 1. Give |interface| a constructor operation that throws when called (matching WebIDL's "no [Constructor]" semantics). + 1. Set |resourceInterfaces|[|export|.ResourceType] to |interface|. + 1. **Function**: + 1. Let |operation| be `create an operation from a component function` given |export|.Func. + 1. If |export| is tagged `[constructor]`: + 1. Let |interface| be |resourceInterfaces|[R]. + 1. Assert |interface| has no contructor operation yet. + 1. Add |operation| to |interface| as its constructor operation. + 1. Else if |export| is tagged `[method].`: + 1. Let |interface| be |resourceInterfaces|[R]. + 1. Add |operation| to |interface| as a regular interface member named `camel case`(|name|). + 1. Else if |export| is tagged `[static].`: + 1. Let |interface| be |resourceInterfaces|[R]. + 1. Add |operation| to |interface| as a static interface member named `camel case`(|name|). + 1. Else: + 1. Add |operation| to |namespace| as a regular member named `camel case`(|export|.Name). + 1. **Value**: + 1. Let |canonicalType| be `CanonicalWebIDLType`(|export|.Type) and |canonicalValue| be `ToCanonicalWebIDLValue`(|export|.Value). + 1. Add a constant of type |canonicalType| with value |canonicalValue| to |namespace|, named `camel case`(|export|.Name). + 1. **Instance**: + 1. TODO: Can we just recurse here? + 1. If |export| added a name to a fragment that already contained that name, throw a `TypeError`. +1. Let |exportsObject| be the result of [creating a namespace object](https://webidl.spec.whatwg.org/#namespace-object) for |namespace|. +1. Return |exportsObject|. + +Notes: +- Re-exported imported resources reuse the same WebIDL interface they were bound to at instantiation, so JS callers see the same identity on both sides of the boundary. +- Component-defined resources without a `[constructor]` export get an interface whose constructor throws. +- Component-defined resources generate a WebIDL interface without any inheritance. +- The WebIDL JS binding needs to be modified to handle an anonymous namespace that is not exposed on a global. This seems like a relatively simple modification to make. + +To `create an operation from a component function` given |componentFunc|: +1. Let |componentFuncType| be |componentFunc|.Type. +1. Let |componentParamTypes| be |componentFuncType|.Params. +1. Let |componentResultType| be |componentFuncType|.Result. +1. If |componentResultType| is `result` (top-level): + 1. Let |okComponentType| = T. + 1. Let |errorComponentType| = E. + 1. Let |throwing| = true. +1. Else: let + 1. Let |okComponentType| = |componentResultType|; + 1. Let |throwing| = false. +1. Let |webIDLParamTypes|[i] be `CanonicalWebIDLType`(|componentParamTypes|[i]) for each i +1. Let |webIDLResultType| be `CanonicalWebIDLType`(|okComponentType|). +1. Construct a WebIDL operation with parameter types |webIDLParamTypes| and return type |webIDLResultType|, whose body, given |webIDLParamValues|: + 1. For each i in |webIDLParamValues|: + 1. Let |componentParamValues|[i] = `FromCanonicalWebIDLValue`(|webIDLParamValues[i]|, |componentParamTypes|[i]). + 1. Let |componentResult| = Invoke |componentFunc| with [|componentParamValues|[0], ..., |componentParamValues|[n-1]]. + 1. TODO: What if the call traps? + 1. If |throwing| and |componentResult| is `error(`|e|`)`: + 1. Let |exception| be `create a component exception` for `|e|` + 1. Throw |exception|. + 1. Else if |throwing| and the result is `result.ok(`|v|`)`: + 1. Let |componentResult| be |v|. + 1. Else: + 1. Let |componentResult| be the returned component value. + 1. Return `ToCanonicalWebIDLValue`(|componentResult|). +1. Return the operation. + +To `create a component exception` for component value `|error|`: + 1. TODO: Create an instance of `ComponentException`, a derived interface of `DOMException`. + +## Open questions + +1. How can you dynamically pass different branches of a WebIDL union? + - The current rules work for statically passing different branches, but not dynamically. + - Passing a variant doesn't work. It's canonical WebIDL value is different from a union. +1. How to specify finalization and destructors? +1. How does own/borrow interact with WebIDL platform objects? +1. How do we support WebIDL callback function types? +1. How do we support downcasting/upcasting of WebIDL interfaces? +1. How to import/export attribute getters/setters? +1. How to export a component as an interface that is derived from another interface? From 8f2f12501415952810e791f8686a130a08ef3235 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Wed, 19 Aug 2026 14:34:04 -0500 Subject: [PATCH 2/7] Redesign as a JS-API --- design/mvp/Explainer.md | 224 +----------- design/mvp/JS-Explainer.md | 264 ++++++++++++++ design/mvp/JS-Reference.md | 714 +++++++++++++++++++++++++++++++++++++ design/mvp/Web.md | 655 ---------------------------------- 4 files changed, 979 insertions(+), 878 deletions(-) create mode 100644 design/mvp/JS-Explainer.md create mode 100644 design/mvp/JS-Reference.md delete mode 100644 design/mvp/Web.md diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 412d2fa1..55a3c2e2 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -3082,229 +3082,7 @@ In particular, the Component Model maintains the following invariants: ## JavaScript Embedding -### JS API - -***NOTE: This will be replaced by Web.md*** - -The [JS API] currently provides `WebAssembly.compile(Streaming)` which take -raw bytes from an `ArrayBuffer` or `Response` object and produces -`WebAssembly.Module` objects that represent decoded and validated modules. To -natively support the Component Model, the JS API would be extended to allow -these same JS API functions to accept component binaries and produce new -`WebAssembly.Component` objects that represent decoded and validated -components. The [binary format of components](Binary.md) is designed to allow -modules and components to be distinguished by the first 8 bytes of the binary -(splitting the 32-bit [`core:version`] field into a 16-bit `version` field and -a 16-bit `layer` field with `0` for modules and `1` for components). - -Once compiled, a `WebAssembly.Component` could be instantiated using the -existing JS API `WebAssembly.instantiate(Streaming)`. Since components have the -same basic import/export structure as modules, this means extending the [*read -the imports*] logic to support single-level imports as well as imports of -modules, components and instances. Since the results of instantiating a -component is a record of JavaScript values, just like an instantiated module, -`WebAssembly.instantiate` would always produce a `WebAssembly.Instance` object -for both module and component arguments. - -Types are a new sort of definition that are not ([yet][type-imports]) present -in Core WebAssembly and so the [*read the imports*] and [*create an exports -object*] steps need to be expanded to cover them: - -For type exports, each type definition would export a JS constructor function. -This function would be callable iff a `[constructor]`-annotated function was -also exported. All `[method]`- and `[static]`-annotated functions would be -dynamically installed on the constructor's prototype chain, making sure to -register `[get]` and `[set]` functions as getters and setters. In the case of -re-exports and multiple exports of the same definition, the same constructor -function object would be exported (following the same rules as WebAssembly -Exported Functions today). In pathological cases (which, importantly, don't -concern the global namespace, but involve the same actual type definition being -imported and re-exported by multiple components), there can be collisions when -installing constructors, methods and statics on the same constructor function -object. In such cases, a conservative option is to undo the initial -installation and require all clients to instead use the full explicit names -as normal instance exports. - -For type imports, the constructors created by type exports would naturally -be importable. Additionally, certain JS- and Web-defined objects that correspond -to types (e.g., the `RegExp` and `ArrayBuffer` constructors or any Web IDL -[interface object]) could be imported. The `ToWebAssemblyValue` checks on -handle values mentioned below can then be defined to perform the associated -[internal slot] type test, thereby providing static type guarantees for -outgoing handles that can avoid runtime dynamic type tests. - -Lastly, when given a component binary, the compile-then-instantiate overloads -of `WebAssembly.instantiate(Streaming)` would inherit the compound behavior of -the abovementioned functions (again, using the `layer` field to eagerly -distinguish between modules and components). - -For example, the following component: -```wat -;; a.wasm -(component - (import "one" (func)) - (import "two" (value string)) 🪙 - (import "three" (instance - (export "four" (instance - (export "five" (core module - (import "six" "a" (func)) - (import "six" "b" (func)) - )) - )) - )) - ... -) -``` -and module: -```wat -;; b.wasm -(module - (import "six" "a" (func)) - (import "six" "b" (func)) - ... -) -``` -could be successfully instantiated via: -```js -WebAssembly.instantiateStreaming(fetch('./a.wasm'), { - one: () => (), - two: "hi", 🪙 - three: { - four: { - five: await WebAssembly.compileStreaming(fetch('./b.wasm')) - } - } -}); -``` - -The other significant addition to the JS API would be the expansion of the set -of WebAssembly types coerced to and from JavaScript values (by [`ToJSValue`] -and [`ToWebAssemblyValue`]) to include all of [`valtype`](#type-definitions). -At a high level, the additional coercions would be: - -| Type | `ToJSValue` | `ToWebAssemblyValue` | -| ---- | ----------- | -------------------- | -| `bool` | `true` or `false` | `ToBoolean` | -| `s8`, `s16`, `s32` | as a Number value | `ToInt8`, `ToInt16`, `ToInt32` | -| `u8`, `u16`, `u32` | as a Number value | `ToUint8`, `ToUint16`, `ToUint32` | -| `s64` | as a BigInt value | `ToBigInt64` | -| `u64` | as a BigInt value | `ToBigUint64` | -| `f32`, `f64` | as a Number value | `ToNumber` | -| `char` | same as [`USVString`] | same as [`USVString`], throw if the USV length is not 1 | -| `record` | TBD: maybe a [JS Record]? | same as [`dictionary`] | -| `variant` | see below | see below | -| `list` | create a typed array copy for number types; otherwise produce a JS array (like [`sequence`]) | same as [`sequence`] | -| `string` | same as [`USVString`] | same as [`USVString`] | -| `tuple` | TBD: maybe a [JS Tuple]? | TBD | -| `flags` | TBD: maybe a [JS Record]? | same as [`dictionary`] of optional `boolean` fields with default values of `false` | -| `enum` | same as [`enum`] | same as [`enum`] | -| `option` | same as [`T?`] | same as [`T?`] | -| `result` | same as `variant`, but coerce a top-level `error` return value to a thrown exception | same as `variant`, but coerce uncaught exceptions to top-level `error` return values | -| `map` | `new Map(_)` | `Map`s directly or other objects via `Object.entries(_)` | -| `own`, `borrow` | see below | see below | -| `future` | to a `Promise` | from a `Promise` | -| `stream` | to a `ReadableStream` | from a `ReadableStream` | - -Notes: -* Function parameter names are ignored since JavaScript doesn't have named - parameters. -* If a function's result type list is empty, the JavaScript function returns - `undefined`. If the result type list contains a single unnamed result, then - the return value is specified by `ToJSValue` above. Otherwise, the function - result is wrapped into a JS object whose field names are taken from the result - names and whose field values are specified by `ToJSValue` above. -* In lieu of an existing standard JS representation for `variant`, the JS API - would need to define its own custom binding built from objects. As a sketch, - the JS values accepted by `(variant (case "a" u32) (case "b" string))` could - include `{ tag: 'a', value: 42 }` and `{ tag: 'b', value: "hi" }`. -* For `option`, when Web IDL doesn't support particular type - combinations (e.g., `(option (option u32))`), the JS API would fall back to - the JS API of the unspecialized `variant` (e.g., - `(variant (case "some" (option u32)) (case "none"))`, despecializing only - the problematic outer `option`). -* When coercing `ToWebAssemblyValue`, `own` and `borrow` handle types would - dynamically guard that the incoming JS value's dynamic type was compatible - with the imported resource type referenced by the handle type. For example, - if a component contains `(import "Object" (type $Object (sub resource)))` and - is instantiated with the JS `Object` constructor, then `(own $Object)` and - `(borrow $Object)` could accept JS `object` values. -* When coercing `ToJSValue`, handle values would be wrapped with JS objects - that are instances of the handles' resource type's exported constructor - (described above). For `own` handles, a [`FinalizationRegistry`] would be - used to drop the `own` handle (thereby calling the resource destructor) when - its wrapper object was unreachable from JS. For `borrow` handles, the wrapper - object would become dynamically invalid (throwing on any access) at the end - of the export call. -* When an imported JavaScript function is a built-in function wrapping a Web - IDL function, the specified behavior should allow the intermediate JavaScript - call to be optimized away when the types are sufficiently compatible, falling - back to a plain call through JavaScript when the types are incompatible or - when the engine does not provide a separate optimized call path. - - -### ESM-integration - -Like the JS API, [ESM-integration] can be extended to load components in all -the same places where modules can be loaded today, branching on the `layer` -field in the binary format to determine whether to decode as a module or a -component. - -When present, the [`external-id`](#import-and-export-definitions) attribute of -an `import` would be used as the [Module Specifier], thereby giving components -the same naming expressivity as JavaScript (in particular, for importing URLs). -In the absence of an `external-id`, the always-present, but syntactically- -restrictive, `externname` of the import would be used instead. - -The main remaining question is how to deal with component imports having a -single string as well as the new importable component, module and instance -types. Going through these one by one: - -For component imports of module type, we need a new way to request that the ESM -loader parse or decode a module without *also* instantiating that module. -Recognizing this same need from JavaScript, there is a TC39 proposal called -[Import Reflection] that adds the ability to write, in JavaScript: -```js -import Foo from "./foo.wasm" as "wasm-module"; -assert(Foo instanceof WebAssembly.Module); -``` -With this extension to JavaScript and the ESM loader, a component import -of module type can be treated the same as `import ... as "wasm-module"`. - -Component imports of component type would work the same way as modules, -potentially replacing `"wasm-module"` with `"wasm-component"`. - -In all other cases, the (single) string imported by a component is first -resolved to a [Module Record] using the same process as resolving the -[Module Specifier] of a JavaScript `import`. After this, the handling of the -imported Module Record is determined by the import type: - -For imports of instance type, the ESM loader would treat the exports of the -instance type as if they were the [Named Imports] of a JavaScript `import`. -Thus, single-level imports of instance type act like the two-level imports -of Core WebAssembly modules where the first-level has been factored out. Since -the exports of an instance type can themselves be instance types, this process -must be performed recursively. - -Otherwise, function or value imports are treated like an [Imported Default Binding] -and the Module Record is converted to its default value. This allows the following -component: -```wat -;; bar.wasm -(component - (import "./foo.js" (func (result string))) - ... -) -``` -to be satisfied by a JavaScript module via ESM-integration: -```js -// foo.js -export default () => "hi"; -``` -when `bar.wasm` is loaded as an ESM: -```html - -``` - +This has been moved to [JS-Overview.md](JS-Overview.md) and [JS-Reference.md](JS-Reference.md). ## Examples diff --git a/design/mvp/JS-Explainer.md b/design/mvp/JS-Explainer.md new file mode 100644 index 00000000..17c2158b --- /dev/null +++ b/design/mvp/JS-Explainer.md @@ -0,0 +1,264 @@ +# WebAssembly Components JS-API Explainer + +This explainer describes how WebAssembly Components (hereafter 'components') can be used from JS. + +See the [reference](./JS-Reference.md) for an in-depth walkthrough. + +**This is a draft and is not complete. Major details are unresolved. See "Status" at the end.** + +## Goals + +1. Components can import and use most web and JS API's +2. Components can export an API useable by JS +3. Components interact with the web platform in similar ways to JS: + a. Components can feature test whether API's are present + b. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill + c. Components are tolerant of web API evolution + d. Components misuse of a web API's result in failure at that call-site, not link time errors +4. Components have improved performance when calling web API's compared to today + +## Non-goals + +1. Components importing every kind of web API +1. Components exporting any kind of JS API + +## Design + +To meet our goals, we need to define interactions (also known as 'bindings') between components, web API's, and JS. + +The scripting interface for web API's is handled (almost but not entirely) by WebIDL, so bindings for web API's effectively means bindings for WebIDL. WebIDL already has a "JavaScript Bindings" section which defines how JS interacts with WebIDL. There are no other bindings yet supported by WebIDL. + +There are roughly three paths forward here: + +A. Define bindings between components and JS - components transitively have access to web API's through the pre-existing JS-WebIDL bindings. +B. (A) and also define bindings between components and WebIDL - components get a separate direct path to web API's. +C. Define bindings between components and WebIDL - components transitively have access to JS through the pre-existing JS-WebIDL bindings. + +There are pros/cons to each. Let's go through them. + +### A. Define only bindings between Components and JS + +This is the smallest step from where we are today. A component's imports and exports are described in terms of JS values, and the web platform is reached the same way JS reaches it. + +Goals #1, #2 and #3 mostly fall out for free. Web API's are already exposed to JS, so importing one is just importing the JS function that reflects it, and exporting to JS is given by the binding. Feature testing, polyfilling and API evolution are all properties the WebIDL-JS binding already supports, so they keep working without us specifying anything new. + +The objection to A has always been goal #4. If a call into a web API is defined as a call through JS, JS semantics are observable at every step. Lookups on the global object and on prototypes can be intercepted, argument coercion can run user code through `valueOf`, `toString` and iterators, and the callee may be a Proxy. An engine can try to speculate these away, but that is not always easy. + +### B. Define bindings between Components and JS and also Components and WebIDL + +This is a superset of option A, so it inherits the pros/cons of that. + +In addition, we add a parallel binding between components and WebIDL to get goal #4 as well. Components that only need to talk to JS use the JS binding, and components that use web API's use the WebIDL binding. + +The cost is that we write and maintain two bindings, and they have to harmonize. + +### C. Define only bindings between Components and WebIDL + +JS already has well-defined bindings to WebIDL. If we define bindings from components to WebIDL, we get direct and efficient access to web API's (goal #4) and transitively get access to JS (goals #1 and #2). + +Like A we only have one specification to draft and maintain. + +The cost is goal #3. Feature testing, polyfills and API evolution are all things A inherits and C has to reinvent, and that is new conceptual ground. + +### Conclusion + +We should take option A. Its one disadvantage against C was goal #4, and we believe that we can work around that by carefully writing value conversion rules so that engines can fuse conversion from component values to WebIDL without any speculation. + +## Walkthrough + +### A greeter + +Start with a component that imports nothing: + +```wit +package example:greeter; + +world greeter { + export greet: func(name: string) -> string; +} +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes); + +instance.exports.greet("world"); // "hello, world" +``` + +`exports` holds one property per export and `greet` is an ordinary function. Component names are kebab-case and JS names are camelCase, so an export named `greet-loudly` would be `greetLoudly`. + +Arguments are converted rather than type checked, the way a WebIDL operation converts its own: + +```js +instance.exports.greet(42); // "hello, 42" +instance.exports.greet(); // TypeError +``` + +Passing too few arguments is a `TypeError`. Extra arguments are ignored. + +### A logger + +Now a component that imports: + +```wit +package example:logger; + +world logger { + import log: func(message: string); + export run: func(); +} +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes, { log: console.log }); + +instance.exports.run(); // logs "hello" +``` + +The component's `message` becomes a String and we call `log` with it. Nothing inspects what `log` is, so any callable does, and a polyfill is as good as the real thing: + +```js +const lines = []; +const log = (message) => { lines.push(message); }; + +const { instance } = await WebAssembly.instantiate(bytes, { log }); +``` + +Which means feature testing is just JS, done before instantiating: + +```js +const log = globalThis.console?.log ?? myPolyfill; +``` + +### When a call fails + +A `result` return is not handed to JS as a value. On the way out it throws, and on the way in a thrown value is caught: + +```wit +package example:parse; + +world parser { + import lookup: func(key: string) -> result; + export parse: func(text: string) -> result; +} +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes, { + lookup: (key) => { throw `no such key: ${key}`; }, +}); + +instance.exports.parse("42"); // 42 + +try { + instance.exports.parse("$name"); +} catch (e) { + e instanceof WebAssembly.ComponentError; // true + e.payload; // "no such key: name" +} +``` + +`payload` is the `E` value converted to JS. In the other direction the thrown JS value is converted to `E`, so `lookup` returns `result.error("no such key: name")` and the component is free to handle it instead of propagating it. + +An import that throws where the component asked for a plain return type has nowhere to put the error, and traps. + +### Importing a resource + +Components see JS objects as resources. A resource type import and the functions on it are satisfied by a single JS value, the constructor: + +```wat +(component + (import "element" (type $element (sub resource))) + (import "[method]element.query-selector" (func + (param "self" (borrow $element)) (param "selectors" string) + (result (option (own $element))))) + (import "[method]element.get-attribute" (func + (param "self" (borrow $element)) (param "name" string) + (result (option string)))) + (export "find" (func + (param "root" (borrow $element)) (param "selectors" string) + (result (option string)))) +) +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes, { element: Element }); + +instance.exports.find(document.body, "h1"); // "page-title" or null +``` + +`Element` covers the type and both methods. The type import checks for `@@isWasmResourceOf`, and the methods are read off `Element.prototype` under their camelCase names, which is where JS finds them too. + +Because `find` takes a `borrow` of the *imported* type, JS keeps passing raw elements. Passing anything else fails the same brand check and is a `TypeError`, and `option` comes back as `null`. + +### Exporting a resource + +A resource a component defines and exports becomes a class: + +```wit +package example:counter; + +world w { + export api: interface { + resource counter { + constructor(); + increment: func() -> u32; + } + } +} +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes); +const { Counter } = instance.exports.api; + +using c = new Counter(); +c.increment(); // 1 +c.increment(); // 2 +``` + +Type names are PascalCase, so `counter` is `Counter`. `new` runs the component's `constructor`, methods live on `Counter.prototype`, and `Symbol.dispose` drops the handle. Dropping is what runs the component's destructor, so a `Counter` nobody disposes is dropped when it is collected, through a `FinalizationRegistry`. + +A `borrow` the component hands out is different: it is only valid for the duration of the call it appeared in, and using it afterwards is a `TypeError`. + +### Loading with ESM + +[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary, so a component loads anywhere a module does today. + +Each component import becomes a JS import, and its module specifier is the import's [`external-id`](Explainer.md#import-and-export-definitions) if it has one and its name otherwise: + +```wit +world my-component { + @external-id("https://esm.unpkg.com/slugify@1.6.6") + import slugify: func(text: string) -> string; +} +``` + +## Values at a glance + +| Component type | JS | +|---|---| +| `bool` | Boolean | +| `s8`-`s32`, `u8`-`u32` | Number, an exact integer | +| `s64`, `u64` | BigInt | +| `f32`, `f64` | Number, including NaN and infinities | +| `char` | String of exactly one Unicode scalar value | +| `string` | String, well formed | +| `list` | `Uint8Array` | +| `list`, `list`, `tuple` | Array | +| `record { a-b: T }` | null-prototype object, `{ aB }` | +| `flags "a" "b"` | null-prototype object of Booleans, `{ a, b }` | +| `enum "a" "b"` | String, the label verbatim | +| `option` | `null`, or the payload | +| `variant`, `option>` | `{ kind, value }` | +| `result` | thrown and caught in return position, else `{ kind, value }` | +| `map` | `Map` | +| `own`, `borrow` | the value the type import was given, or an instance of its class | +| `future`, `stream`, `error-context` | not yet specified | + +Conversions in are looser than conversions out, in the same places WebIDL's are. A `record` takes any object with the right own properties, a `list` takes an Array or any iterable, and a `map` takes a `Map`, an iterable of pairs, or a plain object when `K` is `string`. See [ToJSValue](./JS-Reference.md#tojsvalue) and [ToComponentValue](./JS-Reference.md#tocomponentvalue). + +## Status + +- `future`, `stream` and `error-context` have no binding yet, and neither do async start functions or top-level await. + +Everything else we know is open is collected in the reference's [open questions](./JS-Reference.md#open-questions). diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md new file mode 100644 index 00000000..a1d258b8 --- /dev/null +++ b/design/mvp/JS-Reference.md @@ -0,0 +1,714 @@ +# WebAssembly Components JS-API Reference + +This is the in-depth reference for the WebAssembly Component JS-API. See here for the higher-level [explainer](./JS-Explainer.md). + +**This is a draft and is not complete. Major details are unresolved, and there are bugs. See "Open Questions" at the end for a sampling of them.** + +## The WebAssembly Namespace + +Extend the imperative WebAssembly JS-API interfaces to also allow validation/compilation/instantiation of components in addition to modules. + +```webidl +interface Component { + constructor([AllowResizable] AllowSharedBufferSource bytes); +} + +interface ComponentInstance { + constructor(Component component, optional object importsObject); + readonly attribute object exports; +} + +typedef (Component or Module) InstantiateSource; + +[Exposed=*] +namespace WebAssembly { + // Same as before, but now will detect if the bytes are a component or module and dispatch differently. + boolean validate([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise compile([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise instantiate( + [AllowResizable] AllowSharedBufferSource bytes, optional object importObject, optional WebAssemblyCompileOptions options = {}); + + // Now takes an InstantiateSource instead of just a Module, and returns a + // ComponentInstance for a Component. + Promise<(Instance or ComponentInstance)> instantiate( + InstantiateSource moduleObject, optional object importObject); +} +``` + +We also add an error type for components that return `result<_, E>` to JS: + +```webidl +[Exposed=*] +interface ComponentError : Error { + constructor(optional DOMString message = "", optional any payload); + readonly attribute any payload; +}; +``` + +`payload` is the converted `E` value. See [Create the exports object](#create-the-exports-object). + +## Validation/Compilation + +Validation and compilation of components just defer to the underlying component embedding interface. This explainer adds nothing to it. + +## Names + +Component import/export `plainname's` contain [`label`s](Explainer.md#import-and-export-definitions) that must be transformed into an identifier for use with JS and the web. +Component import/export `interfacename's` (such as `wasi:http/handler@1.0.0`) have no JS name and are currently rejected with a `TypeError`. + +We define a `PascalCase(label)` and `CamelCase(label)` below which are used throughout this spec. + +| `label` | `PascalCase` | `CamelCase` | +|---|---|---| +| `element` | `Element` | `element` | +| `query-selector` | `QuerySelector` | `querySelector` | +| `inner-HTML` | `InnerHTML` | `innerHTML` | +| `XML-http-request` | `XMLHttpRequest` | `xmlHttpRequest` | +| `URL` | `URL` | `url` | +| `a1-2-3` | `A123` | `a123` | + +`LabelOf`(|name|), where |name| is a `plainname`, returns the label that names the definition in JS: +1. If |name| is `[method]r.n` or `[static]r.n`, return `n`. +1. If |name| is `[constructor]r`, return `r`. +1. Return |name|. + +`Fragments`(|label|): +1. Return the List of Strings produced by splitting |label| on occurrences of U+002D (-). The hyphens themselves are discarded. + +`Capitalize`(|fragment|): +1. If |fragment| is an `acronym`, return |fragment|. +1. Return |fragment| with its first character uppercased. + +`PascalCase`(|label|): +1. Let |fragments| be `Fragments`(|label|). +1. Let |result| be the empty String. +1. For each |fragment| of |fragments|: + 1. Set |result| to the string-concatenation of |result| and `Capitalize`(|fragment|). +1. Return |result|. + +`CamelCase`(|label|): +1. Let |fragments| be `Fragments`(|label|). +1. Let |result| be |fragments|[0] with every character lowercased. +1. For each |fragment| of |fragments| after the first: + 1. Set |result| to the string-concatenation of |result| and `Capitalize`(|fragment|). +1. Return |result|. + +The JS name of an import or export declaration is then: + +`JSName`(|decl|): +1. If |decl|.Name is an `interfacename`, throw a `TypeError`. +1. If |decl| is a type declaration, return `PascalCase`(`LabelOf`(|decl|.Name)). +1. Return `CamelCase`(`LabelOf`(|decl|.Name)). + +TODO: `a-b` and `AB` are [strongly-unique](Explainer.md#name-uniqueness) but both `PascalCase` to the identical `AB`. This can lead to collisions in exports. We don't handle this yet. + +## Types and values + +Components and JS maintain separate type/value systems, so any value crossing the boundary needs a defined translation in both directions. + +This section specifies that translation as two abstract operations: + 1. `ToJSValue` - convert a component value to a JS value. Infallible. + 2. `ToComponentValue` - convert a JS value to a component value of a given type. Fallible. + +For every component value type `t` and every component value `v` of type `t`, `ToComponentValue(ToJSValue(v, t), t)` is `v`. The one exception being a `map` with duplicate keys (see [`ToJSValueMap`](#tojsvalue)). + +The abstract operations are carefully designed so that JS scripts cannot intercept round-tripping a component value through JS, or converting a component value to/from a WebIDL value. This allows JS engines to easily fuse conversions and skip creation of intermediate JS values. This is explained in more detail [later](#fusing-component-value-conversions). + +### ToJSValue + +`ToJSValue(componentValue, componentValType)` converts a component value to a JS value. This algorithm is infallible. + +Dispatch on `componentValType`: + +- `bool` → Boolean. +- Integer types other than `s64`/`u64` → Number, an exact integer. +- `s64` / `u64` → BigInt. +- `f32` / `f64` → Number, including NaN and infinities. +- `char` → String containing exactly the one Unicode scalar value. +- `string` → String [(well formed)](https://tc39.es/ecma262/#sec-isstringwellformedunicode). +- `list` → Uint8Array. +- `list` → `ToJSValueList`(the elements, T). +- `list` → as `list`; `length` is `N`. +- `tuple` → as `list`, with element `i` converted as `T_i`. +- `record { f: T, ... }` → `ToJSValueRecord`(|componentValue|, the fields). +- `flags "L"+` → `ToJSValueFlags`(|componentValue|, the labels). +- `enum "L"+` → String, the label verbatim. +- `option` where T is not `option<_>` → `null` for `none`, else `ToJSValue`(the payload, T). +- `variant`, and `option>`, and `result` outside return position → `ToJSValueVariant`(|componentValue|, the cases). In return position a `result` is unwrapped instead, into a return value or a thrown `ComponentError` (see [Read the imports](#read-the-imports-object) and [Create the exports object](#create-the-exports-object)). +- `map` → `ToJSValueMap`(|componentValue|, K, V). +- `own` / `borrow` → the JS value for an imported `R`, an instance of `R`'s [resource class](#guest-resource-classes) for a component-defined `R`. See [Resource types](#resource-types). +- `future` → a Promise (TODO). +- `stream` → a `ReadableStream` (TODO). +- `error-context` → TODO. + +`ToJSValueList(values, T)` returns a *component list object*: +1. Let |n| be the number of |values|. +1. Let |array| be `ArrayCreate`(|n|). +1. For each i in [0, |n|): perform `CreateDataPropertyOrThrow`(|array|, `ToString`(i), `ToJSValue`(|values|[i], T)). +1. Perform `DefinePropertyOrThrow`(|array|, `@@iterator`, PropertyDescriptor { [[Value]]: `%ComponentListValues%`, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **false** }). +1. Return |array|. + +A *component list object* is mostly an ordinary Array object, with the exception of that non-configurable own `@@iterator`. This is important [for fusing value conversions](#fusing-component-value-conversions). `%ComponentListValues%` is a new built-in function that behaves like `%Array.prototype.values%` except that the iterator object it returns: + - has a null prototype, + - has an own, non-writable, non-configurable `next` method, + - and returns, from `next`, a fresh null-prototype object with own `value` and `done` data properties. + +`ToJSValueRecord(value, fields)`: +1. Let |object| be `OrdinaryObjectCreate`(**null**). +1. For each field `f: T` of |fields|, in declaration order, perform `CreateDataPropertyOrThrow`(|object|, `CamelCase`(f), `ToJSValue`(|value|'s `f`, T)). +1. Return |object|. + +`ToJSValueFlags(value, labels)`: +1. Let |object| be `OrdinaryObjectCreate`(**null**). +1. For each label `L` of |labels|, perform `CreateDataPropertyOrThrow`(|object|, `CamelCase`(L), |value|'s `L` bit as a Boolean). +1. Return |object|. + +`ToJSValueVariant(value, cases)`: +1. Let |object| be `OrdinaryObjectCreate`(**null**). +1. Perform `CreateDataPropertyOrThrow`(|object|, "kind", |value|'s case label as a String). +1. If that case has a payload of type `T`, perform `CreateDataPropertyOrThrow`(|object|, "value", `ToJSValue`(the payload, T)). +1. Return |object|. + +`ToJSValueMap(value, K, V)`: +1. Let |map| be a new ordinary `Map` object with an empty [[MapData]]. +1. For each pair (k, v) of |value|, in order: + 1. Let |key| be `ToJSValue`(k, K) and |mapValue| be `ToJSValue`(v, V). + 1. If [[MapData]] has an entry whose key is `SameValueZero` to |key|, set that entry's value to |mapValue|. + 1. Else, append an entry (|key|, |mapValue|) to [[MapData]]. +1. Return |map|. + +A `map` is a [specialization](Explainer.md#type-definitions) of `list>` where the last pair for a key defines its value. So `[(a,1),(a,2)]` round-trips from a component value to JS and back as `[(a,2)]`. This is the one exception to the round-tripping rules we have. + +### ToComponentValue + +`ToComponentValue(jsValue, targetComponentType)` converts a JS value to a component value. It may throw if the JS value doesn't match the component value type. + +Dispatch on `targetComponentType`: + +- `bool` → `ToBoolean`(|jsValue|). +- Integer types → `ToComponentValueInteger`(|jsValue|, the type). +- `f32` / `f64` → `ToNumber`(|jsValue|); for `f32`, round to the nearest f32 value (ties to even). `NaN` and infinities are accepted, as with `unrestricted float`/`unrestricted double`. +- `char` → `ToString`(|jsValue|); it must consist of exactly one Unicode scalar value, else throw a `TypeError`. A lone surrogate is not a scalar value and is therefore a `TypeError`. +- `string` → `ToString`(|jsValue|), then replace each unpaired surrogate with U+FFFD, matching WebIDL `USVString`. +- `list` → `new Uint8Array(ToComponentValueList(|jsValue|, u8))` +- `list` → `ToComponentValueList`(|jsValue|, T). +- `list` → as `list`, then the length must be exactly `N`, else throw a `TypeError`. +- `tuple` → as `list`, then the length must be exactly the arity, and element `i` converts to `T_i`. +- `record { f: T, ... }` → `ToComponentValueRecord`(|jsValue|, the fields). +- `flags "L"+` → `ToComponentValueFlags`(|jsValue|, the labels). +- `enum` → `ToString`(|jsValue|) must be one of the labels, else throw a `TypeError`. +- `option` where T is not `option<_>` → `null` and **undefined** both give `none`; anything else gives `some(ToComponentValue(jsValue, T))`. This matches how WebIDL treats a nullable type. +- `variant`, and `option>`, and `result` outside return position → `ToComponentValueVariant`(|jsValue|, the cases). +- `map` → `ToComponentValueMap`(|jsValue|, K, V). +- `own` / `borrow` → a [host resource value](#host-resource-types-and-values) for an imported `R`, the rep held by the given instance of `R`'s [resource class](#guest-resource-classes) for a component-defined `R`. See [Resource types](#resource-types). +- `future` → TODO. +- `stream` → TODO. +- `error-context` → TODO. + +`ToComponentValueInteger(jsValue, t)`: +1. If |t| is `s64` or `u64` and `Type`(|jsValue|) is BigInt, let |n| be |jsValue|'s value. +1. Else, let |n| be ? `ToNumber`(|jsValue|) put through WebIDL's [integer conversion](https://webidl.spec.whatwg.org/#abstract-opdef-converttoint) **as if `[EnforceRange]` were present**: `NaN` and infinities throw a `TypeError`, anything else truncates toward zero. +1. If |n| is outside |t|'s range, throw a `TypeError`. +1. Return |n|. + +`ToComponentValueList(jsValue, T)`: +1. If |jsValue| is not an Object, throw a `TypeError`. +1. If ? `IsArray`(|jsValue|) is **true** and |jsValue| is not a Proxy exotic object: + 1. Let |len| be ? `LengthOfArrayLike`(|jsValue|). + 1. For each i in [0, |len|): let |e_i| be ? `Get`(|jsValue|, `ToString`(i)), and append `ToComponentValue`(|e_i|, T). +1. Else: + 1. Let |method| be ? `GetMethod`(|jsValue|, `@@iterator`). If |method| is **undefined**, throw a `TypeError`. + 1. Iterate as WebIDL's sequence conversion does, converting each value with `ToComponentValue`(_, T). + +The `Array` case does not check `@@iterator`, so a patched `Array.prototype[@@iterator]` does not change what a component sees when handed an Array. This is important for [fusing value conversions](#fusing-component-value-conversions). + +`ToComponentValueRecord(jsValue, fields)`: +1. If |jsValue| is not an Object, throw a `TypeError`. +1. For each field `f: T` of |fields|, in declaration order: + 1. Let |m| be ? `GetOwnProperty`(|jsValue|, `CamelCase`(f)). + 1. If |m| is **undefined** and `T` is not `option<_>`, throw a `TypeError`. + 1. The field value is `ToComponentValue`(|m|, T). + +Extra properties are ignored. + +`ToComponentValueFlags(jsValue, labels)`: +1. If |jsValue| is not an Object, throw a `TypeError`. +1. For each label `L` of |labels|, the bit is `ToBoolean`(? `GetOwnProperty`(|jsValue|, `CamelCase`(L))). + +An absent property is therefore `false`, matching a `boolean` dictionary member defaulted to `false`. + +`ToComponentValueVariant(jsValue, cases)`: +1. If |jsValue| is not an Object, throw a `TypeError`. +1. Let |kind| be `ToString`(? `GetOwnProperty`(|jsValue|, "kind")). It must be the label of one of |cases|, else throw a `TypeError`. +1. If that case has a payload type `T`, its payload is `ToComponentValue`(? `GetOwnProperty`(|jsValue|, "value"), T). Otherwise `value` is ignored. +1. Return that case. + +`ToComponentValueMap(jsValue, K, V)`: +1. If |jsValue| is not an Object, throw a `TypeError`. +1. If |jsValue| has a [[MapData]] internal slot: + 1. Return one pair per entry of [[MapData]], in insertion order, converting each key with `ToComponentValue`(_, K) and each value with `ToComponentValue`(_, V). +1. If ? `GetMethod`(|jsValue|, `@@iterator`) is not **undefined**: + 1. Return `ToComponentValueList`(|jsValue|, `tuple`). +1. If `K` is not `string`, throw a `TypeError`. +1. Return one pair per own enumerable string-keyed property of |jsValue|, in property order, converting each value with `ToComponentValue`(_, V). + +The `Map` case does not check `@@iterator`, so a patched `Map.prototype[@@iterator]` does not change what a component sees when handed a Map. This is important for [fusing value conversions](#fusing-component-value-conversions). +If a `Map` or `Iterable` is not provided, then we fallback to converting an object following `record` rules for compat with WebIDL. + +## Resource types + +A component resource type can be defined in a component (i.e. a guest resource), or else as an imported abstract type (i.e. a host resource). + +The component JS-API defines: + 1. A protocol for defining host resource types in JS. + 2. A spec representation of host resource types and values. + 3. A JS representation of guest resource types and values. + +### Embedder extensions + +We sketch two things here that should be formalized more fully in the [embedding interface](CanonicalABI.md#embedding). + +To `create a host resource type` given a host function |destructor|: +1. Return a fresh component resource type that whose representation is host-defined and whose destructor is |destructor|. + +To `drop a host owned resource` given a resource type |resourceType| and a rep |rep| owned by the host: +1. Perform the effect of [`canon resource.drop`](CanonicalABI.md#canon-resourcedrop) on an owning handle holding |resourceType| and |rep|, invoking |resourceType|'s destructor. There is no handle table entry to remove, because the host was holding the rep. +1. If that traps, throw a `WebAssembly.RuntimeError`. + +### Host resource types (i.e. imported) + +#### The host resource type protocol + +We add a new well-known symbol, `@@isWasmResourceOf`, whose value is a predicate over JS values: + +```js +Constructor[Symbol.isWasmResourceOf] = (v) => /* return true iff v is an instance of resource type */; +``` + +A resource type import will check for this symbol during instantiation and snapshot it. The type check will be invoked each time a JS value needs to be converted to a resource value. + +If `@@isWasmResourceOf` is not found, then one is synthesized that performs an `instanceof` check. + +WebIDL is extended to define this property on every [interface object](https://webidl.spec.whatwg.org/#dfn-interface-object), returning **true** if and only if its argument is a platform object that [implements](https://webidl.spec.whatwg.org/#implements) that interface. + +#### Host resource types and values + +A *host resource type* is what the JS-API creates to satisfy a resource type import. It is a Record with the following fields: + +| Field | Value | +|---|---| +| [[ComponentResourceType]] | the component resource type produced by `create a host resource type` | +| [[ImportValue]] | the JS object that satisfied the import | +| [[IsWasmResourceOf]] | the type check snapshotted from that object | + +A *host resource value* is the `rep` of a host resource type. It too is a Record: + +| Field | Value | +|---|---| +| [[Type]] | the host resource type this is a rep of | +| [[JSValue]] | the JS value, held strongly | + +A host resource value just holds a strong reference to the underlying value. No user-level destructors are run when it is dropped. + +To `read the type import` given |componentTypeBound| and |importValue|: +1. If |componentTypeBound| is not `(sub resource)`: + 1. Throw a `TypeError`. +1. If `Type`(|importValue|) is not Object: + 1. Throw a `TypeError`. +1. Let |isWasmResourceOf| be ? `GetV`(|importValue|, `@@isWasmResourceOf`). +1. If |isWasmResourceOf| is not callable: + 1. Let |isWasmResourceOf| be a built-in function that, given |jsValue|, returns ? `InstanceofOperator`(|jsValue|, |importValue|). +1. Let |destructor| be a host function that, given a host resource value, releases its reference to [[JSValue]] and returns. +1. Let |resourceType| be `create a host resource type` given |destructor|. +1. Return a host resource type whose [[ComponentResourceType]] is |resourceType|, whose [[ImportValue]] is |importValue| and whose [[IsWasmResourceOf]] is |isWasmResourceOf|. + +One host resource type is created per resource type import declaration per instantiation. Two type imports satisfied by the same JS constructor become distinct component resource types, and a handle for one cannot be passed where the other is expected. Round tripping such a handle through JS does succeed, because JS only ever sees the wrapped value. + +#### Conversions for host resource types + +For a resource type `R` whose type variable is one of the component's type imports, let |hostType| be the host resource type `read the type import` produced for it: + +- `ToJSValue(rep, own | borrow)`: + 1. Assert: |rep| is a host resource value whose [[Type]] is |hostType|. + 1. Return |rep|.[[JSValue]]. +- `ToComponentValue(jsValue, own | borrow)`: + 1. If ? `Call`(|hostType|.[[IsWasmResourceOf]], **undefined**, « |jsValue| ») is not **true**, throw a `TypeError`. + 1. Return a host resource value whose [[Type]] is |hostType| and whose [[JSValue]] is |jsValue|. + +Converting the same JS value to a host resource type will yield fresh handle indices. There is no canonicalization based on reference equality. + +### Guest resource types (i.e. exported) + +#### Re-exported host resource types + +A component can export an imported resource type in one of two ways: + 1. Transparently - by leaving it `eq`-bound to the import + 2. Opaquely - by ascribing it with `(sub resource)` + +This is visible in the component type that the embedder interface can inspect. + +Transparent re-exports on top-level components are disallowed and trap during instantiation. This avoids the problem of figuring out how to mutate a pre-existing prototype to add new methods exported by a component. + +Opaque re-exports are allowed and wrap the original host resource type in a new guest resource class. This prevents leaking of the implementation decision of whether the resource type export is from an import or defined in the component. + +#### Guest resource classes + +An exported resource type is given a JS class. + +A component's type presents each of its exported resource types as an abstract type variable. A unique JS class is created for each type variable. + +For example, the following will create a class for "r1" and "r3", while "r2" will re-use "r1"'s class. + +```wat +(component + (export "r1" (type $r1 (sub resource))) + (export "r2" (type (eq $r1))) + (export "r3" (type (sub resource))) +) +``` + +Guest resource classes are created in multiple phases: + 1. Create constructor and prototype *shells* before instantiation + 2. Instantiate the component, possibly running `start` functions + 3. Finish creating the constructor and prototype, *linking* the methods from the exports + +This allows any resource values that escape during `start` to have a fixed prototype already created. + +A resource class is a built-in function object with one extra internal slot, [[ConstructorFunc]], holding the component function that implements `new` or **empty**. + +To `create resource class shells` given a |component|: +1. For each type export |export| of |component|'s type, in declaration order, recursing into exported instances: + 1. Let |variable| be the abstract type |export| designates. + 1. If |variable| is one of |component|'s type imports, throw a `TypeError`. + 1. If a resource class is already associated with |variable| for this instantiation: + 1. Continue. + 1. Let |arity| be the parameter count of the `[constructor]` export targeting |variable|, or 0 if there is none. + 1. Let |class| be `create a resource class shell` given `JSName`(|export|) and |arity|. + 1. Associate |class| with |variable| for this instantiation. + +To `create a resource class shell` given a String |name| and an integer |arity|: +1. Let |prototype| be `OrdinaryObjectCreate`(`%Object.prototype%`). +1. Let |constructor| be a built-in function object with name |name|, length |arity| and a [[ConstructorFunc]] internal slot set to **empty**, whose [[Call]] throws a `TypeError`, and whose [[Construct]], given JS arguments |args| and |newTarget|, performs: + 1. If |constructor|.[[ConstructorFunc]] is **empty**, throw a `TypeError`. + 1. Let |rep| be ? `invoke a component function` given |constructor|.[[ConstructorFunc]], `[constructor]`, **undefined** and |args|. + 1. Let |resourceType| be the runtime resource type |constructor|.[[ConstructorFunc]]'s `own` result refers to. + 1. Return `create a resource instance` given |constructor|, |resourceType|, |rep|, **true** and |newTarget|. +1. Perform `DefinePropertyOrThrow`(|prototype|, `@@dispose`, PropertyDescriptor { [[Value]]: a built-in function that performs `drop a resource instance` given its **this** value, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. Perform `DefinePropertyOrThrow`(|prototype|, `@@toStringTag`, PropertyDescriptor { [[Value]]: |name|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. Perform `DefinePropertyOrThrow`(|prototype|, "constructor", PropertyDescriptor { [[Value]]: |constructor|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. Perform `DefinePropertyOrThrow`(|constructor|, "prototype", PropertyDescriptor { [[Value]]: |prototype|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **false** }). +1. Perform `DefinePropertyOrThrow`(|constructor|, `@@isWasmResourceOf`, PropertyDescriptor { [[Value]]: a built-in predicate that returns **true** if and only if its argument has a [[ResourceClass]] internal slot whose value is |constructor|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. Return |constructor|. + +To `link resource classes` given a |componentInstance|: +1. For each type export |export| of |componentInstance|, in declaration order, recursing into exported instances: + 1. Let |variable| be the type variable |export| designates and |class| be the resource class associated with |variable|. + 1. If |class| was already linked by an earlier iteration, continue. + 1. Let |tagged| be the `[constructor]`, `[method]` and `[static]` function exports in |export|'s scope that target |variable|. + 1. If |tagged| has a `[constructor]` export |c|, set |class|.[[ConstructorFunc]] to |c|.Func. + 1. For each `[method]` export |m| of |tagged|: + 1. Perform `DefinePropertyOrThrow`(|class|'s `"prototype"`, `JSName`(|m|), PropertyDescriptor { [[Value]]: `create a JS function for a component function` given |m|.Func, `JSName`(|m|) and |m|'s tag, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). + 1. For each `[static]` export |s| of |tagged|, define the corresponding property on |class| with the same attributes. + +#### Guest resource instances + +An instance of a guest resource class holds the same state a handle table entry does, plus the class it belongs to: + +| Slot | Value | +|---|---| +| [[ResourceClass]] | the resource class this is an instance of | +| [[ResourceType]] | the runtime component resource type | +| [[Rep]] | the rep, or **empty** once the handle has been dropped, transferred away, or expired | +| [[Own]] | whether this instance owns the resource | +| [[LendCount]] | how many outstanding `borrow`s were lent from this instance | + +For each instantiation: a class, a type variable and a runtime resource type are all in one-to-one correspondence, so [[ResourceClass]] is what the conversions type check against and [[ResourceType]] is only there to drop the resource with. + +To `create a resource instance` given a resource class |class|, a runtime resource type |resourceType|, |rep|, |own| and an optional |newTarget|: +1. Let |defaultProto| be the value of |class|'s `"prototype"` property. +1. If |newTarget| is present: + 1. Let |proto| be ? `Get`(|newTarget|, "prototype"). + 1. If `Type`(|proto|) is not Object, set |proto| to |defaultProto|. +1. Else, let |proto| be |defaultProto|. +1. Let |instance| be `OrdinaryObjectCreate`(|proto|, « [[ResourceClass]], [[ResourceType]], [[Rep]], [[Own]], [[LendCount]] »). +1. Set |instance|.[[ResourceClass]] to |class|. +1. Set |instance|.[[ResourceType]] to |resourceType|. +1. Set |instance|.[[Rep]] to |rep|. +1. Set |instance|.[[Own]] to |own|. +1. Set |instance|.[[LendCount]] to 0. +1. If |own| is **true**, register |instance| in the JS-API's resource `FinalizationRegistry` with held value (|resourceType|, |rep|) and unregister token |instance|. +1. Return |instance|. + +For a resource type `R` whose type variable is one of the component's type exports, let |class| be the resource class associated with that variable: + +- `ToJSValue(rep, own)`: + 1. Return `create a resource instance` given |class|, `R`, |rep| and **true**. +- `ToJSValue(rep, borrow)`: + 1. Let |instance| be `create a resource instance` given |class|, `R`, |rep| and **false**. + 1. Append |instance| to the current borrow scope. + 1. Return |instance|. +- `ToComponentValue(jsValue, own)`: + 1. If |jsValue| does not have a [[ResourceClass]] internal slot, or |jsValue|.[[ResourceClass]] is not |class|, throw a `TypeError`. + 1. If |jsValue|.[[Rep]] is **empty**, or |jsValue|.[[Own]] is **false**, or |jsValue|.[[LendCount]] is not 0, throw a `TypeError`. + 1. Let |rep| be |jsValue|.[[Rep]]. Set |jsValue|.[[Rep]] to **empty** and unregister |jsValue| from the resource `FinalizationRegistry`. + 1. Return |rep|. +- `ToComponentValue(jsValue, borrow)`: + 1. If |jsValue| does not have a [[ResourceClass]] internal slot, or |jsValue|.[[ResourceClass]] is not |class|, throw a `TypeError`. + 1. If |jsValue|.[[Rep]] is **empty**, throw a `TypeError`. + 1. Increment |jsValue|.[[LendCount]] and append |jsValue| to the current lender list. + 1. Return |jsValue|.[[Rep]]. + +A fresh instance is created for every lift, so two `borrow`s of the same resource are two JS objects that do not compare equal. + +The *current borrow scope* and *current lender list* are per-call spec state: +- `read the function import` establishes a borrow scope for a component-to-JS call. Once the JS call completes, every instance in the scope has its [[Rep]] set to **empty**, so JS holding on to a `borrow` past the call gets a `TypeError` on next use. +- `invoke a component function` establishes a lender list for a JS-to-component call. Once the component call completes, every instance in the list has its [[LendCount]] decremented. + +#### Dropping guest resources + +To `drop a resource instance` given |instance|: +1. If |instance| does not have a [[ResourceClass]] internal slot, throw a `TypeError`. +1. If |instance|.[[Rep]] is **empty** or |instance|.[[Own]] is **false**, return **undefined**. +1. If |instance|.[[LendCount]] is not 0, throw a `TypeError`. +1. Let |rep| be |instance|.[[Rep]]. Set |instance|.[[Rep]] to **empty** and unregister |instance| from the resource `FinalizationRegistry`. +1. Perform ? `drop a host owned resource` given |instance|.[[ResourceType]] and |rep|. +1. Return **undefined**. + +Dropping is idempotent, and dropping a `borrow` instance does nothing because there is nothing to give back. The [[LendCount]] check makes disposing an instance that is currently lent to a component a `TypeError` rather than a trap. + +`create a resource instance` adds `own` instances to a resource `FinalizationRegistry`. When the value is finalized, the host performs `drop a host owned resource` with the held (resource type, rep) pair. + +### Fusing component value conversions + +TODO. + +## Instantiation + +To `instantiate a component` given |component| and a list of component definitions |imports|: +1. Perform ? `create resource class shells` given |component|. +1. Instantiate |component| with |imports|. + 1. If instantiation traps, throw a `WebAssembly.RuntimeError`. +1. Let |instance| be the resulting component instance. +1. Perform `link resource classes` given |instance|. +1. Let |exportsObject| be ? `create the exports object` given |instance|. +1. Return a new `ComponentInstance` whose [[ComponentInstance]] is |instance| and whose [[Exports]] is |exportsObject|. + +To `instantiate a component from an imports object` given |component| and |importsObject|: +1. Let |imports| be ? `read the imports` given |component| and |importsObject|. +1. Return ? `instantiate a component` given |component| and |imports|. + +### Read the imports object + +The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the core JS-API's algorithm of the same name. The resolved JS values are then handed to the per-sort algorithms (`read the function import`, `read the type import` and friends) to produce the component definitions used during instantiation. + +While walking, the algorithm recognizes the pattern of a resource type import accompanied by `[constructor]`, `[static]`, and `[method]` function imports tied to it. A resource type import is read first and looks for a constructor (see [resource types](#resource-types)). The tagged function imports then read from the constructor and its prototype directly. This allows the common case of importing a class to be satisfied by just passing the constructor. + +Passing an exported component definition to a component import via the JS-API/ESM-integration is treated as if the import was a JS value. There is no "direct linking" that bypasses going through JS semantics. This is different from core wasm, where wasm exported functions are linked directly when imported and have stricter type checks. This is intentional to ensure that implementing an ES module using a component doesn't subtly change the behavior because it starts directly linking to components. Component definitions can still be directly linked within a top-level invocation of `instantiate a component`. + +Every name looked up on a JS object is `JSName`(|decl|) (see "Names"). + +To `read the imports` given |component| and |importsObject|: +1. If |component| has no imports: + 1. Return an empty list. +1. Return ? `read a scope of imports` given |component|.Imports and |importsObject|. + +To `read a scope of imports` given a list of declarations |declarations| and |object|: +1. If `Type`(|object|) is not Object: + 1. Throw a `TypeError`. +1. Let |resourceTypes| be a new empty map keyed by resource type declaration, holding host resource types. +1. Let |definitions| be a new empty list. + +1. For each |decl| of |declarations|, in declaration order: + 1. Let |name| be `JSName`(|decl|). + 1. If |decl|.Sort is **func** and |decl|.Name is tagged `[constructor]`, `[method].` or `[static].`: + 1. Let R be the resource type declared by the type declaration named by the tag's `` label. + 1. Assert: |resourceTypes|[R] exists. (Validation requires that declaration to precede this one in the same scope) + 1. Let |constructorFunction| be |resourceTypes|[R].[[ImportValue]]. + 1. If the tag is `[constructor]`: + 1. Let |importValue| be |constructorFunction|. + 1. Else if the tag is `[static]`: + 1. Let |importValue| be ? `GetV`(|constructorFunction|, |name|). + 1. Else: + 1. Let |prototype| be ? `GetV`(|constructorFunction|, "prototype"). + 1. If `Type`(|prototype|) is not Object, throw a `TypeError`. + 1. Let |importValue| be ? `GetV`(|prototype|, |name|). + 1. Else: + 1. Let |importValue| be ? `GetV`(|object|, |name|). + + 1. Let |resolved| be ? `read an import` given |decl|, |importValue| and |resourceTypes|. + 1. If |decl|.Sort is **type**: + 1. Set |resourceTypes|[|decl|.ResourceType] to |resolved|, and append |resolved|.[[ComponentResourceType]] to |definitions|. + 1. Else, append |resolved| to |definitions|. +1. Return |definitions|. + +A type import resolves to a [host resource type](#host-resource-types-and-values), which is a JS-API record wrapping the component resource type. The component only ever gets the resource type, but the record is kept around for the rest of the scope's function imports and for the exports object. + +To `read an import` given |decl|, |importValue| and |resourceTypes|: +1. Match |decl|.Sort: + 1. **core module**: return ? `read the core module import` given |decl|.ModuleType and |importValue|. + 1. **func**: return ? `read the function import` given |decl|.FuncType, |importValue|, |decl|.Name's tag and |resourceTypes|. + 1. **type**: return ? `read the type import` given |decl|.TypeBound and |importValue|. + 1. **value**: return ? `read the value import` given |decl|.ValType and |importValue|. + 1. **instance**: return ? `read the instance import` given |decl|.InstanceType and |importValue|. + 1. **component**: return ? `read the component import` given |decl|.ComponentType and |importValue|. + +To `read the core module import` given |coreModuleType| and |importValue|: +1. If |importValue| does not have a [[Module]] internal slot: + 1. Throw a `TypeError`. +1. If the type of |importValue|.[[Module]] is not a subtype of |coreModuleType|: + 1. Throw a `WebAssembly.LinkError`. +1. Return |importValue|.[[Module]]. + +To `read the component import` given |componentType| and |importValue|: +1. If |importValue| does not have a [[Component]] internal slot: + 1. Throw a `TypeError`. +1. If the type of |importValue|.[[Component]] is not a subtype of |componentType|: + 1. Throw a `WebAssembly.LinkError`. +1. Return |importValue|.[[Component]]. + +To `read the instance import` given |instanceType| and |importValue|: +1. Let |definitions| be ? `read a scope of imports` given |instanceType|.Exports and |importValue|. +1. Return a component instance whose exports are |definitions|. + +To `read the function import` given |componentFuncType|, |importValue|, |importNameTag| and |resourceTypes|: +1. If |importValue| is not callable: + 1. Throw a `TypeError`. +1. Let |paramTypes| be |componentFuncType|.Params and |resultType| be |componentFuncType|.Result. +1. Let |callKind|, |receiverRule| and |paramOffset| be determined by |importNameTag|: + 1. `[constructor]`: `Construct`, no receiver, offset 0. + 1. `[method].`: `Call`, receiver is component argument 0 (the `borrow` self), offset 1. + 1. `[static].`: `Call`, receiver is |resourceTypes|[R].[[ImportValue]], offset 0. + 1. otherwise: `Call`, receiver is **undefined**, offset 0. +1. If |resultType| is `result`: + 1. Let |okType| be T, |errorType| be E, and |throwing| be **true**. +1. Else: + 1. Let |okType| be |resultType| and |throwing| be **false**. +1. Return a component host function of type |componentFuncType| whose body, given component arguments « |v_0|, ..., |v_{n-1}| », performs: + 1. Let |borrowScope| be a new empty List, and set the current borrow scope to |borrowScope|, saving the previous one. However this body completes, set the [[Rep]] of every instance in |borrowScope| to **empty** and restore the previous borrow scope before returning. + 1. If |receiverRule| is "component argument 0": + 1. Let |thisArg| be `ToJSValue`(|v_0|, |paramTypes|[0]). + 1. Else: + 1. Let |thisArg| be the receiver named by |receiverRule|. + 1. Let |args| be a new empty List. + 1. For each i in [|paramOffset|, n): + 1. Append `ToJSValue`(|v_i|, |paramTypes|[i]) to |args|. + 1. If |callKind| is `Construct`, let |completion| be `Construct`(|callable|, |args|); else let |completion| be `Call`(|callable|, |thisArg|, |args|). + 1. If |completion| is an abrupt completion: + 1. If |throwing| is **false**, trap. + 1. Let |errorValue| be `ToComponentValue`(|completion|.[[Value]], |errorType|). If that throws, trap. + 1. Return `result.error(|errorValue|)`. + 1. Let |componentResult| be `ToComponentValue`(|completion|.[[Value]], |okType|). If that throws, trap. + 1. If |throwing| is **true**, return `result.ok(|componentResult|)`; else return |componentResult|. + +The borrow scope covers the whole body, so a `borrow` of a component-defined resource is usable for the duration of the call, including from a callback the JS function passes back into the component, and is a `TypeError` to use afterwards. + +To `read the value import` given |componentValType| and |importValue|: +1. Return `ToComponentValue`(|importValue|, |componentValType|). If that throws, propagate the exception. + +### Create the exports object + +The `create the exports object` algorithm walks the component's exports and builds a fresh JS object whose properties are the exports. + +Component-defined resource types become [resource classes](#guest-resource-classes) named `JSName`(|export|), and tagged function exports are mapped onto them just as in `read the imports`: +- `[constructor]`: the function becomes `R`'s constructor behaviour. By strong-uniqueness there can only be one. +- `[method].`: the function becomes a method named `JSName`(|export|) on `R.prototype`. +- `[static].`: the function becomes a static method named `JSName`(|export|) on `R`. + +All other exported components definitions are given JS definitions named `JSName`(|export|) on the exports object. + +To `create the exports object` given a |componentInstance|: +1. Let |exportsObject| be `OrdinaryObjectCreate`(**null**). +1. For each |export| of |componentInstance|.|component|.Exports, in declaration order: + 1. If |export|.Name is tagged `[constructor]`, `[method].` or `[static].`, it is consumed by `link resource classes` for R; continue. + 1. Let |key| be `JSName`(|export|). + 1. Match |export|.Sort: + 1. **core module**: + 1. Let |value| be a new `Module` whose [[Module]] is |export|.Module. + 1. **type**: + 1. If |export|.Type is not a resource type: + 1. Throw `TypeError`. + 1. Let |variable| be the abstract type |export| designates. + 1. If |variable| is one of the component's type imports: + 1. Throw a `TypeError`. + 1. Else: + 1. Let |value| be the resource class associated with |variable|. + 1. **func**: + 1. Let |value| be `create a JS function for a component function` given |export|.Func, |key| and no tag. + 1. **value**: + 1. Let |value| be `ToJSValue`(|export|.Value, |export|.Type). + 1. **instance**: + 1. Let |value| be ? `create the exports object` given the exported instance. + 1. **component**: + 1. Let |value| be a new `Component` whose [[Component]] is |export|.Component. + 1. Perform `CreateDataPropertyOrThrow`(|exportsObject|, |key|, |value|). +1. Return |exportsObject|. + +To `create a JS function for a component function` given |componentFunc|, |name| and |exportNameTag|: +1. Let |paramOffset| be 1 if |exportNameTag| is `[method].`, else 0. +1. Let |okType| be |componentFunc|.Result's `result` payload type if it is a `result`, else |componentFunc|.Result. +1. Return a built-in function object with name |name| and length |componentFunc|.Params.length - |paramOffset|, whose behaviour, given a **this** value |thisValue| and JS arguments |args|, performs: + 1. Let |componentResult| be ? `invoke a component function` given |componentFunc|, |exportNameTag|, |thisValue| and |args|. + 1. Return `ToJSValue`(|componentResult|, |okType|). + +A `[method]` export takes its **this** value as the component function's first parameter, which validation guarantees is the `borrow` self, mirroring how `read the function import` maps component argument 0 onto a JS receiver. A `[static]` export ignores its **this** value. + +To `invoke a component function` given |componentFunc|, |exportNameTag|, |thisValue| and a List of JS values |args|: +1. Let |paramTypes| be |componentFunc|.Params and |resultType| be |componentFunc|.Result. +1. Let |paramOffset| be 1 if |exportNameTag| is `[method].`, else 0. +1. If |resultType| is `result`: + 1. Let |okType| be T, |errorType| be E, and |throwing| be **true**. +1. Else: + 1. Let |okType| be |resultType| and |throwing| be **false**. +1. Let |lenders| be a new empty List, and set the current lender list to |lenders|, saving the previous one. However this algorithm completes, decrement the [[LendCount]] of every instance in |lenders| and restore the previous lender list before returning. +1. Let |values| be a new empty List. +1. If |paramOffset| is 1, append ? `ToComponentValue`(|thisValue|, |paramTypes|[0]) to |values|. +1. If the number of |args| is less than |paramTypes|.length - |paramOffset|, throw a `TypeError`. +1. For each i in [0, |paramTypes|.length - |paramOffset|): + 1. Append ? `ToComponentValue`(|args|[i], |paramTypes|[i + |paramOffset|]) to |values|. +1. Arguments beyond that are ignored. +1. Let |componentResult| be the result of invoking |componentFunc| with |values|. + 1. If the call traps, throw a `WebAssembly.RuntimeError`. +1. If |throwing| is **true**: + 1. If |componentResult| is `result.error(|e|)`: + 1. Throw `create a component error` for |e| and |errorType|. + 1. Set |componentResult| to the `result.ok` payload. +1. Return |componentResult|. + +The lender list covers the whole call, so JS cannot dispose a resource instance it lent to a component while the component still holds the `borrow`, even if the component calls back out to JS to try. + +To `create a component error` for component value |e| and component type |errorType|: +1. Let |payload| be `ToJSValue`(|e|, |errorType|). +1. Return a new `ComponentError` whose `payload` is |payload| and an implementation defined `message`. + +## WebAssembly ESM-Integration + +[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary to decide whether the bytes decode as a module or a component, so a component can be loaded anywhere a module can be today. + +Each component import becomes a JS import for the module loader. Its [module specifier](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ModuleSpecifier) is the import's [`external-id`](Explainer.md#import-and-export-definitions) attribute if it has one, and its `externname` otherwise. A specifier is resolved (not looked up on an object) so it is not converted to a JS name. + +Which binding of the resolved module the component gets depends on what the import's type is: + +| Import type | JS equivalent | Value | +|---|---|---| +| bare function, value | `import v from "spec"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | +| instance | `import { a, b } from "spec"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per export of the instance type, named `JSName` of that export | +| core module, component | `import source M from "spec"` | the module source, as a `Module` or `Component` | + +Each resolved value is handed to [`read an import`](#read-the-imports-object) and the resulting definitions are passed to [`instantiate a component`](#instantiation). + +A component's exports become the bindings of its module namespace object. There is one binding per `JSName`(|export|), holding what [`create the exports object`](#create-the-exports-object) puts under that name, and no `default` binding. + +Reading the imports snapshots the resolved values, and so components cannot participate in cycles. This matches how core modules work today with ESM-integration. + +TODO: figure out TLA and async start functions. + +## Open questions + +1. How to dynamically pass a union value? Static selection works. +1. How to import an overloaded function? +1. How to support class inheritance and casting? Can a component defined resource sub-class an imported resource type? +1. How to support reference equality? `ToJSValue` creates a fresh resource instance per lift, so two `borrow`s of one component-defined resource are two JS objects that do not compare equal. Reps are opaque and reusable after a drop, so an identity map would need careful invalidation. +1. How to import/export properties with getters/setters? +1. What happens if a component traps? Do we have lockdown semantics of some sort? +1. There is no `any` in the component model, so a component's only way to hold an opaque JS value is a resource type import with no brand check hook. Should we define builtin resource types for JS primitive types? +1. A `start` function can pass a resource value to a JS function import and then trap. Disposing the resource value would run a destructor in an uninstantiated component. diff --git a/design/mvp/Web.md b/design/mvp/Web.md deleted file mode 100644 index ac734271..00000000 --- a/design/mvp/Web.md +++ /dev/null @@ -1,655 +0,0 @@ -# Web API for Components - -This explainer describes how WebAssembly Components (hereafter 'components') can be used in a web engine. It could also be used in non-web engines (such as Node) that support the subset of WebIDL used in this document. - -This spec would be layered on a future component embedder interface (similar to how the JS-API is layered on the core spec embedder interface). - -**This is a draft and is not complete. Major details are unresolved, and there are bugs. See "Open Questions" at the end for a sampling of them.** - -## Goals - -1. Components can import and use most web and JS API's -2. Components can export an API useable by JS -3. Components interact with the web platform in similar ways to JS: - a. Components can feature test whether API's are present - b. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill - c. Components are tolerant of web API evolution - d. Components misuse of a web API's result in failure at that call-site, not link time errors -4. Components have improved performance when calling web API's compared to today - -## Non-goals - -1. Components importing every kind of web API -1. Components exporting any kind of JS API - -## Design - -To meet our goals, we need to define interactions (also known as 'bindings') between components, web API's, and JS. - -The scripting interface for web API's is handled (almost but not entirely) by WebIDL, so bindings for web API's effectively means bindings for WebIDL. WebIDL already has a "JavaScript Bindings" section which defines how JS interacts with WebIDL. There are no other bindings yet supported by WebIDL. - -There are roughly three paths forward here: - -1. Define bindings between components and JS - components transitively have access to web API's through the pre-existing JS-WebIDL bindings. -2. (1) and also define bindings between components and WebIDL - components get a separate direct path to web API's. -3. Define bindings between components and WebIDL - components transitively have access to JS through the pre-existing JS-WebIDL bindings. - -There are pros/cons to each. Let's go through them. - -### A. Define only bindings between Components and JS - -This is the smallest step from where we are today. A component's imports and exports are described in terms of JS values, and the web platform is reached the same way JS reaches it. - -Goals #1, #2 and #3 mostly fall out for free. Web API's are already exposed to JS, so importing one is just importing the JS function that reflects it, and exporting to JS is given by the binding. Feature testing, polyfilling and API evolution are all properties the WebIDL-JS binding already supports, so they keep working without us specifying anything new. - -The problem is goal #4. Once a call into a web API is defined as a call through JS, JS semantics are observable at every step. Lookups on the global object and on prototypes can be intercepted, argument coercion can run user code through `valueOf`, `toString` and iterators, and the callee may be a Proxy. An engine can speculate and fast-path the common case, but it cannot skip those steps in general. TODO(elaborate). - -JS (specifically ECMA-262) also is missing many concepts that components require. Components have resources, streams, sized integers and guaranteed-valid unicode strings. WebIDL has interface types, `ReadableStream`, sized integer types and `USVString`. JS just has objects and doubles. Going through JS means lowering all of those concepts down to their JS representations so that the JS-WebIDL bindings can immediately raise them back up. Both conversions still have to be specified, and information can be lost in the middle. - -### B. Define bindings between Components and JS and also Components and WebIDL - -This is a superset of option A, so it inherits the pros/cons of that. In addition, we add a parallel binding between components and WebIDL to get goal #4 as well. Components that only need to talk to JS use the JS binding, and components that use web API's use the WebIDL binding. - -The cost is that we write and maintain two bindings, and they have to harmonize. - -### C. Define only bindings between Components and WebIDL - -JS already has well-defined bindings to WebIDL. If we define bindings from components to WebIDL, we get direct and efficient access to web API's (goal #4) and transitively get access to JS (goals #1 and #2). - -Like #1 we only have one specification to draft and maintain. - -The open question is goal #3. We need to decide how feature testing, polyfills and API evolution work in the direct WebIDL binding. This is new conceptual ground that needs careful design. - -### Conclusion - -We should take option C. Option A has too many cons, while option B is twice the work to implement and maintain. Option C has the potential to get us everything we want at the smallest conceptual burden. - -## Walkthrough - -Let's walk through how this all works in practice. After this will be an in-depth explainer of the exact proposed rules. - -### A greeter - -Start with a component that imports nothing: - -```wit -package example:greeter; - -world greeter { - export greet: func(name: string) -> string; -} -``` - -Exports are converted to canonical WebIDL which is then exposed to JS through the existing WebIDL-to-JS machinery. A component `string` is a sequence of unicode scalar values, which is exactly what WebIDL calls a `USVString`, so this component is described as: - -```webidl -namespace { - USVString greet(USVString name); -}; -``` - -What JS gets is an ordinary object with an ordinary method on it: - -```js -const { instance } = await WebAssembly.instantiate(bytes); -instance.exports.greet("world"); // "hello, world" -``` - -The JS caller interacts with greet like any normal WebIDL operation. For example, `greet(42)` converts the number to a string and passes `"42"`, and `greet()` throws a `TypeError` for the missing argument. - -### A logger - -Now a component that imports: - -```wit -package example:logger; - -world logger { - import log: func(message: string); - export run: func(); -} -``` - -The obvious thing to pass is `console.log`: - -```js -const { instance } = await WebAssembly.instantiate(bytes, { - log: console.log, -}); -``` - -`console.log` is a web API, so the engine already knows its [WebIDL signature](https://console.spec.whatwg.org/#console-namespace): -``` -undefined log(any... data); -``` - -it takes any number of arguments of any type. The component's `message` is a string, and a string is one of the things it can take, so there is nothing to convert and nothing to check. - -#### Polyfilling it - -Now suppose `console.log` isn't available, or we want to capture the output. Pass a plain JS function instead: - -```js -const lines = []; -const log = (message) => { lines.push(message); }; -const { instance } = await WebAssembly.instantiate(bytes, { - log, -}); -``` - -A plain JS function has no WebIDL signature, so we treat it as one that takes anything and returns anything, and convert the component's values to JS values on the way in. - -Since both work, the choice can be made in JS before the component is instantiated: - -```js -const log = globalThis.console?.log ?? myPolyfill; -``` - -### Searching the DOM - -Now let's import a resource type and a more complex API. - -```wit -package example:search; - -interface dom { - resource element { - query-selector: func(selectors: string) -> option; - get-attribute: func(name: string) -> option; - scroll-into-view: func(align-to-top: bool); - } -} - -world search { - import dom; - export find: func(root: borrow, selectors: string) -> option; -} -``` - -To satisfy all of that, you can just import `Element` itself: - -```js -const { instance } = await WebAssembly.instantiate(bytes, { element: Element }); - -instance.exports.find(document.body, "h1"); // "page-title" or null -``` - -One import value covers the resource and all three of its methods. `Element` names the interface, and the methods are found on `Element.prototype`, which is where JS finds them too. Component names are kebab-case and JS names are camelCase, so `query-selector` is matched with `querySelector`. - -Binding the resource to `Element` also influences how the component's own exports look. Its `find` takes an element, so what JS sees is: - -```webidl -namespace { - USVString? find(Element root, USVString selectors); -}; -``` - -JS must pass a real element or else it gets a `TypeError`. - -#### When the API evolves - -`scroll-into-view` is interesting here, because `scrollIntoView` has evolved over time. It used to take a single boolean, but now it takes either a boolean or an options dictionary. The component above was written against the old version and still asks for a `bool`. - -This is okay. When an argument is allowed to be one of several types, we try the component's value against each of them and use the first one that fits, and a boolean still fits. - -Mismatched argument counts get a similar treatment. Extra arguments are dropped, and arguments the component doesn't pass behave as if a JS caller had left them out. - -Arguments that don't actually match do fail, but they fail at the call rather than at load. If a component asks for `scroll-into-view: func(align-to-top: string)`, a string is neither a boolean nor an options dictionary, so that call traps. Instantiation still succeeds, `find` still works, and a component that never calls `scroll-into-view` never traps. - -## The WebAssembly Namespace - -Extend the imperative WebAssembly JS-API interfaces to also allow validation/compilation/instantiation of components in addition to modules. - -```webidl -interface Component { - constructor([AllowResizable] AllowSharedBufferSource bytes); -} - -interface ComponentInstance { - constructor(Component component, object args); -} - -typedef (Component or Module) InstantiateSource; - -[Exposed=*] -namespace WebAssembly { - // Same as before, but now will detect if the bytes are a component or module and dispatch differently. - boolean validate([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); - Promise compile([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); - Promise instantiate( - [AllowResizable] AllowSharedBufferSource bytes, optional object importObject, optional WebAssemblyCompileOptions options = {}); - - // Now takes an InstantiateSource instead of just a Module. - Promise instantiate( - InstantiateSource moduleObject, optional object importObject); -} -``` - -## WebAssembly ESM-Integration - -TODO. - -## Names - -Component names are [`label`s](Explainer.md#import-and-export-names) and must be transformed when looking up what JS/Web interface they refer to. - -TODO: Define `pascal case`(|name|) -TODO: Define `camel case`(|name|) - -## Types and values - -Components and WebIDL maintain separate type systems, so any value crossing the boundary needs a defined translation in both directions. - -This section specifies that translation as four [abstract operations](https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations): - 1. CanonicalWebIDLType - pick the WebIDL type that best represents a given component value type - 2. ToCanonicalWebIDLValue - infallibly convert from a component value to a canonical WebIDL value - 3. FromCanonicalWebIDLValue - infallibly convert from a canonical WebIDL value to a component value - 4. CoerceWebIDLValue - convert from one WebIDL type to another - -### Resource types - -A component resource type in the web embedding is a [WebIDL object type](https://webidl.spec.whatwg.org/#dfn-object-type). Resource defined in a component are given a WebIDL interface that represents them as WebIDL object types. - -When a component imports a resource type, if a WebIDL [interface object](https://webidl.spec.whatwg.org/#dfn-interface-object) is given then the type of the interface it represents is used. Otherwise the generic `object` type is used instead. - -The interface object is what identifies the interface, not its constructor. Most interfaces on the platform are not constructible, since `new Element()` throws and `Element` has no `constructor` operation at all, but `Element` is still the value a JS author reaches for to name the type, and it is still the object carrying the prototype that `[method]` imports are resolved from. Keying on constructibility instead would make nearly every DOM interface unimportable. - -### CanonicalWebIDLType - -`CanonicalWebIDLType(componentValType)` computes the canonical WebIDL type used to represent a component value type. Specialized component types are handled directly rather than being despecialized first, since many have natural WebIDL counterparts. - -| Component type | Canonical WebIDL type | -|---|---| -| `bool` | `boolean` | -| `s8` / `u8` | `byte` / `octet` | -| `s16` / `u16` | `short` / `unsigned short` | -| `s32` / `u32` | `long` / `unsigned long` | -| `s64` / `u64` | `long long` / `unsigned long long` | -| `f32` / `f64` | `unrestricted float` / `unrestricted double` | -| `char` | `USVString` (length 1, asserted at conversion time) | -| `string` | `USVString` | -| `list` | `sequence` | -| `list` (fixed-length) | `sequence` (length-N invariant) | -| `record { f: T, ... }` | an anonymous `dictionary` type with required member `camel case(f)` of `CanonicalWebIDLType(T)` per field | -| `tuple` | `sequence` | -| `flags "L"+` | an anonymous `dictionary` type with optional `boolean` member `camel case(L)` per label, default `false` | -| `enum "L"+` | an anonymous `enumeration` with the same label set | -| `option` where T is not option<_> | `CanonicalWebIDLType(T)?` | -| `option` where T is option<_> | fallthrough to generic variant case below | -| `result` | fallthrough to generic variant case below. This is also special cased elsewhere when used as return value of a function. | -| `variant (case "L" T?)+` | an anonymous `dictionary { KindEnum kind; (union of case payload types)? value; }` and an anonymous `enumeration KindEnum` with the same label set | -| `own` / `borrow` | the WebIDL type chosen for `R` by `read the component type import` | -| `future` | `Promise` | -| `stream` | `ReadableStream`? | -| `error-context` | TODO | - -Notes: - - `f32`/`f64` map to `unrestricted float`/`unrestricted double` rather than the restricted forms because the component model permits NaN values, while restricted WebIDL float types forbid NaN and infinity. - - For `stream`, the element type `T` is not encoded into the WebIDL type; element-level conversion occurs at read time. - - `record` fields and `flags` labels are dictionary members, so they are `camel case`d. `enum` and `variant` case labels are enumeration values, which JS sees as strings, so they are used verbatim. See "Names". Two fields or labels of the same type that `camel case` to the same string are a link-time error, as elsewhere. - -### ToCanonicalWebIDLValue - -`ToCanonicalWebIDLValue(componentValue)` converts a component value to the canonical WebIDL value of type `CanonicalWebIDLType(componentValType)`. This algorithm is infallible. - -Dispatch on the component value type: -- `bool` → IDL `boolean` -- Integer types → IDL number of the matching IDL integer type -- `f32` / `f64` → IDL `unrestricted float` / `unrestricted double` -- `char` → `USVString` of length 1 from the Unicode scalar value -- `string` → `USVString` -- `list` → `sequence` with each element recursively converted by `ToCanonicalWebIDLValue` -- `record { f: T, ... }` → a `dictionary` value with each field recursively converted -- `tuple` → a `sequence` value with each field recursively converted -- `flags` → a `dictionary` value with each set label `true`, each unset label `false` -- `enum` → the `enumeration` value matching the label -- `option` where T is not option<_> → `null` for `none`; else `ToCanonicalWebIDLValue` the inner value. -- `variant` → `{ kind: label, value: ToCanonicalWebIDLValue(payload) }` (omit `value` for cases which don't have a payload) -- `own` / `borrow` → the host interface object wrapping the handle; `own` resources use a `FinalizationRegistry` to invoke the destructor; `borrow` wrappers are invalidated after the call returns -- `future` → an IDL `Promise` wrapping the future (TODO) -- `stream` → an IDL `ReadableStream` wrapping the stream (TODO) -- `error-context` → TODO - -### FromCanonicalWebIDLValue - -`FromCanonicalWebIDLValue(webIDLValue, targetComponentType)` converts a canonical WebIDL value back to a component value of `targetComponentType`. The algorithm is driven by `targetComponentType` and assumes `webIDLValue` is of type `CanonicalWebIDLType(targetComponentType)`. This algorithm is infallible. - -Each case is the inverse of the corresponding `ToCanonicalWebIDLValue` rule above. - -### CoerceWebIDLValue - -`CoerceWebIDLValue(fromWebIDLValue, toWebIDLType)` coerces a WebIDL value to a different WebIDL type. This algorithm is defined entirely over IDL values without invoking JavaScript semantics. It may throw `TypeError` (or `RangeError` under `[EnforceRange]`). - -Coercions are restricted to within the same [WebIDL overload type class](https://webidl.spec.whatwg.org/#idl-overloading) — numeric types coerce only to other numeric types, string types only to other string types, and so on. This gives the following invariant: if `CoerceWebIDLValue(v, t1)` and `CoerceWebIDLValue(v, t2)` both succeed, then `t1` and `t2` fall in the same overload type class and therefore are not distinguishable. Coercing a value will not change which overload should be selected. This is in contrast to JS, which performs two-step overload selection first comparing the JS value kind to find a candidate and then performing more permissive coercions to try and call the candidate. - -`fromWebIDLValue` may itself be `undefined` — e.g. a missing WebIDL operation argument with no declared default (see "create a component function for WebIDL operation" below). Each dispatch case below calls out its `undefined`-source behavior where it differs from throwing; where a rule mirrors a well-known ECMAScript abstract operation's behavior on `undefined` (`ToBoolean`, `ToNumber`, `ToString`), that's a description of the resulting value, not an invocation — the algorithm still never runs JavaScript semantics. - -Dispatch on `toWebIDLType`: - -- **`any`** — return `fromWebIDLValue` unchanged. -- **`undefined`** — accept only `undefined`; else throw `TypeError`. -- **`boolean`** — - - source `boolean`: identity. - - source `undefined`: `false` (matches `ToBoolean(undefined)`). - - other sources: throw `TypeError`. -- **Integer types** (`byte`, `octet`, `short`, `unsigned short`, `long`, `unsigned long`, `long long`, `unsigned long long`) — - - source any integer or float type: apply the IDL integer-conversion rules (modular reduction by default, clamping under `[Clamp]`, range check under `[EnforceRange]`) on the source's mathematical value. - - source `undefined`: treated as `NaN` (matches `ToNumber(undefined)`), then the same integer-conversion rules apply to that `NaN` — so `[EnforceRange]` throws (non-finite), `[Clamp]` clamps to `0`, and the default rule modularly reduces to `0`. - - source `bigint`: range-checked; valid only for `long long` and `unsigned long long`. - - other sources: throw `TypeError`. -- **Float types** (`float`, `unrestricted float`, `double`, `unrestricted double`) — - - source any integer or float type: convert by IEEE-754 round-to-nearest-even; restricted forms (`float`, `double`) throw `TypeError` for `NaN` or `±Infinity`. - - source `undefined`: treated as `NaN` (matches `ToNumber(undefined)`); as above, restricted forms throw and unrestricted forms keep the `NaN`. - - other sources: throw `TypeError`. -- **`bigint`** — - - source `bigint`: identity. - - source integer type: exact conversion. - - source float: the value must be a finite integer; else throw `TypeError`. - - other sources (including `undefined`, matching `BigInt(undefined)` throwing in JS): throw `TypeError`. -- **`DOMString`** — - - source `DOMString`, `USVString`, or `ByteString`: identity (re-typed). - - source `enumeration`: the label string. - - source `undefined`: the literal string `"undefined"` (matches `ToString(undefined)`). - - source `null` under `[LegacyNullToEmptyString]`: the empty string. - - other sources: throw `TypeError`. -- **`USVString`** — - - source `USVString`: identity. - - source `DOMString` or `ByteString`: replace lone surrogates with U+FFFD; reinterpret otherwise. - - source `enumeration`: the label string, then apply surrogate replacement. - - source `undefined`: the literal string `"undefined"` (already valid USV; no replacement needed). - - other sources: throw `TypeError`. -- **`ByteString`** — - - source `ByteString`: identity. - - source `DOMString` or `USVString`: each code unit must be `≤ U+00FF`; else throw `TypeError`. - - source `enumeration`: the label string, then check the range. - - source `undefined`: the literal string `"undefined"` (already valid ByteString). - - other sources: throw `TypeError`. -- **`object`** — accept any non-primitive IDL value (interface, dictionary, sequence, record, callback, Promise); else throw `TypeError` (including for `undefined`). -- **`symbol`** — accept only `symbol`; else throw `TypeError`. -- **Interface `I`** — accept iff the source is an interface value whose type is `I` or a derived interface of `I`; else throw `TypeError`. -- **Callback function** — accept iff the source is a callback; else throw `TypeError`. -- **`dictionary D`** — accept iff the source is a dictionary value (or a record whose entry set covers all required members of `D`). For each declared member `m: T` of `D`: retrieve `m` from the source and recurse with `CoerceWebIDLValue(srcM, T)`. Missing required member: throw `TypeError`. Extra members in the source are ignored. TODO: per real WebIDL, an `undefined` source should build an all-defaults dictionary instead of throwing, once dictionary coercion itself is specified in more detail. -- **Enumeration `E`** — accept iff the source is a string value (any string type, or another enumeration whose label is in `E`'s label set); else throw `TypeError` (an `undefined` source is therefore rejected unless a label is literally `"undefined"`). -- **`sequence`** — accept iff the source is a sequence (or frozen/observable array). Convert each element via `CoerceWebIDLValue(elem, T)`. -- **`record`** — accept iff the source is a `record. Convert each key via `CoerceWebIDLValue(k, K) and value via `CoerceWebIDLValue(v, V)`. -- **`T?` (nullable)** — if the source is `null` or `undefined`, return `null`; else `CoerceWebIDLValue(source, T)`. (A deliberate simplification: an `undefined` source could instead recurse into `T`'s own `undefined`-handling, but a missing nullable-typed value is simpler to just treat as `null` outright.) -- **Union types** — try each member type in declaration order; return the result of the first `CoerceWebIDLValue` call that does not throw. If all throw, throw `TypeError`. (An `undefined` source therefore succeeds against whichever member type accepts it, e.g. the first numeric or string member in declaration order.) -- **Buffer source types** — identity if the source is the same buffer-source kind; else throw `TypeError`. `[AllowShared]` and `[AllowResizable]` gate acceptance. -- **`FrozenArray`** / **`ObservableArray`** — as `sequence`, but produce a frozen or observable array. -- **`Promise`** — TODO. -- **`ReadableStream`** — TODO. - -Notes: -- `[Clamp]` and `[EnforceRange]` are properties of the target parameter or member site. They parameterize the integer-conversion rules above. -- This algorithm does not invoke any JavaScript abstract operation. All source values are fully-typed IDL values (including `undefined`, which is itself a valid IDL value, not a JS one). - -## Validation/Compilation - -Validation and compilation of components just defer to the underlying component embedding interface. This explainer adds nothing to it. - -## Instantiation - -Instantiating a component is a two step process: -1. `Read the imports object` to translate from web/js values to component values -1. `Create the exports object` to translate from component values to web/js values - -This is the core of the web embedding and where most of the logic lives. - -### Read the imports object - -The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the Core JS-API's algorithm of the same name. The resolved JS values are then handed to the per-kind algorithms (`read the component function import`, `read the component type import`, `read the component value import`) to produce the component definitions used during instantiation. - -While walking, the algorithm recognizes the common pattern of a resource type import accompanied by `[constructor]`, `[static]`, and `[method]` function imports tied to it. A resource type import is read first and should be given a WebIDL interface object (see "Resource Types"). The tagged function imports then read from that interface object and its prototype directly. This allows the common case of importing an interface to be satisfied by just passing the interface object. - -Every name looked up on a JS object is `camel case`d first (see "Names"). - -To `read the imports` given |component| and |importsObject|: -1. If |component| has no imports: - 1. Return an empty list. -1. If `Type`(|importsObject|) is not Object: - 1. Throw a `TypeError`. -1. If two names within any of the following groups `camel case` to the same string, throw a `TypeError`: - 1. The names of the imports that are resolved on |importsObject| (that is, every import except a `[constructor]`, `[method]` or `[static]` function import whose resource type is itself imported). - 1. For each resource type import R, the `[method]` names tied to R. - 1. For each resource type import R, the `[static]` names tied to R. -1. Let |resourceInterfaceObjects| be a new empty map keyed by resource type. -1. Let |imports| be a new empty list. - -1. For each |import| of |component|.Imports, in declaration order: - 1. If |import| is a type import: - 1. TODO: handle non-resource type imports. - 1. Let |importValue| be ? `GetV`(|importsObject|, `camel case`(|import|.Name)). - 1. Set |resourceInterfaceObjects|[|import|.ResourceType] to |importValue|. - 1. Else if |import| is a function import: - 1. If |import| is tagged `[constructor]`: - 1. Let R be the resource whose `own` type is the function's return type. - 1. Else if |import| is tagged `[method]`: - 1. Let R be the resource whose `borrow` type is the function's first parameter (the `self` position). - 1. Else if |import| is tagged `[static]`: - 1. Let R be the resource named in the `[static].` tag. - 1. Else: - 1. Let R be undefined. - - 1. If R is defined and |resourceInterfaceObjects|[R] exists: - 1. Let |interfaceObject| be |resourceInterfaceObjects|[R]. - 1. If tagged `[constructor]`: - 1. Let |importValue| be |interfaceObject|. - 1. Else if tagged `[static]`: - 1. Let |importValue| be ? `GetV`(|interfaceObject|, `camel case`(|import|.StaticName)). - 1. Else if tagged `[method]`: - 1. Let |prototype| be ? `GetV`(|interfaceObject|, "prototype"). - 1. If `Type`(|prototype|) is not Object, throw a `TypeError`. - 1. Let |importValue| be ? `GetV`(|prototype|, `camel case`(|import|.MethodName)). - 1. Else: - 1. Let |importValue| be ? `GetV`(|importsObject|, `camel case`(|import|.Name)). - 1. Else: - 1. Let |importValue| be ? `GetV`(|importsObject|, `camel case`(|import|.Name)). - - 1. Let |resolved| be `read a component import` given |import| and |importValue|. - 1. Append |resolved| to |imports|. -1. Return |imports|. - -To `read a component import` given |import| and |importValue|: -1. Match |import|.Kind: - 1. **Instance**: TODO. - 1. **Function**: return `read the component function import` given |import|.Type and |importValue|. - 1. **Type**: return `read the component type import` given |import|.TypeBound and |importValue|. - 1. **Value**: return `read the component value import` given |import|.Type and |importValue|. - -To `read the component function import` given |componentFuncType| and |importValue|: -1. If |importValue| is not callable: - 1. Throw TypeError. -1. If |importValue| is an exported component function: - 1. Return the wrapped component function. -1. If |importValue| is a WebIDL interface object: - 1. If the interface it represents has a constructor operation: - 1. Let |importValue| be that constructor operation. - 1. Else: - 1. Let |importValue| be an operation that throws a `TypeError` when invoked, matching what calling the interface object does. -1. Else if |importValue| is not a WebIDL operation: - 1. Let |importValue| = `create a WebIDL operation for a JS callable`. -1. Return `create a component function for WebIDL operation` for |importValue| - -To `read the component type import` given |componentTypeBound| and |importValue|: -1. If |componentTypeBound| is not `(sub resource)`: - 1. TODO. -1. If |importValue| is not a WebIDL interface object: - 1. Return WebIDL `object`. -1. Return the interface type that |importValue| represents. - -To `read the component value import` given |componentValType| and |importValue|: -1. Let |canonicalType| be `CanonicalWebIDLType`(|componentValType|). -1. Let |canonicalValue| be the result of converting |importValue| to IDL type |canonicalType| using WebIDL's [convert an ECMAScript value to an IDL value](https://webidl.spec.whatwg.org/#js-type-mapping) algorithm. If that algorithm throws, propagate the exception. -1. Return `FromCanonicalWebIDLValue`(|canonicalValue|, |componentValType|). - -Notes: - - Unlike function imports, value import conversion failures surface at instantiation, not at first use. - -To `create a WebIDL operation for a JS callable` given |callable|: -1. TODO: sketch this out more. -1. Return an operation with a `any (any...)` WebIDL signature that immediately invokes |callable|. - -To `create a component function for WebIDL operation` given |operation| and |componentFuncType|: -1. Let |paramComponentTypes| be |componentFuncType|.Params. -1. Let |returnComponentType| be |componentFuncType|.Return. -1. If |returnComponentType| is `result`: - 1. Let |okComponentType| = T. - 1. Let |errorComponentType| = E. - 1. Let |throwing| = true. -1. Else: - 1. Let |okComponentType| = |returnComponentType|. - 1. Let |throwing| = false. -1. If |operation| is an overload set: - 1. Compute |canonicalParamType_i| = `CanonicalWebIDLType`(|paramComponentTypes|[i]) for each i. - 1. Look for the unique overload whose declared parameter type at the distinguishing argument index has the same WebIDL overload type class as |canonicalParamType_i| at that index, considering only positions present in both. - 1. If an overload was found: - 1. Let |selectedOperation| be that overload. - 1. Else: - 1. let |selectedOperation| be a placeholder that traps when invoked. -1. Else: - 1. Let |selectedOperation| = |operation|. -1. Let |result| = Construct a component host function with type |componentFuncType| whose body, given component args [|v_0|, ..., |v_{N_c - 1}|]: - 1. If |selectedOperation| is the trap placeholder, trap. - 1. Let |declaredParamTypes| = |selectedOperation|.Params - 1. Let |N_o| = |declaredParamTypes|.length. - 1. If |selectedOperation|'s final declared parameter is variadic: - 1. Let |fixedCount| = |N_o| - 1. - 1. Let |variadicElemType| be that parameter's element type/ - 1. Else: - 1. Let |fixedCount| = |N_o|. - 1. Let |variadicElemType| be undefined. - 1. For each i in [0, |fixedCount|): - 1. If i < |N_c|: - 1. Let |args|[i] = `CoerceWebIDLValue`(`ToCanonicalWebIDLValue`(|v_i|), |declaredParamTypes|[i]). If this throws, trap. - 1. Else if the i-th declared parameter has a default value expression (WebIDL's `optional T x = defaultExpr`): - 1. Let |args|[i] be that default value, already of type |declaredParamTypes|[i]. - 1. Else: - 1. Let |args|[i] = `CoerceWebIDLValue`(`undefined`, |declaredParamTypes|[i]). If this throws, trap. - 1. Let |variadicArgs| be a fresh empty IDL sequence with element type |variadicElemType|. - 1. If |variadicElemType| is defined: - 1. For each j in [|fixedCount|, |N_c|): - 1. Append `CoerceWebIDLValue`(`ToCanonicalWebIDLValue`(|v_j|), |variadicElemType|) to |variadicArgs|. If this throws, trap. - 1. Pass |variadicArgs| as the variadic invocation arguments to |selectedOperation|. - 1. Else: - 1. Component args |v_{|fixedCount|}|, ..., |v_{N_c - 1}| are ignored when |N_c| > |fixedCount|. - 1. Invoke |selectedOperation|(|args|). - 1. If the invocation throws |error|: - 1. If the function is marked throwing: - 1. Let |canonicalError| = `CoerceWebIDLValue`(|error|, `CanonicalWebIDLType`(|errorComponentType|)). If this throws, trap. - 1. Return `result.error(`FromCanonicalWebIDLValue`(|canonicalError|, |errorComponentType|))`. - 1. Else: trap. - 1. Else: let |webIDLResult| = the returned WebIDL value. - 1. Let |canonicalReturn| = `CoerceWebIDLValue`(|webIDLResult|, `CanonicalWebIDLType`(|okComponentType|)). If this throws, trap. - 1. Let |componentResult| = `FromCanonicalWebIDLValue`(|canonicalReturn|, |okComponentType|). - 1. If |throwing|: - 1. Return `result.ok(|componentResult|)`. - 1. Else: - 1. Return |componentResult|. -1. Return |result|. - -Notes: -- Construction always succeeds. Type and arity mismatches surface as runtime traps when the function is invoked; not at instantiation time. -- Pre-resolved overload selection runs once at instantiation. The component import has a fixed function type that is used to select the closest overload. -- Param-length mismatches are JS-permissive: a missing arg uses its declared default value if the parameter has one, else falls back to `undefined` (subject to per-param `CoerceWebIDLValue` rules, including its `undefined`-source cases above); extras are dropped. -- Variadic operations are spread one-per-element from the component caller's trailing args. -- TODO: should we special case a list passed as the final argument to a variadic overload? -- TODO: can we get away with only ever having static overload selection? - -### Create the exports object - -The `create the exports object` algorithm analyzes the component's exports, builds a set of WebIDL fragments (interfaces, namespace members, dictionaries, enumerations) describing them, and then defers to WebIDL's existing [JS binding](https://webidl.spec.whatwg.org/#javascript-binding) to materialize JS values for those fragments. The returned object is a fresh JS object whose properties are the materialized exports. - -Tagged function exports are mapped to interface members just as in `read the imports`: -- `[constructor]`: The operation becomes the interface `R`'s constructor. By strong-uniqueness, there can only be one for an interface, and we don't have to worry about overloading a constructor. -- `[method].`: The operation becomes a regular interface member named `camel case`(|name|) on `R`. -- `[static].`: The operation becomes a static interface member named `camel case`(|name|) on `R`. - -Resource types become interfaces named `pascal case`(|name|), and everything else becomes a member named `camel case`(|name|); see "Names". - -To `create the exports object` given a |componentInstance|: -1. Let |fragments| be a new empty set of WebIDL fragments. -1. Let |resourceInterfaces| be a new empty map keyed by component resource type. -1. Let |namespace| be an fresh anonymous WebIDL `namespace` fragment that will host plain function and value exports. Add it to |fragments|. -1. For each |export| of |componentInstance|.|component|.Exports, in declaration order: - 1. Match |export|.Kind: - 1. **Type (resource)**: - 1. If the resource is re-exported from imports: - 1. Let |interface| be the WebIDL interface that was selected for that resource by `read the component type import` at instantiation. - 1. Else (resource defined in the component): - 1. Let |interface| be a fresh WebIDL `interface` fragment named `pascal case`(|export|.Name). - 1. Add a `[LegacyNamespace=|namespace|]` extended attribute to |interface|. - 1. Add |interface| to |fragments|. - 1. If no `[constructor]` export targets this resource: - 1. Give |interface| a constructor operation that throws when called (matching WebIDL's "no [Constructor]" semantics). - 1. Set |resourceInterfaces|[|export|.ResourceType] to |interface|. - 1. **Function**: - 1. Let |operation| be `create an operation from a component function` given |export|.Func. - 1. If |export| is tagged `[constructor]`: - 1. Let |interface| be |resourceInterfaces|[R]. - 1. Assert |interface| has no contructor operation yet. - 1. Add |operation| to |interface| as its constructor operation. - 1. Else if |export| is tagged `[method].`: - 1. Let |interface| be |resourceInterfaces|[R]. - 1. Add |operation| to |interface| as a regular interface member named `camel case`(|name|). - 1. Else if |export| is tagged `[static].`: - 1. Let |interface| be |resourceInterfaces|[R]. - 1. Add |operation| to |interface| as a static interface member named `camel case`(|name|). - 1. Else: - 1. Add |operation| to |namespace| as a regular member named `camel case`(|export|.Name). - 1. **Value**: - 1. Let |canonicalType| be `CanonicalWebIDLType`(|export|.Type) and |canonicalValue| be `ToCanonicalWebIDLValue`(|export|.Value). - 1. Add a constant of type |canonicalType| with value |canonicalValue| to |namespace|, named `camel case`(|export|.Name). - 1. **Instance**: - 1. TODO: Can we just recurse here? - 1. If |export| added a name to a fragment that already contained that name, throw a `TypeError`. -1. Let |exportsObject| be the result of [creating a namespace object](https://webidl.spec.whatwg.org/#namespace-object) for |namespace|. -1. Return |exportsObject|. - -Notes: -- Re-exported imported resources reuse the same WebIDL interface they were bound to at instantiation, so JS callers see the same identity on both sides of the boundary. -- Component-defined resources without a `[constructor]` export get an interface whose constructor throws. -- Component-defined resources generate a WebIDL interface without any inheritance. -- The WebIDL JS binding needs to be modified to handle an anonymous namespace that is not exposed on a global. This seems like a relatively simple modification to make. - -To `create an operation from a component function` given |componentFunc|: -1. Let |componentFuncType| be |componentFunc|.Type. -1. Let |componentParamTypes| be |componentFuncType|.Params. -1. Let |componentResultType| be |componentFuncType|.Result. -1. If |componentResultType| is `result` (top-level): - 1. Let |okComponentType| = T. - 1. Let |errorComponentType| = E. - 1. Let |throwing| = true. -1. Else: let - 1. Let |okComponentType| = |componentResultType|; - 1. Let |throwing| = false. -1. Let |webIDLParamTypes|[i] be `CanonicalWebIDLType`(|componentParamTypes|[i]) for each i -1. Let |webIDLResultType| be `CanonicalWebIDLType`(|okComponentType|). -1. Construct a WebIDL operation with parameter types |webIDLParamTypes| and return type |webIDLResultType|, whose body, given |webIDLParamValues|: - 1. For each i in |webIDLParamValues|: - 1. Let |componentParamValues|[i] = `FromCanonicalWebIDLValue`(|webIDLParamValues[i]|, |componentParamTypes|[i]). - 1. Let |componentResult| = Invoke |componentFunc| with [|componentParamValues|[0], ..., |componentParamValues|[n-1]]. - 1. TODO: What if the call traps? - 1. If |throwing| and |componentResult| is `error(`|e|`)`: - 1. Let |exception| be `create a component exception` for `|e|` - 1. Throw |exception|. - 1. Else if |throwing| and the result is `result.ok(`|v|`)`: - 1. Let |componentResult| be |v|. - 1. Else: - 1. Let |componentResult| be the returned component value. - 1. Return `ToCanonicalWebIDLValue`(|componentResult|). -1. Return the operation. - -To `create a component exception` for component value `|error|`: - 1. TODO: Create an instance of `ComponentException`, a derived interface of `DOMException`. - -## Open questions - -1. How can you dynamically pass different branches of a WebIDL union? - - The current rules work for statically passing different branches, but not dynamically. - - Passing a variant doesn't work. It's canonical WebIDL value is different from a union. -1. How to specify finalization and destructors? -1. How does own/borrow interact with WebIDL platform objects? -1. How do we support WebIDL callback function types? -1. How do we support downcasting/upcasting of WebIDL interfaces? -1. How to import/export attribute getters/setters? -1. How to export a component as an interface that is derived from another interface? From c161d8cc01451e9078906703b754a2779f7c4913 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Tue, 15 Sep 2026 16:19:08 -0500 Subject: [PATCH 3/7] [js-api] WIP checkout --- design/mvp/JS-Explainer.md | 48 +- design/mvp/JS-Reference.md | 871 +++++++++++++++++++++++-------------- 2 files changed, 579 insertions(+), 340 deletions(-) diff --git a/design/mvp/JS-Explainer.md b/design/mvp/JS-Explainer.md index 17c2158b..4ba4fbcf 100644 --- a/design/mvp/JS-Explainer.md +++ b/design/mvp/JS-Explainer.md @@ -186,10 +186,52 @@ const { instance } = await WebAssembly.instantiate(bytes, { element: Element }); instance.exports.find(document.body, "h1"); // "page-title" or null ``` -`Element` covers the type and both methods. The type import checks for `@@isWasmResourceOf`, and the methods are read off `Element.prototype` under their camelCase names, which is where JS finds them too. +`Element` covers the type and both methods. The type import brand checks against `Element`, which for a WebIDL interface object means the same `implements` check JS gets, and the methods are read off `Element.prototype` under their camelCase names, which is where JS finds them too. Because `find` takes a `borrow` of the *imported* type, JS keeps passing raw elements. Passing anything else fails the same brand check and is a `TypeError`, and `option` comes back as `null`. +### Importing a web API + +The example above still needs someone to write `{ element: Element }`. A component can skip that and take its imports straight from the global scope by importing `wasm:js/global`: + +```wat +(component + (import "wasm:js/global" (instance $g + (export "element" (type $element (sub resource))) + (export "[method]element.get-attribute" (func + (param "self" (borrow $element)) (param "name" string) + (result (option string)))) + (export "btoa" (func (param "data" string) (result string))) + )) + (alias export $g "element" (type $el)) + (export "encode-id" (func (param "el" (borrow $el)) (result (option string)))) +) +``` + +```js +const c = new WebAssembly.Component(bytes, { builtins: ["js/global"] }); +const { exports } = new WebAssembly.ComponentInstance(c); + +exports.encodeId(document.body); +``` + +Every field of the instance is read off the global under its JS name, so `element` finds `Element`, `[method]element.get-attribute` finds `Element.prototype.getAttribute`, and `btoa` finds the global function. That is all `wasm:js/global` does. The web API bindings come from the same rules as any other JS import, which is why the JS-API needs no per-API knowledge and why goals #3b and #3c keep holding: the component sees whatever the page sees, polyfills included, and an API that grows a method needs no new binding. Goal #3a is the one that does not follow, because a missing name is a link error and a component has nothing to feature test with. + +The lookups happen once, when the imports are read, so this costs nothing per call. + +Names that are not constructors work too. A singleton like `document` is a value import of `own`, which needs the component model's value imports feature, and `console`, which has no constructor to brand check against, is a nested instance import that reads `log` off the `console` object. + +Importing `wasm:js/global` grants the component everything the page can do, which is why it is opt-in through the same `builtins` compile option core modules use. With ESM the lever is the import map, which can point `wasm:js/global` at a JS module instead: + +```html + +``` + +What is missing is described in [the reference](./JS-Reference.md#what-the-global-object-cannot-express-yet). The short version: no properties, so `element.textContent` is not expressible; no way to hand a component function to `addEventListener`; and no way to feature test an API before importing it. + ### Exporting a resource A resource a component defines and exports becomes a class: @@ -218,8 +260,6 @@ c.increment(); // 2 Type names are PascalCase, so `counter` is `Counter`. `new` runs the component's `constructor`, methods live on `Counter.prototype`, and `Symbol.dispose` drops the handle. Dropping is what runs the component's destructor, so a `Counter` nobody disposes is dropped when it is collected, through a `FinalizationRegistry`. -A `borrow` the component hands out is different: it is only valid for the duration of the call it appeared in, and using it afterwards is a `TypeError`. - ### Loading with ESM [ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary, so a component loads anywhere a module does today. @@ -261,4 +301,4 @@ Conversions in are looser than conversions out, in the same places WebIDL's are. - `future`, `stream` and `error-context` have no binding yet, and neither do async start functions or top-level await. -Everything else we know is open is collected in the reference's [open questions](./JS-Reference.md#open-questions). +Everything else we know is open is collected in the reference's [follow ups](./JS-Reference.md#follow-ups). diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md index a1d258b8..af157021 100644 --- a/design/mvp/JS-Reference.md +++ b/design/mvp/JS-Reference.md @@ -2,15 +2,15 @@ This is the in-depth reference for the WebAssembly Component JS-API. See here for the higher-level [explainer](./JS-Explainer.md). -**This is a draft and is not complete. Major details are unresolved, and there are bugs. See "Open Questions" at the end for a sampling of them.** +**This is a draft and is not complete. Major details are unresolved, and there are bugs.** -## The WebAssembly Namespace +## The WebAssembly namespace -Extend the imperative WebAssembly JS-API interfaces to also allow validation/compilation/instantiation of components in addition to modules. +We extend the imperative WebAssembly JS-API interfaces to also allow validation, compilation, and instantiation of components in addition to modules. ```webidl interface Component { - constructor([AllowResizable] AllowSharedBufferSource bytes); + constructor([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); } interface ComponentInstance { @@ -20,12 +20,17 @@ interface ComponentInstance { typedef (Component or Module) InstantiateSource; +dictionary WebAssemblyInstantiatedComponentSource { + required Component component; + required ComponentInstance instance; +}; + [Exposed=*] namespace WebAssembly { // Same as before, but now will detect if the bytes are a component or module and dispatch differently. boolean validate([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); Promise compile([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); - Promise instantiate( + Promise<(WebAssemblyInstantiatedSource or WebAssemblyInstantiatedComponentSource)> instantiate( [AllowResizable] AllowSharedBufferSource bytes, optional object importObject, optional WebAssemblyCompileOptions options = {}); // Now takes an InstantiateSource instead of just a Module, and returns a @@ -35,28 +40,55 @@ namespace WebAssembly { } ``` -We also add an error type for components that return `result<_, E>` to JS: +We also add an error type for component functions that return `result<_, E>` to JS: ```webidl [Exposed=*] interface ComponentError : Error { - constructor(optional DOMString message = "", optional any payload); - readonly attribute any payload; + constructor(optional DOMString message = "", optional any data); + readonly attribute any data; }; ``` -`payload` is the converted `E` value. See [Create the exports object](#create-the-exports-object). +`data` is the converted `E` value. See [Create the exports object](#create-the-exports-object). + +## Validation/compilation + +Validation and compilation of components defer to the underlying component embedding interface. This reference adds nothing to it. + +## Entry points + +A `Component` has a [[Component]] internal slot holding a compiled component. A `ComponentInstance` has [[ComponentInstance]], [[Exports]], [[HostResourceTypes]], and [[GuestResourceClasses]] slots. + +To `construct a Component` given |bytes| and |options|: +1. Let |stableBytes| be a copy of the bytes held by |bytes|. +1. If |options| has any non-default values: + 1. Throw `TypeError`. +1. Let |component| be the result of compiling |stableBytes| as a component, per the embedding interface. +1. If compilation fails: + 1. Throw a `WebAssembly.CompileError`. +1. Set **this**.[[Component]] to |component|. + +To `construct a ComponentInstance` given a `Component` |component|, |importsObject|, and |compileOptions|: +1. Let |enabledBuiltins| be the result of parsing [`builtins`](#builtin-imports) from |compileOptions|. +1. Let |result| be ? [`instantiate a component from an imports object`](#instantiation) given |component|.[[Component]], |importsObject|, |enabledBuiltins|. +1. Set **this**.[[ComponentInstance]] to |result|.[[ComponentInstance]]. +1. Set **this**.[[Exports]] to |result|.[[Exports]]. +1. Set **this**.[[HostResourceTypes]] to |result|.[[HostResourceTypes]]. +1. Set **this**.[[GuestResourceClasses]] to |result|.[[GuestResourceClasses]]. -## Validation/Compilation +The `exports` getter returns **this**.[[Exports]]. -Validation and compilation of components just defer to the underlying component embedding interface. This explainer adds nothing to it. +`validate` is modified to validate the bytes as a component if the version flag is set to be a component. +`compile`/`instantiate` is modified to asynchronously compile/instantiate the bytes as a component if the version flag is set to be a component. ## Names -Component import/export `plainname's` contain [`label`s](Explainer.md#import-and-export-definitions) that must be transformed into an identifier for use with JS and the web. -Component import/export `interfacename's` (such as `wasi:http/handler@1.0.0`) have no JS name and are currently rejected with a `TypeError`. +Component import/export `plainname`s contain [`label`s](Explainer.md#import-and-export-definitions) that must be transformed into an identifier for use with JS. -We define a `PascalCase(label)` and `CamelCase(label)` below which are used throughout this spec. +Component import/export `interfacename`s (such as `wasi:http/handler@1.0.0`) are used as-is when converted to JS strings. + +We define `PascalCase(label)` and `CamelCase(label)` below; both are used throughout this spec. | `label` | `PascalCase` | `CamelCase` | |---|---|---| @@ -68,15 +100,18 @@ We define a `PascalCase(label)` and `CamelCase(label)` below which are used thro | `a1-2-3` | `A123` | `a123` | `LabelOf`(|name|), where |name| is a `plainname`, returns the label that names the definition in JS: -1. If |name| is `[method]r.n` or `[static]r.n`, return `n`. -1. If |name| is `[constructor]r`, return `r`. +1. If |name| is `[method]r.n` or `[static]r.n`: + 1. Return `n`. +1. If |name| is `[constructor]r`: + 1. Return `r`. 1. Return |name|. `Fragments`(|label|): 1. Return the List of Strings produced by splitting |label| on occurrences of U+002D (-). The hyphens themselves are discarded. `Capitalize`(|fragment|): -1. If |fragment| is an `acronym`, return |fragment|. +1. If |fragment| is an `acronym`: + 1. Return |fragment|. 1. Return |fragment| with its first character uppercased. `PascalCase`(|label|): @@ -96,27 +131,36 @@ We define a `PascalCase(label)` and `CamelCase(label)` below which are used thro The JS name of an import or export declaration is then: `JSName`(|decl|): -1. If |decl|.Name is an `interfacename`, throw a `TypeError`. -1. If |decl| is a type declaration, return `PascalCase`(`LabelOf`(|decl|.Name)). +1. If |decl|.Name is an `interfacename`: + 1. Return |decl|.Name. +1. If |decl| is a type declaration: + 1. Return `PascalCase`(`LabelOf`(|decl|.Name)). 1. Return `CamelCase`(`LabelOf`(|decl|.Name)). -TODO: `a-b` and `AB` are [strongly-unique](Explainer.md#name-uniqueness) but both `PascalCase` to the identical `AB`. This can lead to collisions in exports. We don't handle this yet. +An import's *specifier* is its [`external-id`](Explainer.md#import-and-export-definitions) attribute if it has one, and its JS name otherwise: + +`JSSpecifier`(|decl|): +1. If |decl| has an `external-id` attribute: + 1. Return that attribute's name. +1. Return `JSName(|decl|)`. ## Types and values Components and JS maintain separate type/value systems, so any value crossing the boundary needs a defined translation in both directions. This section specifies that translation as two abstract operations: - 1. `ToJSValue` - convert a component value to a JS value. Infallible. - 2. `ToComponentValue` - convert a JS value to a component value of a given type. Fallible. +1. `ToJSValue` - convert a component value to a JS value. +1. `ToComponentValue` - convert a JS value to a component value of a given type. -For every component value type `t` and every component value `v` of type `t`, `ToComponentValue(ToJSValue(v, t), t)` is `v`. The one exception being a `map` with duplicate keys (see [`ToJSValueMap`](#tojsvalue)). +Roundtripping from `ToJSValue` back through `ToComponentValue` is designed to be strictly the identity function with the following exceptions: + 1. a `map` with duplicate keys (see [`ToJSValueMap`](#tojsvalue)) + 2. Float NaNs are [canonicalized](CanonicalABI.md#loading) -The abstract operations are carefully designed so that JS scripts cannot intercept round-tripping a component value through JS, or converting a component value to/from a WebIDL value. This allows JS engines to easily fuse conversions and skip creation of intermediate JS values. This is explained in more detail [later](#fusing-component-value-conversions). +Every object a conversion creates belongs to the *conversion realm*: the realm of the `WebAssembly` namespace the component was instantiated through. ### ToJSValue -`ToJSValue(componentValue, componentValType)` converts a component value to a JS value. This algorithm is infallible. +`ToJSValue(componentValue, componentValType)` converts a component value to a JS value. It is infallible Dispatch on `componentValType`: @@ -126,58 +170,59 @@ Dispatch on `componentValType`: - `f32` / `f64` → Number, including NaN and infinities. - `char` → String containing exactly the one Unicode scalar value. - `string` → String [(well formed)](https://tc39.es/ecma262/#sec-isstringwellformedunicode). -- `list` → Uint8Array. +- `list` → a Uint8Array over a fresh ArrayBuffer holding the bytes. - `list` → `ToJSValueList`(the elements, T). - `list` → as `list`; `length` is `N`. - `tuple` → as `list`, with element `i` converted as `T_i`. - `record { f: T, ... }` → `ToJSValueRecord`(|componentValue|, the fields). - `flags "L"+` → `ToJSValueFlags`(|componentValue|, the labels). - `enum "L"+` → String, the label verbatim. -- `option` where T is not `option<_>` → `null` for `none`, else `ToJSValue`(the payload, T). +- `option` where T is not `option<_>` → **null** for `none`, else `ToJSValue`(the payload, T). - `variant`, and `option>`, and `result` outside return position → `ToJSValueVariant`(|componentValue|, the cases). In return position a `result` is unwrapped instead, into a return value or a thrown `ComponentError` (see [Read the imports](#read-the-imports-object) and [Create the exports object](#create-the-exports-object)). - `map` → `ToJSValueMap`(|componentValue|, K, V). -- `own` / `borrow` → the JS value for an imported `R`, an instance of `R`'s [resource class](#guest-resource-classes) for a component-defined `R`. See [Resource types](#resource-types). +- `own` / `borrow` → See [Resource types](#resource-types). - `future` → a Promise (TODO). - `stream` → a `ReadableStream` (TODO). - `error-context` → TODO. -`ToJSValueList(values, T)` returns a *component list object*: +`ToJSValueList(values, T)`: 1. Let |n| be the number of |values|. 1. Let |array| be `ArrayCreate`(|n|). -1. For each i in [0, |n|): perform `CreateDataPropertyOrThrow`(|array|, `ToString`(i), `ToJSValue`(|values|[i], T)). -1. Perform `DefinePropertyOrThrow`(|array|, `@@iterator`, PropertyDescriptor { [[Value]]: `%ComponentListValues%`, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **false** }). +1. For each i in [0, |n|): + 1. Perform `CreateDataPropertyOrThrow`(|array|, `ToString`(𝔽(i)), `ToJSValue`(|values|[i], T)). 1. Return |array|. -A *component list object* is mostly an ordinary Array object, with the exception of that non-configurable own `@@iterator`. This is important [for fusing value conversions](#fusing-component-value-conversions). `%ComponentListValues%` is a new built-in function that behaves like `%Array.prototype.values%` except that the iterator object it returns: - - has a null prototype, - - has an own, non-writable, non-configurable `next` method, - - and returns, from `next`, a fresh null-prototype object with own `value` and `done` data properties. - `ToJSValueRecord(value, fields)`: 1. Let |object| be `OrdinaryObjectCreate`(**null**). -1. For each field `f: T` of |fields|, in declaration order, perform `CreateDataPropertyOrThrow`(|object|, `CamelCase`(f), `ToJSValue`(|value|'s `f`, T)). +1. For each field `f: T` of |fields|, in declaration order: + 1. Perform `CreateDataPropertyOrThrow`(|object|, `CamelCase`(f), `ToJSValue`(|value|'s `f`, T)). 1. Return |object|. `ToJSValueFlags(value, labels)`: 1. Let |object| be `OrdinaryObjectCreate`(**null**). -1. For each label `L` of |labels|, perform `CreateDataPropertyOrThrow`(|object|, `CamelCase`(L), |value|'s `L` bit as a Boolean). +1. For each label `L` of |labels|: + 1. Perform `CreateDataPropertyOrThrow`(|object|, `CamelCase`(L), |value|'s `L` bit as a Boolean). 1. Return |object|. `ToJSValueVariant(value, cases)`: 1. Let |object| be `OrdinaryObjectCreate`(**null**). -1. Perform `CreateDataPropertyOrThrow`(|object|, "kind", |value|'s case label as a String). -1. If that case has a payload of type `T`, perform `CreateDataPropertyOrThrow`(|object|, "value", `ToJSValue`(the payload, T)). +1. Perform `CreateDataPropertyOrThrow`(|object|, "kind", `PascalCase`(`CaseLabelOf`(|value|, |cases|))). +1. If that case has a payload of type T: + 1. Perform `CreateDataPropertyOrThrow`(|object|, "value", `ToJSValue`(`PayloadOf`(|value|), T)). 1. Return |object|. `ToJSValueMap(value, K, V)`: -1. Let |map| be a new ordinary `Map` object with an empty [[MapData]]. +1. Let |map| be a new ordinary `Map` object with an empty [[MapData]] and the conversion realm's `%Map.prototype%`. 1. For each pair (k, v) of |value|, in order: - 1. Let |key| be `ToJSValue`(k, K) and |mapValue| be `ToJSValue`(v, V). - 1. If [[MapData]] has an entry whose key is `SameValueZero` to |key|, set that entry's value to |mapValue|. - 1. Else, append an entry (|key|, |mapValue|) to [[MapData]]. + 1. Let |key| be `ToJSValue`(k, K). + 1. Let |mapValue| be `ToJSValue`(v, V). + 1. If [[MapData]] has an entry whose key is `SameValueZero` to |key|: + 1. Set that entry's value to |mapValue|. + 1. Else: + 1. Append an entry (|key|, |mapValue|) to [[MapData]]. 1. Return |map|. -A `map` is a [specialization](Explainer.md#type-definitions) of `list>` where the last pair for a key defines its value. So `[(a,1),(a,2)]` round-trips from a component value to JS and back as `[(a,2)]`. This is the one exception to the round-tripping rules we have. +A `map` is a [specialization](Explainer.md#type-definitions) of `list>` where the last pair for a key defines its value. So `[(a,1),(a,2)]` round-trips from a component value to JS and back as `[(a,2)]`. ### ToComponentValue @@ -187,109 +232,173 @@ Dispatch on `targetComponentType`: - `bool` → `ToBoolean`(|jsValue|). - Integer types → `ToComponentValueInteger`(|jsValue|, the type). -- `f32` / `f64` → `ToNumber`(|jsValue|); for `f32`, round to the nearest f32 value (ties to even). `NaN` and infinities are accepted, as with `unrestricted float`/`unrestricted double`. -- `char` → `ToString`(|jsValue|); it must consist of exactly one Unicode scalar value, else throw a `TypeError`. A lone surrogate is not a scalar value and is therefore a `TypeError`. -- `string` → `ToString`(|jsValue|), then replace each unpaired surrogate with U+FFFD, matching WebIDL `USVString`. -- `list` → `new Uint8Array(ToComponentValueList(|jsValue|, u8))` +- Float types → `ToComponentValueFloat`(|jsValue|, the type). +- `char` → ? `ToString`(|jsValue|); it must consist of exactly one Unicode scalar value, else throw a `TypeError`. A lone surrogate is not a scalar value and is therefore a `TypeError`. +- `string` → ? `ToString`(|jsValue|), then replace each unpaired surrogate with U+FFFD (matching WebIDL `USVString`). +- `list` → `ToComponentValueBytes`(|jsValue|). - `list` → `ToComponentValueList`(|jsValue|, T). - `list` → as `list`, then the length must be exactly `N`, else throw a `TypeError`. - `tuple` → as `list`, then the length must be exactly the arity, and element `i` converts to `T_i`. - `record { f: T, ... }` → `ToComponentValueRecord`(|jsValue|, the fields). - `flags "L"+` → `ToComponentValueFlags`(|jsValue|, the labels). -- `enum` → `ToString`(|jsValue|) must be one of the labels, else throw a `TypeError`. -- `option` where T is not `option<_>` → `null` and **undefined** both give `none`; anything else gives `some(ToComponentValue(jsValue, T))`. This matches how WebIDL treats a nullable type. -- `variant`, and `option>`, and `result` outside return position → `ToComponentValueVariant`(|jsValue|, the cases). +- `enum` → ? `ToString`(|jsValue|) must be one of the labels, else throw a `TypeError`. +- `option` where T is not `option<_>` → **null** and **undefined** both give `none`; anything else gives `some(ToComponentValue(jsValue, T))` (matching how WebIDL treats a nullable type). +- `variant`, and `option>`, and `result` outside return position → `ToComponentValueVariant`(|jsValue|, the cases). In return position a `result` is unwrapped instead: a JS return value becomes `result.ok`, and a thrown exception becomes `result.error` (see [Read the imports](#read-the-imports-object) and [Create the exports object](#create-the-exports-object)). - `map` → `ToComponentValueMap`(|jsValue|, K, V). -- `own` / `borrow` → a [host resource value](#host-resource-types-and-values) for an imported `R`, the rep held by the given instance of `R`'s [resource class](#guest-resource-classes) for a component-defined `R`. See [Resource types](#resource-types). +- `own` / `borrow` → See [Resource types](#resource-types). - `future` → TODO. - `stream` → TODO. - `error-context` → TODO. `ToComponentValueInteger(jsValue, t)`: -1. If |t| is `s64` or `u64` and `Type`(|jsValue|) is BigInt, let |n| be |jsValue|'s value. -1. Else, let |n| be ? `ToNumber`(|jsValue|) put through WebIDL's [integer conversion](https://webidl.spec.whatwg.org/#abstract-opdef-converttoint) **as if `[EnforceRange]` were present**: `NaN` and infinities throw a `TypeError`, anything else truncates toward zero. -1. If |n| is outside |t|'s range, throw a `TypeError`. +1. If |t| is `s64` or `u64` and `Type`(|jsValue|) is BigInt: + 1. Let |n| be |jsValue|'s value. + 1. If |n| is outside |t|'s range: + 1. Throw a `TypeError`. + 1. Return |n|. +1. Let |number| be ? `ToNumber`(|jsValue|). +1. If |number| is `NaN` or an infinity: + 1. Throw a `TypeError`. +1. Let |n| be |number| truncated toward zero. +1. If |n| is outside |t|'s range: + 1. Throw a `TypeError`. 1. Return |n|. -`ToComponentValueList(jsValue, T)`: -1. If |jsValue| is not an Object, throw a `TypeError`. -1. If ? `IsArray`(|jsValue|) is **true** and |jsValue| is not a Proxy exotic object: - 1. Let |len| be ? `LengthOfArrayLike`(|jsValue|). - 1. For each i in [0, |len|): let |e_i| be ? `Get`(|jsValue|, `ToString`(i)), and append `ToComponentValue`(|e_i|, T). -1. Else: - 1. Let |method| be ? `GetMethod`(|jsValue|, `@@iterator`). If |method| is **undefined**, throw a `TypeError`. - 1. Iterate as WebIDL's sequence conversion does, converting each value with `ToComponentValue`(_, T). +`ToComponentValueFloat(jsValue, t)`: +1. let |num| = ? `ToNumber`(|jsValue|). +1. If |t| is `f32`: + 1. |num| = |num| rounded to the nearest f32 value (ties to even). +1. Return |num|. + + `NaN` and infinities are accepted (matching WebIDL `unrestricted float`/`unrestricted double`). + +`ToComponentValueBytes(jsValue)`: +1. If |jsValue| has a [[TypedArrayName]] internal slot whose value is "Uint8Array": + 1. If |jsValue|'s underlying buffer is detached or |jsValue| is out of bounds: + 1. Throw a `TypeError`. + 1. Return one `u8` per byte of |jsValue|, in order. +1. Return `ToComponentValueList`(|jsValue|, `u8`). + +A `Uint8Array` is copied directly, since that is what `ToJSValue` produces. Anything else (including other typed arrays) goes through the iterable path. -The `Array` case does not check `@@iterator`, so a patched `Array.prototype[@@iterator]` does not change what a component sees when handed an Array. This is important for [fusing value conversions](#fusing-component-value-conversions). +`ToComponentValueList(jsValue, T)`: +1. If |jsValue| is not an Object: + 1. Throw a `TypeError`. +1. Let |method| be ? `GetMethod`(|jsValue|, `%Symbol.iterator%`). +1. If |method| is **undefined**: + 1. Throw a `TypeError`. +1. Let |iteratorRecord| be ? `GetIteratorFromMethod`(|jsValue|, |method|). +1. Initialize |list| be an empty component list of type `T`. +1. Repeat + 1. Let |next| be ? `IteratorStepValue`(|iteratorRecord|). + 1. If |next| is done, then return |list|. + 1. Set |list| to |list| with ? `ToComponentValue`(|next|, |T|) appended to the end. `ToComponentValueRecord(jsValue, fields)`: -1. If |jsValue| is not an Object, throw a `TypeError`. +1. If |jsValue| is not an Object: + 1. Throw a `TypeError`. +1. Let |record| be a new component record value with one field per |fields|. 1. For each field `f: T` of |fields|, in declaration order: - 1. Let |m| be ? `GetOwnProperty`(|jsValue|, `CamelCase`(f)). - 1. If |m| is **undefined** and `T` is not `option<_>`, throw a `TypeError`. - 1. The field value is `ToComponentValue`(|m|, T). - -Extra properties are ignored. + 1. Let |m| be ? `Get`(|jsValue|, `CamelCase`(f)). + 1. If |m| is **undefined** and `T` is not `option<_>`: + 1. Throw a `TypeError`. + 1. Set |record|'s `f` field to `ToComponentValue`(|m|, T). +1. Return |record|. `ToComponentValueFlags(jsValue, labels)`: -1. If |jsValue| is not an Object, throw a `TypeError`. -1. For each label `L` of |labels|, the bit is `ToBoolean`(? `GetOwnProperty`(|jsValue|, `CamelCase`(L))). +1. If |jsValue| is not an Object: + 1. Throw a `TypeError`. +1. Let |flags| be a new component flags value with every bit initially **false**. +1. For each label `L` of |labels|: + 1. Set |flags|'s `L` bit to `ToBoolean`(? `Get`(|jsValue|, `CamelCase`(L))). +1. Return |flags|. -An absent property is therefore `false`, matching a `boolean` dictionary member defaulted to `false`. +An absent property is therefore **false**, matching a `boolean` dictionary member defaulted to **false**. `ToComponentValueVariant(jsValue, cases)`: -1. If |jsValue| is not an Object, throw a `TypeError`. -1. Let |kind| be `ToString`(? `GetOwnProperty`(|jsValue|, "kind")). It must be the label of one of |cases|, else throw a `TypeError`. -1. If that case has a payload type `T`, its payload is `ToComponentValue`(? `GetOwnProperty`(|jsValue|, "value"), T). Otherwise `value` is ignored. -1. Return that case. +1. If |jsValue| is not an Object: + 1. Throw a `TypeError`. +1. Let |kind| be ? `ToString`(? `Get`(|jsValue|, "kind")). +1. If |kind| is not the label of one of |cases|: + 1. Throw a `TypeError`. +1. Let |case| be the case of |cases| whose label is |kind|. +1. If |case| has a payload type T: + 1. Set |case|'s payload to `ToComponentValue`(? `Get`(|jsValue|, "value"), T). +1. Return |case|. `ToComponentValueMap(jsValue, K, V)`: -1. If |jsValue| is not an Object, throw a `TypeError`. -1. If |jsValue| has a [[MapData]] internal slot: - 1. Return one pair per entry of [[MapData]], in insertion order, converting each key with `ToComponentValue`(_, K) and each value with `ToComponentValue`(_, V). -1. If ? `GetMethod`(|jsValue|, `@@iterator`) is not **undefined**: +1. If |jsValue| is not an Object: + 1. Throw a `TypeError`. +1. If ? `GetMethod`(|jsValue|, `%Symbol.iterator%`) is not **undefined**: 1. Return `ToComponentValueList`(|jsValue|, `tuple`). -1. If `K` is not `string`, throw a `TypeError`. -1. Return one pair per own enumerable string-keyed property of |jsValue|, in property order, converting each value with `ToComponentValue`(_, V). +1. If K is not `string`: + 1. Throw a `TypeError`. +1. Return one pair per own enumerable string-keyed property of |jsValue|, in property order, reading each value with ? `Get` and converting it with `ToComponentValue`(_, V). -The `Map` case does not check `@@iterator`, so a patched `Map.prototype[@@iterator]` does not change what a component sees when handed a Map. This is important for [fusing value conversions](#fusing-component-value-conversions). -If a `Map` or `Iterable` is not provided, then we fallback to converting an object following `record` rules for compat with WebIDL. +If an `Iterable` is not provided, we fall back to converting the object the way WebIDL's `record` would, for compatibility. ## Resource types A component resource type can be defined in a component (i.e. a guest resource), or else as an imported abstract type (i.e. a host resource). The component JS-API defines: - 1. A protocol for defining host resource types in JS. - 2. A spec representation of host resource types and values. - 3. A JS representation of guest resource types and values. + +1. How a JS value satisfies a resource type import. +1. A spec representation of host resource types and values. +1. A JS representation of guest resource types and values. ### Embedder extensions -We sketch two things here that should be formalized more fully in the [embedding interface](CanonicalABI.md#embedding). +We sketch three things here that will be formalized more fully in the [embedding interface](CanonicalABI.md#embedding). -To `create a host resource type` given a host function |destructor|: -1. Return a fresh component resource type that whose representation is host-defined and whose destructor is |destructor|. +To `create a resource type for host` given a host function |destructor|: +1. Return a fresh component resource type whose representation is host-defined and whose destructor is |destructor|. -To `drop a host owned resource` given a resource type |resourceType| and a rep |rep| owned by the host: +To `drop a guest resource` given a resource type |resourceType| and a guest rep |rep| owned by the host: 1. Perform the effect of [`canon resource.drop`](CanonicalABI.md#canon-resourcedrop) on an owning handle holding |resourceType| and |rep|, invoking |resourceType|'s destructor. There is no handle table entry to remove, because the host was holding the rep. -1. If that traps, throw a `WebAssembly.RuntimeError`. +1. If that traps: + 1. Throw a `WebAssembly.RuntimeError`. -### Host resource types (i.e. imported) +### Abstract and transparent types + +Imported and exported resource types are either abstract or transparently equivalent to a previous abstract import or export. -#### The host resource type protocol +``` +(component + (import "r1" (type $r1 (sub resource))) + (import "r2" (type (eq $r1))) + (import "r3" (type (sub resource))) -We add a new well-known symbol, `@@isWasmResourceOf`, whose value is a predicate over JS values: + (export "r4" (type $r4 (sub resource))) + (export "r5" (type (eq $r4))) + (export "r6" (type (sub resource))) -```js -Constructor[Symbol.isWasmResourceOf] = (v) => /* return true iff v is an instance of resource type */; + (export "r7" (type (eq $r3))) +) ``` -A resource type import will check for this symbol during instantiation and snapshot it. The type check will be invoked each time a JS value needs to be converted to a resource value. +`r1`, `r3`, `r4`, `r6` are the abstract types of this component type, while `r2`, `r5`, and `r7` are transparently equal to one of the abstract types. + +Host/guest resource types below are created only for abstract types, and stored in maps on the component instance. The map is keyed by an *abstract type key* which is the import/export declaration for the abstract type. -If `@@isWasmResourceOf` is not found, then one is synthesized that performs an `instanceof` check. +An *abstract type key* can be found for any import/export declaration by following `eq R` until you reach a `sub resource`. + +### Host resource types (i.e. imported) -WebIDL is extended to define this property on every [interface object](https://webidl.spec.whatwg.org/#dfn-interface-object), returning **true** if and only if its argument is a platform object that [implements](https://webidl.spec.whatwg.org/#implements) that interface. +#### Brand checks + +A resource type import is satisfied by a constructor. Each time a JS value needs to be converted to a value of that resource type, it is *brand checked* against the constructor. + +To `brand check` given a JS value |jsValue| and an Object |constructor|: +1. If |constructor| is a WebIDL [interface object](https://webidl.spec.whatwg.org/#dfn-interface-object): + 1. Return **true** if and only if |jsValue| is a platform object that [implements](https://webidl.spec.whatwg.org/#implements) the interface |constructor| is the interface object of. +1. If |constructor| has a [[ConstructorFunc]] internal slot (i.e. it is a [guest resource class](#guest-resource-classes)): + 1. Return **true** if and only if |jsValue| has a [[ResourceClass]] internal slot whose value is |constructor|. +1. Return ? `InstanceofOperator`(|jsValue|, |constructor|). + +Which case applies is fixed for the lifetime of |constructor|. + +The first two cases are real brand checks. The `instanceof` fallback only inspects the prototype chain, so a value that was never created by |constructor| can pass it. In the future we may specify a way for JS to specify custom brand checks. #### Host resource types and values @@ -297,9 +406,8 @@ A *host resource type* is what the JS-API creates to satisfy a resource type imp | Field | Value | |---|---| -| [[ComponentResourceType]] | the component resource type produced by `create a host resource type` | -| [[ImportValue]] | the JS object that satisfied the import | -| [[IsWasmResourceOf]] | the type check snapshotted from that object | +| [[ComponentResourceType]] | the component resource type produced by `create a resource type for host` | +| [[ConstructorObject]] | the JS constructor that satisfied the import | A *host resource value* is the `rep` of a host resource type. It too is a Record: @@ -310,105 +418,96 @@ A *host resource value* is the `rep` of a host resource type. It too is a Record A host resource value just holds a strong reference to the underlying value. No user-level destructors are run when it is dropped. -To `read the type import` given |componentTypeBound| and |importValue|: -1. If |componentTypeBound| is not `(sub resource)`: - 1. Throw a `TypeError`. -1. If `Type`(|importValue|) is not Object: - 1. Throw a `TypeError`. -1. Let |isWasmResourceOf| be ? `GetV`(|importValue|, `@@isWasmResourceOf`). -1. If |isWasmResourceOf| is not callable: - 1. Let |isWasmResourceOf| be a built-in function that, given |jsValue|, returns ? `InstanceofOperator`(|jsValue|, |importValue|). -1. Let |destructor| be a host function that, given a host resource value, releases its reference to [[JSValue]] and returns. -1. Let |resourceType| be `create a host resource type` given |destructor|. -1. Return a host resource type whose [[ComponentResourceType]] is |resourceType|, whose [[ImportValue]] is |importValue| and whose [[IsWasmResourceOf]] is |isWasmResourceOf|. +One host resource type is created per [abstract type](#abstract-and-transparent-types). Two abstract type imports satisfied by the same JS constructor become distinct component resource types, and a handle for one cannot be passed where the other is expected. -One host resource type is created per resource type import declaration per instantiation. Two type imports satisfied by the same JS constructor become distinct component resource types, and a handle for one cannot be passed where the other is expected. Round tripping such a handle through JS does succeed, because JS only ever sees the wrapped value. +A map from resource type import declaration to host resource type is built by [`read the imports object`](#read-the-imports-object) and stored on a component instance. -#### Conversions for host resource types +#### Conversions for host resources -For a resource type `R` whose type variable is one of the component's type imports, let |hostType| be the host resource type `read the type import` produced for it: +For a resource type `R` whose abstract type is one of the component's type imports: - `ToJSValue(rep, own | borrow)`: + 1. Let |instance| be the surrounding component instance. + 1. Let |abstractTypeKey| be the *abstract type key* of |R|. + 1. Let |hostType| be |instance|.[[HostResourceTypes]][|abstractTypeKey|]. 1. Assert: |rep| is a host resource value whose [[Type]] is |hostType|. 1. Return |rep|.[[JSValue]]. - `ToComponentValue(jsValue, own | borrow)`: - 1. If ? `Call`(|hostType|.[[IsWasmResourceOf]], **undefined**, « |jsValue| ») is not **true**, throw a `TypeError`. + 1. Let |instance| be the surrounding component instance. + 1. Let |abstractTypeKey| be the *abstract type key* of |R|. + 1. Let |hostType| be |instance|.[[HostResourceTypes]][|abstractTypeKey|]. + 1. Let |matches| be ? `brand check` given |jsValue| and |hostType|.[[ConstructorObject]]. + 1. If |matches| is **false**: + 1. Throw a `TypeError`. 1. Return a host resource value whose [[Type]] is |hostType| and whose [[JSValue]] is |jsValue|. -Converting the same JS value to a host resource type will yield fresh handle indices. There is no canonicalization based on reference equality. +Converting the same JS value to a host resource type yields fresh handle indices. There is no canonicalization based on reference equality. ### Guest resource types (i.e. exported) #### Re-exported host resource types -A component can export an imported resource type in one of two ways: - 1. Transparently - by leaving it `eq`-bound to the import - 2. Opaquely - by ascribing it with `(sub resource)` - -This is visible in the component type that the embedder interface can inspect. +An exported resource type may be transparently equal to an imported resource type (see [abstract types](#abstract-and-transparent-types)). This currently throws, but may be relaxed in the future. -Transparent re-exports on top-level components are disallowed and trap during instantiation. This avoids the problem of figuring out how to mutate a pre-existing prototype to add new methods exported by a component. - -Opaque re-exports are allowed and wrap the original host resource type in a new guest resource class. This prevents leaking of the implementation decision of whether the resource type export is from an import or defined in the component. +An exported resource type that is privately a re-export of an imported type will wrap the original host resource type in a new [guest resource class](#guest-resource-classes). This keeps callers from observing whether an exported resource type is a re-export or defined in the component. #### Guest resource classes -An exported resource type is given a JS class. - -A component's type presents each of its exported resource types as an abstract type variable. A unique JS class is created for each type variable. +A unique JS *guest resource class* is created for each exported [abstract type](#abstract-and-transparent-types). A map from exported resource type declaration to guest resource class is stored on the component instance. -For example, the following will create a class for "r1" and "r3", while "r2" will re-use "r1"'s class. +A guest resource class is a built-in function object with extra internal slots: -```wat -(component - (export "r1" (type $r1 (sub resource))) - (export "r2" (type (eq $r1))) - (export "r3" (type (sub resource))) -) -``` - -Guest resource classes are created in multiple phases: - 1. Create constructor and prototype *shells* before instantiation - 2. Instantiate the component, possibly running `start` functions - 3. Finish creating the constructor and prototype, *linking* the methods from the exports - -This allows any resource values that escape during `start` to have a fixed prototype already created. - -A resource class is a built-in function object with one extra internal slot, [[ConstructorFunc]], holding the component function that implements `new` or **empty**. +| Slot | Value | +|---|---| +| [[ResourceType]] | the guest resource type | +| [[ConstructorFunc]] | the component function that implements `new`, or **empty** | +| [[ComponentInstance]] | the component instance the class belongs to | -To `create resource class shells` given a |component|: +To `create guest resource classes` given a component instance |componentInstance|: +1. Let |guestResourceClasses| be an empty map from export declaration to guest resource class. +1. Let |component| be |componentInstance|.[[Component]]. 1. For each type export |export| of |component|'s type, in declaration order, recursing into exported instances: - 1. Let |variable| be the abstract type |export| designates. - 1. If |variable| is one of |component|'s type imports, throw a `TypeError`. - 1. If a resource class is already associated with |variable| for this instantiation: + 1. Let |abstractTypeKey| be the *abstract type key* of |export|.Type. + 1. If |abstractTypeKey| is one of |component|'s type imports: + 1. Throw a `TypeError`. + 1. If |guestResourceClasses|[|abstractTypeKey|] exists: 1. Continue. 1. Let |arity| be the parameter count of the `[constructor]` export targeting |variable|, or 0 if there is none. - 1. Let |class| be `create a resource class shell` given `JSName`(|export|) and |arity|. - 1. Associate |class| with |variable| for this instantiation. + 1. Let |class| be `create a guest resource class` given |componentInstance|, |export|, `JSName`(|export|) and |arity|. + 1. Set |guestResourceClasses|[|abstractTypeKey|] to |class|. +1. Set |componentInstance|.[[GuestResourceClasses]] to |guestResourceClasses|. -To `create a resource class shell` given a String |name| and an integer |arity|: +To `create a guest resource class` given a component instance |componentInstance|, resource type |resourceType|, String |name| and an integer |arity|: 1. Let |prototype| be `OrdinaryObjectCreate`(`%Object.prototype%`). -1. Let |constructor| be a built-in function object with name |name|, length |arity| and a [[ConstructorFunc]] internal slot set to **empty**, whose [[Call]] throws a `TypeError`, and whose [[Construct]], given JS arguments |args| and |newTarget|, performs: - 1. If |constructor|.[[ConstructorFunc]] is **empty**, throw a `TypeError`. +1. Let |constructor| be a built-in function object with name |name| and length |arity|. +1. Set |constructor|.[[ResourceType]] to |resourceType|. +1. Set |constructor|.[[ConstructorFunc]] to **empty**. +1. Set |constructor|.[[ComponentInstance]] to |componentInstance|. +1. Set |constructor|'s [[Call]] behaviour to throw a `TypeError`. +1. Set |constructor|'s [[Construct]] behaviour, given JS arguments |args| and |newTarget|, to perform: + 1. If |constructor|.[[ConstructorFunc]] is **empty**: + 1. Throw a `TypeError`. 1. Let |rep| be ? `invoke a component function` given |constructor|.[[ConstructorFunc]], `[constructor]`, **undefined** and |args|. - 1. Let |resourceType| be the runtime resource type |constructor|.[[ConstructorFunc]]'s `own` result refers to. - 1. Return `create a resource instance` given |constructor|, |resourceType|, |rep|, **true** and |newTarget|. -1. Perform `DefinePropertyOrThrow`(|prototype|, `@@dispose`, PropertyDescriptor { [[Value]]: a built-in function that performs `drop a resource instance` given its **this** value, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). -1. Perform `DefinePropertyOrThrow`(|prototype|, `@@toStringTag`, PropertyDescriptor { [[Value]]: |name|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). + 1. Let |ownType| be |constructor|.[[ConstructorFunc]]'s result, or its `ok` payload if that result is a `result`. + 1. Return `create a guest resource instance` given |constructor|, |rep|, **true** and |newTarget|. +1. Perform `DefinePropertyOrThrow`(|prototype|, `%Symbol.dispose%`, PropertyDescriptor { [[Value]]: a built-in function that performs `drop a guest resource instance` given its **this** value, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. Perform `DefinePropertyOrThrow`(|prototype|, `%Symbol.toStringTag%`, PropertyDescriptor { [[Value]]: |name|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Perform `DefinePropertyOrThrow`(|prototype|, "constructor", PropertyDescriptor { [[Value]]: |constructor|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Perform `DefinePropertyOrThrow`(|constructor|, "prototype", PropertyDescriptor { [[Value]]: |prototype|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **false** }). -1. Perform `DefinePropertyOrThrow`(|constructor|, `@@isWasmResourceOf`, PropertyDescriptor { [[Value]]: a built-in predicate that returns **true** if and only if its argument has a [[ResourceClass]] internal slot whose value is |constructor|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. Let |tagged| be the `[constructor]`, `[method]` and `[static]` function exports in |componentInstance|'s scope that target |resourceType|. +1. If |tagged| has a `[constructor]` export |c|: + 1. Set |constructor|.[[ConstructorFunc]] to |c|.Func. +1. For each `[method]` export |m| of |tagged|: + 1. If `JSName`(|m|) is "constructor": + 1. Throw a `TypeError`. + 1. Perform `DefinePropertyOrThrow`(|constructor|'s `"prototype"`, `JSName`(|m|), PropertyDescriptor { [[Value]]: `create a JS function for a component function` given |m|.Func, `JSName`(|m|) and |m|'s tag, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). +1. For each `[static]` export |s| of |tagged|: + 1. If `JSName`(|s|) is "prototype": + 1. Throw a `TypeError`. + 1. Perform `DefinePropertyOrThrow`(|constructor|, `JSName`(|s|), PropertyDescriptor { [[Value]]: `create a JS function for a component function` given |s|.Func, `JSName`(|s|) and |s|'s tag, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Return |constructor|. -To `link resource classes` given a |componentInstance|: -1. For each type export |export| of |componentInstance|, in declaration order, recursing into exported instances: - 1. Let |variable| be the type variable |export| designates and |class| be the resource class associated with |variable|. - 1. If |class| was already linked by an earlier iteration, continue. - 1. Let |tagged| be the `[constructor]`, `[method]` and `[static]` function exports in |export|'s scope that target |variable|. - 1. If |tagged| has a `[constructor]` export |c|, set |class|.[[ConstructorFunc]] to |c|.Func. - 1. For each `[method]` export |m| of |tagged|: - 1. Perform `DefinePropertyOrThrow`(|class|'s `"prototype"`, `JSName`(|m|), PropertyDescriptor { [[Value]]: `create a JS function for a component function` given |m|.Func, `JSName`(|m|) and |m|'s tag, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). - 1. For each `[static]` export |s| of |tagged|, define the corresponding property on |class| with the same attributes. +A method named `constructor` and a static named `prototype` are rejected because they would unexpectedly change JS class semantics. #### Guest resource instances @@ -417,199 +516,282 @@ An instance of a guest resource class holds the same state a handle table entry | Slot | Value | |---|---| | [[ResourceClass]] | the resource class this is an instance of | -| [[ResourceType]] | the runtime component resource type | -| [[Rep]] | the rep, or **empty** once the handle has been dropped, transferred away, or expired | +| [[Rep]] | the rep, or **empty** once the handle has been dropped | | [[Own]] | whether this instance owns the resource | | [[LendCount]] | how many outstanding `borrow`s were lent from this instance | -For each instantiation: a class, a type variable and a runtime resource type are all in one-to-one correspondence, so [[ResourceClass]] is what the conversions type check against and [[ResourceType]] is only there to drop the resource with. - -To `create a resource instance` given a resource class |class|, a runtime resource type |resourceType|, |rep|, |own| and an optional |newTarget|: +To `create a guest resource instance` given a resource class |class|, |rep|, |own| and an optional |newTarget|: 1. Let |defaultProto| be the value of |class|'s `"prototype"` property. 1. If |newTarget| is present: 1. Let |proto| be ? `Get`(|newTarget|, "prototype"). - 1. If `Type`(|proto|) is not Object, set |proto| to |defaultProto|. -1. Else, let |proto| be |defaultProto|. -1. Let |instance| be `OrdinaryObjectCreate`(|proto|, « [[ResourceClass]], [[ResourceType]], [[Rep]], [[Own]], [[LendCount]] »). + 1. If `Type`(|proto|) is not Object: + 1. Set |proto| to |defaultProto|. +1. Else: + 1. Let |proto| be |defaultProto|. +1. Let |instance| be `OrdinaryObjectCreate`(|proto|, « [[ResourceClass]], [[Rep]], [[Own]], [[LendCount]] »). 1. Set |instance|.[[ResourceClass]] to |class|. -1. Set |instance|.[[ResourceType]] to |resourceType|. 1. Set |instance|.[[Rep]] to |rep|. 1. Set |instance|.[[Own]] to |own|. 1. Set |instance|.[[LendCount]] to 0. -1. If |own| is **true**, register |instance| in the JS-API's resource `FinalizationRegistry` with held value (|resourceType|, |rep|) and unregister token |instance|. +1. If |own| is **true**: + 1. Register |instance| in the [guest resource `FinalizationRegistry`](#guest-resource-finalizationregistry) with held value |instance| and unregister token |instance|. 1. Return |instance|. -For a resource type `R` whose type variable is one of the component's type exports, let |class| be the resource class associated with that variable: +#### Conversions for guest resources + +The *current lender list* is a per-call spec state. `invoke a component function` establishes it for a JS-to-component call. During the JS-component call, every instance has its [[LendCount]] incremented to protect against being dropped while lent. After the call, the [[LendCount]] is decremented. + +For a resource type `R` whose [abstract type](#abstract-and-transparent-types) is one of the component's type exports: - `ToJSValue(rep, own)`: - 1. Return `create a resource instance` given |class|, `R`, |rep| and **true**. + 1. Let |instance| be the surrounding component instance. + 1. Let |abstractTypeKey| be the *abstract type key* of |R|. + 1. Let |class| be |instance|.[[GuestResourceClasses]][|abstractTypeKey|]. + 1. Return `create a guest resource instance` given |class|, `R`, |rep| and **true**. - `ToJSValue(rep, borrow)`: - 1. Let |instance| be `create a resource instance` given |class|, `R`, |rep| and **false**. - 1. Append |instance| to the current borrow scope. - 1. Return |instance|. + 1. Assert: unreachable. + 1. This can only happen if an exported function returns a borrow, which is not allowed. - `ToComponentValue(jsValue, own)`: - 1. If |jsValue| does not have a [[ResourceClass]] internal slot, or |jsValue|.[[ResourceClass]] is not |class|, throw a `TypeError`. - 1. If |jsValue|.[[Rep]] is **empty**, or |jsValue|.[[Own]] is **false**, or |jsValue|.[[LendCount]] is not 0, throw a `TypeError`. - 1. Let |rep| be |jsValue|.[[Rep]]. Set |jsValue|.[[Rep]] to **empty** and unregister |jsValue| from the resource `FinalizationRegistry`. + 1. Let |instance| be the surrounding component instance. + 1. Let |abstractTypeKey| be the *abstract type key* of |R|. + 1. Let |class| be |instance|.[[GuestResourceClasses]][|abstractTypeKey|]. + 1. If |jsValue| does not have a [[ResourceClass]] internal slot, or |jsValue|.[[ResourceClass]] is not |class|: + 1. Throw a `TypeError`. + 1. If |jsValue|.[[Rep]] is **empty**, or |jsValue|.[[Own]] is **false**, or |jsValue|.[[LendCount]] is not 0: + 1. Throw a `TypeError`. + 1. Let |rep| be |jsValue|.[[Rep]]. + 1. Set |jsValue|.[[Rep]] to **empty**. + 1. Unregister |jsValue| from the [guest resource `FinalizationRegistry`](#guest-resource-finalizationregistry). 1. Return |rep|. - `ToComponentValue(jsValue, borrow)`: - 1. If |jsValue| does not have a [[ResourceClass]] internal slot, or |jsValue|.[[ResourceClass]] is not |class|, throw a `TypeError`. - 1. If |jsValue|.[[Rep]] is **empty**, throw a `TypeError`. - 1. Increment |jsValue|.[[LendCount]] and append |jsValue| to the current lender list. + 1. Let |instance| be the surrounding component instance. + 1. Let |abstractTypeKey| be the *abstract type key* of |R|. + 1. Let |class| be |instance|.[[GuestResourceClasses]][|abstractTypeKey|]. + 1. If |jsValue| does not have a [[ResourceClass]] internal slot, or |jsValue|.[[ResourceClass]] is not |class|: + 1. Throw a `TypeError`. + 1. If |jsValue|.[[Rep]] is **empty**: + 1. Throw a `TypeError`. + 1. Assert: a lender list is currently established. + 1. Increment |jsValue|.[[LendCount]]. + 1. Append |jsValue| to the current lender list. 1. Return |jsValue|.[[Rep]]. -A fresh instance is created for every lift, so two `borrow`s of the same resource are two JS objects that do not compare equal. +#### Guest resource FinalizationRegistry -The *current borrow scope* and *current lender list* are per-call spec state: -- `read the function import` establishes a borrow scope for a component-to-JS call. Once the JS call completes, every instance in the scope has its [[Rep]] set to **empty**, so JS holding on to a `borrow` past the call gets a `TypeError` on next use. -- `invoke a component function` establishes a lender list for a JS-to-component call. Once the component call completes, every instance in the list has its [[LendCount]] decremented. +There is an un-exposed `FinalizationRegistry` created per-Realm of the WebAssembly namespace object. The callback for it invokes `drop a guest resource instance` with the held value. -#### Dropping guest resources - -To `drop a resource instance` given |instance|: -1. If |instance| does not have a [[ResourceClass]] internal slot, throw a `TypeError`. -1. If |instance|.[[Rep]] is **empty** or |instance|.[[Own]] is **false**, return **undefined**. -1. If |instance|.[[LendCount]] is not 0, throw a `TypeError`. -1. Let |rep| be |instance|.[[Rep]]. Set |instance|.[[Rep]] to **empty** and unregister |instance| from the resource `FinalizationRegistry`. -1. Perform ? `drop a host owned resource` given |instance|.[[ResourceType]] and |rep|. +To `drop a guest resource instance` given |resourceInstance|: +1. If |resourceInstance|.[[Rep]] is **empty** or |resourceInstance|.[[Own]] is **false**: + 1. Return **undefined**. +1. Let |class| be |resourceInstance|.[[ResourceClass]]. +1. If |resourceInstance|.[[LendCount]] is not 0: + 1. Throw a `TypeError`. +1. Let |rep| be |resourceInstance|.[[Rep]]. +1. Set |resourceInstance|.[[Rep]] to **empty**. +1. Unregister |resourceInstance| from the guest resource `FinalizationRegistry`. +1. Perform `drop a guest resource` given |class|.[[ResourceType]] and |rep|. 1. Return **undefined**. -Dropping is idempotent, and dropping a `borrow` instance does nothing because there is nothing to give back. The [[LendCount]] check makes disposing an instance that is currently lent to a component a `TypeError` rather than a trap. - -`create a resource instance` adds `own` instances to a resource `FinalizationRegistry`. When the value is finalized, the host performs `drop a host owned resource` with the held (resource type, rep) pair. - -### Fusing component value conversions - -TODO. - ## Instantiation -To `instantiate a component` given |component| and a list of component definitions |imports|: -1. Perform ? `create resource class shells` given |component|. -1. Instantiate |component| with |imports|. - 1. If instantiation traps, throw a `WebAssembly.RuntimeError`. -1. Let |instance| be the resulting component instance. -1. Perform `link resource classes` given |instance|. +To `instantiate a component` given |component|, a list of component definitions |imports|, and |hostResourceTypes|: +1. Let |instance| be the result of instantiating |component| with |imports|. +1. If instantiation traps: + 1. Throw a `WebAssembly.RuntimeError`. +1. Perform ? `create guest resource classes` given |instance|. 1. Let |exportsObject| be ? `create the exports object` given |instance|. -1. Return a new `ComponentInstance` whose [[ComponentInstance]] is |instance| and whose [[Exports]] is |exportsObject|. +1. Return a Record whose [[ComponentInstance]] is |instance|, [[Exports]] is |exportsObject|, and [[HostResourceTypes]] is |hostResourceTypes|. -To `instantiate a component from an imports object` given |component| and |importsObject|: -1. Let |imports| be ? `read the imports` given |component| and |importsObject|. -1. Return ? `instantiate a component` given |component| and |imports|. +To `instantiate a component from an imports object` given |component|, |importsObject|, and |enabledBuiltins|: +1. Let |imports| and |hostResourceTypes| be ? `read the imports` given |component|, |importsObject|, and |enabledBuiltins|. +1. Return ? `instantiate a component` given |component|, |imports|, and |hostResourceTypes|. ### Read the imports object -The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the core JS-API's algorithm of the same name. The resolved JS values are then handed to the per-sort algorithms (`read the function import`, `read the type import` and friends) to produce the component definitions used during instantiation. +The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the core JS-API's algorithm of the same name. The resolved JS values are then handed to the per-sort algorithms (`read the function import`, `read the type import`, and the rest) to produce the component definitions used during instantiation. While walking, the algorithm recognizes the pattern of a resource type import accompanied by `[constructor]`, `[static]`, and `[method]` function imports tied to it. A resource type import is read first and looks for a constructor (see [resource types](#resource-types)). The tagged function imports then read from the constructor and its prototype directly. This allows the common case of importing a class to be satisfied by just passing the constructor. -Passing an exported component definition to a component import via the JS-API/ESM-integration is treated as if the import was a JS value. There is no "direct linking" that bypasses going through JS semantics. This is different from core wasm, where wasm exported functions are linked directly when imported and have stricter type checks. This is intentional to ensure that implementing an ES module using a component doesn't subtly change the behavior because it starts directly linking to components. Component definitions can still be directly linked within a top-level invocation of `instantiate a component`. +Passing an exported component definition to a component import via the JS-API/ESM-integration is treated as if the import were a JS value. There is no "direct linking" that bypasses going through JS semantics. This is different from core wasm, where exported functions are linked directly when imported and have stricter type checks. This is intentional to prevent the implementation detail of how a JS function was implemented from leaking. Components can still nested and directly linked inside a single binary. -Every name looked up on a JS object is `JSName`(|decl|) (see "Names"). - -To `read the imports` given |component| and |importsObject|: +To `read the imports` given |component|, |importsObject|, and |enabledBuiltins|: +1. Let |hostResourceTypes| be an empty map from imported resource type declaration to [host resource type record](#host-resource-types-and-values). 1. If |component| has no imports: - 1. Return an empty list. -1. Return ? `read a scope of imports` given |component|.Imports and |importsObject|. + 1. Return an empty list and |hostResourceTypes|. +1. Let |imports| be ? `read a scope of imports` given |component|.Imports, |importsObject|, |enabledBuiltins|, and |hostResourceTypes|. +1. Return |imports| and |hostResourceTypes|. -To `read a scope of imports` given a list of declarations |declarations| and |object|: -1. If `Type`(|object|) is not Object: - 1. Throw a `TypeError`. -1. Let |resourceTypes| be a new empty map keyed by resource type declaration, holding host resource types. +To `read a scope of imports` given a list of import declarations |importDecls|, |importsObject|, |enabledBuiltins|, and |hostResourceTypes|: 1. Let |definitions| be a new empty list. -1. For each |decl| of |declarations|, in declaration order: - 1. Let |name| be `JSName`(|decl|). - 1. If |decl|.Sort is **func** and |decl|.Name is tagged `[constructor]`, `[method].` or `[static].`: - 1. Let R be the resource type declared by the type declaration named by the tag's `` label. - 1. Assert: |resourceTypes|[R] exists. (Validation requires that declaration to precede this one in the same scope) - 1. Let |constructorFunction| be |resourceTypes|[R].[[ImportValue]]. - 1. If the tag is `[constructor]`: - 1. Let |importValue| be |constructorFunction|. - 1. Else if the tag is `[static]`: - 1. Let |importValue| be ? `GetV`(|constructorFunction|, |name|). - 1. Else: - 1. Let |prototype| be ? `GetV`(|constructorFunction|, "prototype"). - 1. If `Type`(|prototype|) is not Object, throw a `TypeError`. - 1. Let |importValue| be ? `GetV`(|prototype|, |name|). +1. For each |importDecl| of |importDecls|, in declaration order: + 1. Let |name| be `JSSpecifier`(|importDecl|). + 1. Let |builtin| be `resolve a builtin specifier` given |name| and |enabledBuiltins|. + 1. If |builtin| is not **empty**: + 1. Let |importValue| be |builtin|. 1. Else: - 1. Let |importValue| be ? `GetV`(|object|, |name|). - - 1. Let |resolved| be ? `read an import` given |decl|, |importValue| and |resourceTypes|. - 1. If |decl|.Sort is **type**: - 1. Set |resourceTypes|[|decl|.ResourceType] to |resolved|, and append |resolved|.[[ComponentResourceType]] to |definitions|. - 1. Else, append |resolved| to |definitions|. + 1. If |importDecl|.Sort is **func** and |importDecl|.Name is tagged `[constructor]`, `[method].` or `[static].`: + 1. Let |R| be the imported resource type declaration named by the tag's `` label. + 1. Assert: |hostResourceTypes|[|R|] exists. (Validation requires that declaration to precede this one in the same scope) + 1. Let |constructorFunction| be |hostResourceTypes|[|R|].[[ConstructorObject]]. + 1. If the tag is `[constructor]`: + 1. Let |importValue| be |constructorFunction|. + 1. Else if the tag is `[static]`: + 1. Let |importValue| be ? `Get`(|constructorFunction|, |name|). + 1. Else: + 1. Let |prototype| be ? `Get`(|constructorFunction|, "prototype"). + 1. If `Type`(|prototype|) is not Object: + 1. Throw a `WebAssembly.LinkError`. + 1. Let |importValue| be ? `Get`(|prototype|, |name|). + 1. Else: + 1. If `Type`(|object|) is not Object: + 1. Throw a `TypeError`. + 1. Let |importValue| be ? `Get`(|object|, |name|). + 1. Let |resolved| be ? `read an import` given |importDecl|, |importValue| and |hostResourceTypes|. + 1. Append |resolved| to |definitions|. 1. Return |definitions|. -A type import resolves to a [host resource type](#host-resource-types-and-values), which is a JS-API record wrapping the component resource type. The component only ever gets the resource type, but the record is kept around for the rest of the scope's function imports and for the exports object. - -To `read an import` given |decl|, |importValue| and |resourceTypes|: +To `read an import` given |importDecl|, |importValue| and |hostResourceTypes|: 1. Match |decl|.Sort: - 1. **core module**: return ? `read the core module import` given |decl|.ModuleType and |importValue|. - 1. **func**: return ? `read the function import` given |decl|.FuncType, |importValue|, |decl|.Name's tag and |resourceTypes|. - 1. **type**: return ? `read the type import` given |decl|.TypeBound and |importValue|. - 1. **value**: return ? `read the value import` given |decl|.ValType and |importValue|. - 1. **instance**: return ? `read the instance import` given |decl|.InstanceType and |importValue|. - 1. **component**: return ? `read the component import` given |decl|.ComponentType and |importValue|. + 1. **core module**: return ? `read the core module import` given |importDecl|.ModuleType and |importValue|. + 1. **func**: return ? `read the function import` given |importDecl|.FuncType, |importValue|, |importDecl|.Name's tag and |hostResourceTypes|. + 1. **type**: return ? `read the type import` given |importDecl|, |importValue|, and |hostResourceTypes|. + 1. **value**: return ? `read the value import` given |importDecl|.ValType and |importValue|. + 1. **instance**: return ? `read the instance import` given |importDecl|.InstanceType and |importValue|. + 1. **component**: return ? `read the component import` given |importDecl|.ComponentType and |importValue|. To `read the core module import` given |coreModuleType| and |importValue|: 1. If |importValue| does not have a [[Module]] internal slot: - 1. Throw a `TypeError`. -1. If the type of |importValue|.[[Module]] is not a subtype of |coreModuleType|: + 1. Throw a `WebAssembly.LinkError`. +1. If the type of |importValue|.[[Module]] is not equal to |coreModuleType|: 1. Throw a `WebAssembly.LinkError`. 1. Return |importValue|.[[Module]]. To `read the component import` given |componentType| and |importValue|: 1. If |importValue| does not have a [[Component]] internal slot: - 1. Throw a `TypeError`. + 1. Throw a `WebAssembly.LinkError`. 1. If the type of |importValue|.[[Component]] is not a subtype of |componentType|: 1. Throw a `WebAssembly.LinkError`. 1. Return |importValue|.[[Component]]. To `read the instance import` given |instanceType| and |importValue|: -1. Let |definitions| be ? `read a scope of imports` given |instanceType|.Exports and |importValue|. +1. If `Type`(|importValue|) is not Object: + 1. Throw a `WebAssembly.LinkError`. +1. Let |definitions| be ? `read a scope of imports` given |instanceType|.Exports, |importValue| and an empty set of enabled builtins. 1. Return a component instance whose exports are |definitions|. -To `read the function import` given |componentFuncType|, |importValue|, |importNameTag| and |resourceTypes|: +To `read the type import` given |importDecl|, |importValue|, and |hostResourceTypes|: +1. Let |typeBound| be |importDecl|.TypeBound. +1. Let |abstractTypeKey| be the *abstract type key* of |typeBound|. +1. If |hostResourceTypes|[|abstractTypeKey|] exists: + 1. Return |hostResourceTypes|[|abstractTypeKey|].[[ComponentResourceType]]. +1. If `IsCallable`(|importValue|) is **false**: + 1. Throw a `WebAssembly.LinkError`. +1. Let |destructor| be a host function that, given a host resource value, releases its reference to [[JSValue]] and returns. +1. Let |resourceType| be `create a resource type for host` given |destructor|. +1. Let |hostResourceType| be a new host resource type record whose [[ComponentResourceType]] is |resourceType| and whose [[ConstructorObject]] is |importValue|. +1. Set |hostResourceTypes|[|abstractTypeKey|] to |hostResourceType|. +1. Return |resourceType|. + +To `read the function import` given |componentFuncType|, |importValue|, |importNameTag| and |hostResourceTypes|: 1. If |importValue| is not callable: - 1. Throw a `TypeError`. -1. Let |paramTypes| be |componentFuncType|.Params and |resultType| be |componentFuncType|.Result. + 1. Throw a `WebAssembly.LinkError`. +1. If |importNameTag| is `[constructor]` and `IsConstructor`(|importValue|) is **false**: + 1. Throw a `WebAssembly.LinkError`. +1. Let |callable| be |importValue|. +1. Let |paramTypes| be |componentFuncType|.Params. +1. Let |resultType| be |componentFuncType|.Result. 1. Let |callKind|, |receiverRule| and |paramOffset| be determined by |importNameTag|: 1. `[constructor]`: `Construct`, no receiver, offset 0. 1. `[method].`: `Call`, receiver is component argument 0 (the `borrow` self), offset 1. - 1. `[static].`: `Call`, receiver is |resourceTypes|[R].[[ImportValue]], offset 0. + 1. `[static].`: `Call`, receiver is |hostResourceTypes|[R].[[ConstructorObject]], offset 0. 1. otherwise: `Call`, receiver is **undefined**, offset 0. -1. If |resultType| is `result`: - 1. Let |okType| be T, |errorType| be E, and |throwing| be **true**. +1. If |resultType| is a `result`: + 1. Let |okType| be its `ok` payload type, or **empty** if it has none. + 1. Let |errorType| be its `error` payload type, or **empty** if it has none. + 1. Let |throwing| be **true**. 1. Else: - 1. Let |okType| be |resultType| and |throwing| be **false**. + 1. Let |okType| be |resultType|, or **empty** if |componentFuncType| has no result. + 1. Let |throwing| be **false**. 1. Return a component host function of type |componentFuncType| whose body, given component arguments « |v_0|, ..., |v_{n-1}| », performs: - 1. Let |borrowScope| be a new empty List, and set the current borrow scope to |borrowScope|, saving the previous one. However this body completes, set the [[Rep]] of every instance in |borrowScope| to **empty** and restore the previous borrow scope before returning. - 1. If |receiverRule| is "component argument 0": - 1. Let |thisArg| be `ToJSValue`(|v_0|, |paramTypes|[0]). - 1. Else: - 1. Let |thisArg| be the receiver named by |receiverRule|. 1. Let |args| be a new empty List. 1. For each i in [|paramOffset|, n): 1. Append `ToJSValue`(|v_i|, |paramTypes|[i]) to |args|. - 1. If |callKind| is `Construct`, let |completion| be `Construct`(|callable|, |args|); else let |completion| be `Call`(|callable|, |thisArg|, |args|). + 1. If |callKind| is `Construct`: + 1. Let |completion| be `Construct`(|callable|, |args|). + 1. Else: + 1. If |receiverRule| is "component argument 0": + 1. Let |thisArg| be `ToJSValue`(|v_0|, |paramTypes|[0]). + 1. Else: + 1. Let |thisArg| be |receiverRule|. + 1. Let |completion| be `Call`(|callable|, |thisArg|, |args|). 1. If |completion| is an abrupt completion: - 1. If |throwing| is **false**, trap. - 1. Let |errorValue| be `ToComponentValue`(|completion|.[[Value]], |errorType|). If that throws, trap. + 1. If |throwing| is **false**: + 1. Trap. + 1. If |errorType| is **empty**: + 1. Return `result.error`. + 1. Let |errorValue| be `ToComponentValue`(|completion|.[[Value]], |errorType|). + 1. If that throws: + 1. Trap. 1. Return `result.error(|errorValue|)`. - 1. Let |componentResult| be `ToComponentValue`(|completion|.[[Value]], |okType|). If that throws, trap. - 1. If |throwing| is **true**, return `result.ok(|componentResult|)`; else return |componentResult|. - -The borrow scope covers the whole body, so a `borrow` of a component-defined resource is usable for the duration of the call, including from a callback the JS function passes back into the component, and is a `TypeError` to use afterwards. + 1. If |okType| is **empty**: + 1. If |throwing| is **true**: + 1. Return `result.ok`. + 1. Return with no result. + 1. Let |componentResult| be `ToComponentValue`(|completion|.[[Value]], |okType|). + 1. If that throws: + 1. Trap. + 1. If |throwing| is **true**: + 1. Return `result.ok(|componentResult|)`. + 1. Else: + 1. Return |componentResult|. To `read the value import` given |componentValType| and |importValue|: -1. Return `ToComponentValue`(|importValue|, |componentValType|). If that throws, propagate the exception. +1. Return ? `ToComponentValue`(|importValue|, |componentValType|). + +### Builtin imports + +The component JS-API can provide builtins to imports just as the core JS-API does. + +Builtin imports are opt-in via `WebAssemblyCompileOptions` when used in the JS-API. [ESM-integration](#webassembly-esm-integration) enables all builtins by default. + +This spec defines one builtin specifier: + +| Specifier | Resolves to | +|---|---| +| `wasm:js/global` | [the global object](#the-global-object) | + +To `resolve a builtin specifier` given a String |specifier| and a set of Strings |enabledBuiltins|: +1. If |specifier| is not in |enabledBuiltins|: + 1. Return **empty**. +1. If |specifier| is "wasm:js/global": + 1. Return the current realm's global object. +1. Return **empty**. + +#### The global object + +`wasm:js/global` can be used to import JS/web API's off of the global object. It simply resolves to `globalThis`, and then the normal [`read the imports object`](#read-the-imports-object) rules can take it from there. + +The following component imports `Element`, `Element.prototype.getAttribute`, and `btoa`: + +```wat +(component + (import "wasm:js/global" (instance $g + (export "element" (type $element (sub resource))) + (export "[method]element.get-attribute" (func + (param "self" (borrow $element)) (param "name" string) + (result (option string)))) + (export "btoa" (func (param "data" string) (result string))) + )) + ... +) +``` ### Create the exports object The `create the exports object` algorithm walks the component's exports and builds a fresh JS object whose properties are the exports. -Component-defined resource types become [resource classes](#guest-resource-classes) named `JSName`(|export|), and tagged function exports are mapped onto them just as in `read the imports`: +Exported resource types become [guest resource classes](#guest-resource-classes) named `JSName`(|export|), and tagged function exports are mapped onto them just as in `read the imports`: - `[constructor]`: the function becomes `R`'s constructor behaviour. By strong-uniqueness there can only be one. - `[method].`: the function becomes a method named `JSName`(|export|) on `R.prototype`. - `[static].`: the function becomes a static method named `JSName`(|export|) on `R`. @@ -618,20 +800,16 @@ All other exported components definitions are given JS definitions named `JSName To `create the exports object` given a |componentInstance|: 1. Let |exportsObject| be `OrdinaryObjectCreate`(**null**). -1. For each |export| of |componentInstance|.|component|.Exports, in declaration order: - 1. If |export|.Name is tagged `[constructor]`, `[method].` or `[static].`, it is consumed by `link resource classes` for R; continue. +1. For each |export| of |componentInstance|.Exports, in declaration order: + 1. If |export|.Name is tagged `[constructor]`, `[method].` or `[static].`: + 1. Continue. 1. Let |key| be `JSName`(|export|). 1. Match |export|.Sort: 1. **core module**: 1. Let |value| be a new `Module` whose [[Module]] is |export|.Module. 1. **type**: - 1. If |export|.Type is not a resource type: - 1. Throw `TypeError`. - 1. Let |variable| be the abstract type |export| designates. - 1. If |variable| is one of the component's type imports: - 1. Throw a `TypeError`. - 1. Else: - 1. Let |value| be the resource class associated with |variable|. + 1. Let |abstractTypeKey| be the *abstract type key* of |export|.TypeBound. + 1. Let |value| be |componentInstance|.[[GuestResourceClasses]][|abstractTypeKey|] 1. **func**: 1. Let |value| be `create a JS function for a component function` given |export|.Func, |key| and no tag. 1. **value**: @@ -641,74 +819,95 @@ To `create the exports object` given a |componentInstance|: 1. **component**: 1. Let |value| be a new `Component` whose [[Component]] is |export|.Component. 1. Perform `CreateDataPropertyOrThrow`(|exportsObject|, |key|, |value|). +1. Perform `SetIntegrityLevel`(|exportsObject|, "frozen"). 1. Return |exportsObject|. To `create a JS function for a component function` given |componentFunc|, |name| and |exportNameTag|: 1. Let |paramOffset| be 1 if |exportNameTag| is `[method].`, else 0. -1. Let |okType| be |componentFunc|.Result's `result` payload type if it is a `result`, else |componentFunc|.Result. +1. If |componentFunc|.Result is a `result`: + 1. Let |okType| be its `ok` payload type, or **empty** if it has none. +1. Else: + 1. Let |okType| be |componentFunc|.Result, or **empty** if |componentFunc| has no result. 1. Return a built-in function object with name |name| and length |componentFunc|.Params.length - |paramOffset|, whose behaviour, given a **this** value |thisValue| and JS arguments |args|, performs: 1. Let |componentResult| be ? `invoke a component function` given |componentFunc|, |exportNameTag|, |thisValue| and |args|. + 1. If |okType| is **empty**: + 1. Return **undefined**. 1. Return `ToJSValue`(|componentResult|, |okType|). A `[method]` export takes its **this** value as the component function's first parameter, which validation guarantees is the `borrow` self, mirroring how `read the function import` maps component argument 0 onto a JS receiver. A `[static]` export ignores its **this** value. To `invoke a component function` given |componentFunc|, |exportNameTag|, |thisValue| and a List of JS values |args|: -1. Let |paramTypes| be |componentFunc|.Params and |resultType| be |componentFunc|.Result. +1. Let |paramTypes| be |componentFunc|.Params. +1. Let |resultType| be |componentFunc|.Result. 1. Let |paramOffset| be 1 if |exportNameTag| is `[method].`, else 0. -1. If |resultType| is `result`: - 1. Let |okType| be T, |errorType| be E, and |throwing| be **true**. +1. If |resultType| is a `result`: + 1. Let |okType| be its `ok` payload type, or **empty** if it has none. + 1. Let |errorType| be its `error` payload type, or **empty** if it has none. + 1. Let |throwing| be **true**. 1. Else: - 1. Let |okType| be |resultType| and |throwing| be **false**. -1. Let |lenders| be a new empty List, and set the current lender list to |lenders|, saving the previous one. However this algorithm completes, decrement the [[LendCount]] of every instance in |lenders| and restore the previous lender list before returning. + 1. Let |okType| be |resultType|, or **empty** if |componentFunc| has no result. + 1. Let |throwing| be **false**. +1. Let |lenders| be a new empty List. +1. Let |previousLenders| be the current lender list. +1. Set the current lender list to |lenders|. +1. After the following returns either normally or abruptly: + 1. Decrement the [[LendCount]] of every instance in |lenders|. + 1. Set the current lender list to |previousLenders|. 1. Let |values| be a new empty List. -1. If |paramOffset| is 1, append ? `ToComponentValue`(|thisValue|, |paramTypes|[0]) to |values|. -1. If the number of |args| is less than |paramTypes|.length - |paramOffset|, throw a `TypeError`. -1. For each i in [0, |paramTypes|.length - |paramOffset|): - 1. Append ? `ToComponentValue`(|args|[i], |paramTypes|[i + |paramOffset|]) to |values|. -1. Arguments beyond that are ignored. +1. If |paramOffset| is 1: + 1. Append ? `ToComponentValue`(|thisValue|, |paramTypes|[0]) to |values|. +1. If the number of |args| is less than |paramTypes|.length - |paramOffset|: + 1. Throw a `TypeError`. +1. For each i in [0, |paramTypes|.length - |paramOffset|): append ? `ToComponentValue`(|args|[i], |paramTypes|[i + |paramOffset|]) to |values|. 1. Let |componentResult| be the result of invoking |componentFunc| with |values|. - 1. If the call traps, throw a `WebAssembly.RuntimeError`. +1. If the call traps: + 1. Throw a `WebAssembly.RuntimeError`. +1. If |throwing| is **true** and |componentResult| is `result.error(|e|)`: + 1. Throw `create a component error` for |e| and |errorType|. +1. If |okType| is **empty**: + 1. Return **empty**. 1. If |throwing| is **true**: - 1. If |componentResult| is `result.error(|e|)`: - 1. Throw `create a component error` for |e| and |errorType|. - 1. Set |componentResult| to the `result.ok` payload. -1. Return |componentResult|. - -The lender list covers the whole call, so JS cannot dispose a resource instance it lent to a component while the component still holds the `borrow`, even if the component calls back out to JS to try. + 1. Return the `result.ok` payload of |componentResult|. +1. Else: + 1. Return |componentResult|. -To `create a component error` for component value |e| and component type |errorType|: -1. Let |payload| be `ToJSValue`(|e|, |errorType|). -1. Return a new `ComponentError` whose `payload` is |payload| and an implementation defined `message`. +To `create a component error` for an optional component value |e| and component type |errorType|: +1. If |errorType| is **empty**: + 1. Let |payload| be **undefined**. +1. Else: + 1. Let |payload| be `ToJSValue`(|e|, |errorType|). +1. Return a new `ComponentError` whose `data` is |payload| and an implementation defined `message`. -## WebAssembly ESM-Integration +## WebAssembly ESM-integration -[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary to decide whether the bytes decode as a module or a component, so a component can be loaded anywhere a module can be today. +[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The module loader branches on the `layer` field of the binary to decide whether the bytes decode as a module or a component, so a component can be loaded anywhere a module can be today. -Each component import becomes a JS import for the module loader. Its [module specifier](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ModuleSpecifier) is the import's [`external-id`](Explainer.md#import-and-export-definitions) attribute if it has one, and its `externname` otherwise. A specifier is resolved (not looked up on an object) so it is not converted to a JS name. +Each component import becomes has a [module specifier](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ModuleSpecifier) given by `JSSpecifier`(|decl|). -Which binding of the resolved module the component gets depends on what the import's type is: +Which binding of the resolved module the component receives depends on the import's type: | Import type | JS equivalent | Value | |---|---|---| -| bare function, value | `import v from "spec"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | -| instance | `import { a, b } from "spec"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per export of the instance type, named `JSName` of that export | -| core module, component | `import source M from "spec"` | the module source, as a `Module` or `Component` | +| bare type, function, value | `import v from "Specifier(|decl|)"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | +| instance | `import { a, b } from "Specifier(|decl|)"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per untagged export of the instance type, named `JSName` of that export | +| core module, component | `import source M from "Specifier(|decl|)"` | the module source, as a `Module` or `Component` | + +Reading the imports snapshots the resolved values, and so components cannot participate in cycles. This matches how core modules work today with ESM-integration. Each resolved value is handed to [`read an import`](#read-the-imports-object) and the resulting definitions are passed to [`instantiate a component`](#instantiation). A component's exports become the bindings of its module namespace object. There is one binding per `JSName`(|export|), holding what [`create the exports object`](#create-the-exports-object) puts under that name, and no `default` binding. -Reading the imports snapshots the resolved values, and so components cannot participate in cycles. This matches how core modules work today with ESM-integration. - -TODO: figure out TLA and async start functions. - -## Open questions +## Follow ups -1. How to dynamically pass a union value? Static selection works. -1. How to import an overloaded function? +1. How to dynamically pass a union value? Statically passing a single case of the union works, but not dynamic choice. +1. How to import multiple overloads of a function? 1. How to support class inheritance and casting? Can a component defined resource sub-class an imported resource type? -1. How to support reference equality? `ToJSValue` creates a fresh resource instance per lift, so two `borrow`s of one component-defined resource are two JS objects that do not compare equal. Reps are opaque and reusable after a drop, so an identity map would need careful invalidation. +1. Do we support a reference equality protocol? `ToJSValue` creates a fresh resource instance per lift, so two `borrow`s of one component-defined resource are two JS objects that do not compare equal. Reps are opaque and reusable after a drop, so an identity map would need careful invalidation. +1. Do we let a JS constructor supply its own brand check? +1. Should a `ComponentInstance` expose whether it is [locked down](#lockdown-after-a-trap)? Today JS can only find out by calling in and catching a `WebAssembly.RuntimeError`. 1. How to import/export properties with getters/setters? -1. What happens if a component traps? Do we have lockdown semantics of some sort? -1. There is no `any` in the component model, so a component's only way to hold an opaque JS value is a resource type import with no brand check hook. Should we define builtin resource types for JS primitive types? -1. A `start` function can pass a resource value to a JS function import and then trap. Disposing the resource value would run a destructor in an uninstantiated component. +1. How does a component feature test an import? +1. How does a component pass one of its own functions to a JS callback, e.g. `add-event-listener`? +1. What is the precise timing of [Get][Set] during lifting/lowering if a wasm trap happens. +1. Top-level await, and async start functions. From c3378bb76bb94eb070a0df8a5c42a0864d70b5f2 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Tue, 15 Sep 2026 22:42:06 -0500 Subject: [PATCH 4/7] [js-api] Edits --- design/mvp/JS-Explainer.md | 300 ++++++++++++++++--------------------- design/mvp/JS-Reference.md | 286 ++++++++++++++++++----------------- 2 files changed, 276 insertions(+), 310 deletions(-) diff --git a/design/mvp/JS-Explainer.md b/design/mvp/JS-Explainer.md index 4ba4fbcf..31ef69db 100644 --- a/design/mvp/JS-Explainer.md +++ b/design/mvp/JS-Explainer.md @@ -4,78 +4,44 @@ This explainer describes how WebAssembly Components (hereafter 'components') can See the [reference](./JS-Reference.md) for an in-depth walkthrough. -**This is a draft and is not complete. Major details are unresolved. See "Status" at the end.** +**This is a draft and is not complete.** -## Goals - -1. Components can import and use most web and JS API's -2. Components can export an API useable by JS -3. Components interact with the web platform in similar ways to JS: - a. Components can feature test whether API's are present - b. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill - c. Components are tolerant of web API evolution - d. Components misuse of a web API's result in failure at that call-site, not link time errors -4. Components have improved performance when calling web API's compared to today - -## Non-goals - -1. Components importing every kind of web API -1. Components exporting any kind of JS API - -## Design - -To meet our goals, we need to define interactions (also known as 'bindings') between components, web API's, and JS. - -The scripting interface for web API's is handled (almost but not entirely) by WebIDL, so bindings for web API's effectively means bindings for WebIDL. WebIDL already has a "JavaScript Bindings" section which defines how JS interacts with WebIDL. There are no other bindings yet supported by WebIDL. - -There are roughly three paths forward here: - -A. Define bindings between components and JS - components transitively have access to web API's through the pre-existing JS-WebIDL bindings. -B. (A) and also define bindings between components and WebIDL - components get a separate direct path to web API's. -C. Define bindings between components and WebIDL - components transitively have access to JS through the pre-existing JS-WebIDL bindings. - -There are pros/cons to each. Let's go through them. - -### A. Define only bindings between Components and JS - -This is the smallest step from where we are today. A component's imports and exports are described in terms of JS values, and the web platform is reached the same way JS reaches it. - -Goals #1, #2 and #3 mostly fall out for free. Web API's are already exposed to JS, so importing one is just importing the JS function that reflects it, and exporting to JS is given by the binding. Feature testing, polyfilling and API evolution are all properties the WebIDL-JS binding already supports, so they keep working without us specifying anything new. - -The objection to A has always been goal #4. If a call into a web API is defined as a call through JS, JS semantics are observable at every step. Lookups on the global object and on prototypes can be intercepted, argument coercion can run user code through `valueOf`, `toString` and iterators, and the callee may be a Proxy. An engine can try to speculate these away, but that is not always easy. - -### B. Define bindings between Components and JS and also Components and WebIDL - -This is a superset of option A, so it inherits the pros/cons of that. - -In addition, we add a parallel binding between components and WebIDL to get goal #4 as well. Components that only need to talk to JS use the JS binding, and components that use web API's use the WebIDL binding. - -The cost is that we write and maintain two bindings, and they have to harmonize. - -### C. Define only bindings between Components and WebIDL - -JS already has well-defined bindings to WebIDL. If we define bindings from components to WebIDL, we get direct and efficient access to web API's (goal #4) and transitively get access to JS (goals #1 and #2). - -Like A we only have one specification to draft and maintain. +## Walkthrough -The cost is goal #3. Feature testing, polyfills and API evolution are all things A inherits and C has to reinvent, and that is new conceptual ground. +### Values at a glance -### Conclusion +Components and JS maintain separate type/value systems, so any value crossing the boundary needs a defined translation in both directions. -We should take option A. Its one disadvantage against C was goal #4, and we believe that we can work around that by carefully writing value conversion rules so that engines can fuse conversion from component values to WebIDL without any speculation. +| Component type | JS | +|---|---| +| `bool` | Boolean | +| `s8`-`s32`, `u8`-`u32` | Number, an exact integer | +| `s64`, `u64` | BigInt | +| `f32`, `f64` | Number, including NaN and infinities | +| `char` | String of exactly one Unicode scalar value | +| `string` | String, well formed | +| `list` | `Uint8Array` | +| `list`, `list`, `tuple` | Array | +| `record { a-b: T }` | null-prototype object, `{ aB }` | +| `flags "a" "b"` | null-prototype object of Booleans, `{ a, b }` | +| `enum "a" "b"` | String, the label verbatim | +| `option` | `null`, or the payload | +| `variant`, `option>` | `{ kind, value }` | +| `result` | thrown and caught in return position, else `{ kind, value }` | +| `map` | `Map` | +| `own`, `borrow` | the original JS value for an imported resource type, an instance of its class for an exported one | +| `future`, `stream`, `error-context` | not yet specified | -## Walkthrough +See [ToJSValue](./JS-Reference.md#tojsvalue) and [ToComponentValue](./JS-Reference.md#tocomponentvalue) for detailed algorithms. ### A greeter -Start with a component that imports nothing: +Let's start with a component that imports nothing: -```wit -package example:greeter; - -world greeter { - export greet: func(name: string) -> string; -} +```wat +(component + (export "greet" (func (param "who" string) (result string))) +) ``` ```js @@ -84,9 +50,9 @@ const { instance } = await WebAssembly.instantiate(bytes); instance.exports.greet("world"); // "hello, world" ``` -`exports` holds one property per export and `greet` is an ordinary function. Component names are kebab-case and JS names are camelCase, so an export named `greet-loudly` would be `greetLoudly`. +`exports` holds one property per export and `greet` is an ordinary function. Component names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. -Arguments are converted rather than type checked, the way a WebIDL operation converts its own: +Arguments are coerced to their expected type, and if that fails a `TypeError` is thrown: ```js instance.exports.greet(42); // "hello, 42" @@ -99,22 +65,22 @@ Passing too few arguments is a `TypeError`. Extra arguments are ignored. Now a component that imports: -```wit -package example:logger; - -world logger { - import log: func(message: string); - export run: func(); -} +```wat +(component + (import "log" (func (param "message" string))) + (export "run" (func)) +) ``` +The import `log` must be a JS callable object, and will be called with a JS String. We can provide the `console.log` builtin here: + ```js const { instance } = await WebAssembly.instantiate(bytes, { log: console.log }); instance.exports.run(); // logs "hello" ``` -The component's `message` becomes a String and we call `log` with it. Nothing inspects what `log` is, so any callable does, and a polyfill is as good as the real thing: +Or provide a custom implementation: ```js const lines = []; @@ -123,25 +89,46 @@ const log = (message) => { lines.push(message); }; const { instance } = await WebAssembly.instantiate(bytes, { log }); ``` -Which means feature testing is just JS, done before instantiating: +### Loading with ESM + +[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary, so a component loads anywhere a core module does today. -```js -const log = globalThis.console?.log ?? myPolyfill; +Each component import becomes a JS import, and its module specifier is the import's [`external-id`](Explainer.md#import-and-export-definitions) if it has one and its name otherwise: + +```wat +(component + (import "slugify" + (external-id "https://esm.unpkg.com/slugify@1.6.6") + (func (param "text" string) (result string)) + ) + (export "run" (func)) +) +``` + +```html + ``` ### When a call fails -A `result` return is not handed to JS as a value. On the way out it throws, and on the way in a thrown value is caught: +Component functions signal failure using a `result` value: + 1. Exported component functions that return an error `result` throw JS exceptions. + 1. Imported JS functions that throw JS exceptions are captured as a `result`. -```wit -package example:parse; +An imported JS function that throws where the component asked for a plain return type results in a trap. -world parser { - import lookup: func(key: string) -> result; - export parse: func(text: string) -> result; -} +```wat +(component + (import "lookup" (func (param "key" string) (result string (error string)))) + (export "parse" (func (param "text" string) (result u32 (error string)))) +) ``` +`parse` tries to parse its `text` argument as an integer, and if that fails performs a fallible lookup. + ```js const { instance } = await WebAssembly.instantiate(bytes, { lookup: (key) => { throw `no such key: ${key}`; }, @@ -153,30 +140,40 @@ try { instance.exports.parse("$name"); } catch (e) { e instanceof WebAssembly.ComponentError; // true - e.payload; // "no such key: name" + e.data; // "no such key: $name" } ``` -`payload` is the `E` value converted to JS. In the other direction the thrown JS value is converted to `E`, so `lookup` returns `result.error("no such key: name")` and the component is free to handle it instead of propagating it. - -An import that throws where the component asked for a plain return type has nowhere to put the error, and traps. +In the second call, parsing fails and leads to a call to `lookup` which throws a JS exception. This is converted to a `result` and consumed by the component. The component then propagates it to the original JS caller as a thrown `ComponentError` carrying the original message. ### Importing a resource -Components see JS objects as resources. A resource type import and the functions on it are satisfied by a single JS value, the constructor: +Components see JS values as resources. A resource type import is satisfied by passing a constructor function. + +Whenever a JS value must be converted to a resource type, an `instanceof` check is performed against the imported constructor. If the constructor is actually a [WebIDL interface object](https://webidl.spec.whatwg.org/#interface-object) or an [exported component resource constructor](#exporting-a-resource), a precise [brand check](./JS-Reference.md#brand-checks) is performed. + +Any imported function whose name is tagged `[constructor]`, `[method]`, or `[static]` is looked up on the imported constructor instead of the imports object: + 1. `[constructor]R` - `R` + 1. `[method]R.M` - `R.prototype.M` + 1. `[static]R.S` - `R.S` + +The above allows most JS classes to be imported as a resource by just passing the constructor function: ```wat (component - (import "element" (type $element (sub resource))) - (import "[method]element.query-selector" (func - (param "self" (borrow $element)) (param "selectors" string) - (result (option (own $element))))) - (import "[method]element.get-attribute" (func - (param "self" (borrow $element)) (param "name" string) - (result (option string)))) - (export "find" (func - (param "root" (borrow $element)) (param "selectors" string) - (result (option string)))) + (import "element" + (type $element (sub resource)) + ) + (import + "[method]element.query-selector" + (func (param "self" (borrow $element)) (param "selectors" string) (result (option (own $element)))) + ) + (import "[method]element.get-attribute" + (func (param "self" (borrow $element)) (param "name" string) (result (option string))) + ) + (export "find" + (func (param "root" (borrow $element)) (param "selectors" string) (result (option string))) + ) ) ``` @@ -186,24 +183,24 @@ const { instance } = await WebAssembly.instantiate(bytes, { element: Element }); instance.exports.find(document.body, "h1"); // "page-title" or null ``` -`Element` covers the type and both methods. The type import brand checks against `Element`, which for a WebIDL interface object means the same `implements` check JS gets, and the methods are read off `Element.prototype` under their camelCase names, which is where JS finds them too. - -Because `find` takes a `borrow` of the *imported* type, JS keeps passing raw elements. Passing anything else fails the same brand check and is a `TypeError`, and `option` comes back as `null`. +### Importing from the JS global -### Importing a web API - -The example above still needs someone to write `{ element: Element }`. A component can skip that and take its imports straight from the global scope by importing `wasm:js/global`: +The example above still needs someone to write `{ element: Element }`. A component can skip that and take its imports straight from the global object by importing `wasm:js/global`: ```wat (component - (import "wasm:js/global" (instance $g - (export "element" (type $element (sub resource))) - (export "[method]element.get-attribute" (func - (param "self" (borrow $element)) (param "name" string) - (result (option string)))) - (export "btoa" (func (param "data" string) (result string))) - )) + (import "wasm:js/global" + (instance $g + (export "btoa" (func (param "data" string) (result string))) + + (export "element" (type $element (sub resource))) + (export "[method]element.get-attribute" (func + (param "self" (borrow $element)) (param "name" string) + (result (option string)))) + ) + ) (alias export $g "element" (type $el)) + (export "encode-id" (func (param "el" (borrow $el)) (result (option string)))) ) ``` @@ -215,90 +212,43 @@ const { exports } = new WebAssembly.ComponentInstance(c); exports.encodeId(document.body); ``` -Every field of the instance is read off the global under its JS name, so `element` finds `Element`, `[method]element.get-attribute` finds `Element.prototype.getAttribute`, and `btoa` finds the global function. That is all `wasm:js/global` does. The web API bindings come from the same rules as any other JS import, which is why the JS-API needs no per-API knowledge and why goals #3b and #3c keep holding: the component sees whatever the page sees, polyfills included, and an API that grows a method needs no new binding. Goal #3a is the one that does not follow, because a missing name is a link error and a component has nothing to feature test with. +Importing from `wasm:js/global` is equivalent to an imports object with: `{ "wasm:js/global": globalThis }`. The normal rules for reading from the imports object still apply. -The lookups happen once, when the imports are read, so this costs nothing per call. - -Names that are not constructors work too. A singleton like `document` is a value import of `own`, which needs the component model's value imports feature, and `console`, which has no constructor to brand check against, is a nested instance import that reads `log` off the `console` object. - -Importing `wasm:js/global` grants the component everything the page can do, which is why it is opt-in through the same `builtins` compile option core modules use. With ESM the lever is the import map, which can point `wasm:js/global` at a JS module instead: +ESM-integration defaults to enabling `wasm:js/global` which allows a component to import and use web APIs without any glue code: ```html ``` -What is missing is described in [the reference](./JS-Reference.md#what-the-global-object-cannot-express-yet). The short version: no properties, so `element.textContent` is not expressible; no way to hand a component function to `addEventListener`; and no way to feature test an API before importing it. - ### Exporting a resource -A resource a component defines and exports becomes a class: +A resource type exported from a component becomes a JS class: -```wit -package example:counter; - -world w { - export api: interface { - resource counter { - constructor(); - increment: func() -> u32; - } - } -} +```wat +(component + (export "counter" (type $counter (sub resource))) + (export "[constructor]counter" (func (result (own $counter)))) + (export "[method]counter.increment" (func + (param "self" (borrow $counter)) + (result u32)) + ) +) ``` ```js const { instance } = await WebAssembly.instantiate(bytes); -const { Counter } = instance.exports.api; +const { Counter } = instance.exports; -using c = new Counter(); +let c = new Counter(); c.increment(); // 1 c.increment(); // 2 ``` -Type names are PascalCase, so `counter` is `Counter`. `new` runs the component's `constructor`, methods live on `Counter.prototype`, and `Symbol.dispose` drops the handle. Dropping is what runs the component's destructor, so a `Counter` nobody disposes is dropped when it is collected, through a `FinalizationRegistry`. - -### Loading with ESM - -[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary, so a component loads anywhere a module does today. - -Each component import becomes a JS import, and its module specifier is the import's [`external-id`](Explainer.md#import-and-export-definitions) if it has one and its name otherwise: - -```wit -world my-component { - @external-id("https://esm.unpkg.com/slugify@1.6.6") - import slugify: func(text: string) -> string; -} -``` - -## Values at a glance - -| Component type | JS | -|---|---| -| `bool` | Boolean | -| `s8`-`s32`, `u8`-`u32` | Number, an exact integer | -| `s64`, `u64` | BigInt | -| `f32`, `f64` | Number, including NaN and infinities | -| `char` | String of exactly one Unicode scalar value | -| `string` | String, well formed | -| `list` | `Uint8Array` | -| `list`, `list`, `tuple` | Array | -| `record { a-b: T }` | null-prototype object, `{ aB }` | -| `flags "a" "b"` | null-prototype object of Booleans, `{ a, b }` | -| `enum "a" "b"` | String, the label verbatim | -| `option` | `null`, or the payload | -| `variant`, `option>` | `{ kind, value }` | -| `result` | thrown and caught in return position, else `{ kind, value }` | -| `map` | `Map` | -| `own`, `borrow` | the value the type import was given, or an instance of its class | -| `future`, `stream`, `error-context` | not yet specified | - -Conversions in are looser than conversions out, in the same places WebIDL's are. A `record` takes any object with the right own properties, a `list` takes an Array or any iterable, and a `map` takes a `Map`, an iterable of pairs, or a plain object when `K` is `string`. See [ToJSValue](./JS-Reference.md#tojsvalue) and [ToComponentValue](./JS-Reference.md#tocomponentvalue). - ## Status -- `future`, `stream` and `error-context` have no binding yet, and neither do async start functions or top-level await. +- `async` functions, `future`, `stream` and `error-context` have no binding yet. -Everything else we know is open is collected in the reference's [follow ups](./JS-Reference.md#follow-ups). +Everything else we know to be open is collected in the reference's [follow ups](./JS-Reference.md#follow-ups). diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md index af157021..4a28cdde 100644 --- a/design/mvp/JS-Reference.md +++ b/design/mvp/JS-Reference.md @@ -1,22 +1,40 @@ # WebAssembly Components JS-API Reference -This is the in-depth reference for the WebAssembly Component JS-API. See here for the higher-level [explainer](./JS-Explainer.md). +This is the in-depth reference for the WebAssembly Component JS-API. See the [explainer](./JS-Explainer.md) for a higher-level introduction. **This is a draft and is not complete. Major details are unresolved, and there are bugs.** +## Goals + +1. Components can import and use most web and JS APIs +2. Components can export an API usable by JS +3. Components interact with the web platform in similar ways to JS: + a. Components can feature test whether APIs are present + b. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill + c. Components are tolerant of web API evolution + d. Component misuse of a web API results in failure at that call-site, not a link time error +4. Components have improved performance when calling web APIs compared to today + +## Non-goals + +1. Components importing every kind of web API +1. Components exporting any kind of JS API + ## The WebAssembly namespace We extend the imperative WebAssembly JS-API interfaces to also allow validation, compilation, and instantiation of components in addition to modules. ```webidl +[LegacyNamespace=WebAssembly, Exposed=*] interface Component { constructor([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); -} +}; +[LegacyNamespace=WebAssembly, Exposed=*] interface ComponentInstance { constructor(Component component, optional object importsObject); readonly attribute object exports; -} +}; typedef (Component or Module) InstantiateSource; @@ -37,13 +55,13 @@ namespace WebAssembly { // ComponentInstance for a Component. Promise<(Instance or ComponentInstance)> instantiate( InstantiateSource moduleObject, optional object importObject); -} +}; ``` We also add an error type for component functions that return `result<_, E>` to JS: ```webidl -[Exposed=*] +[LegacyNamespace=WebAssembly, Exposed=*] interface ComponentError : Error { constructor(optional DOMString message = "", optional any data); readonly attribute any data; @@ -58,19 +76,27 @@ Validation and compilation of components defer to the underlying component embed ## Entry points -A `Component` has a [[Component]] internal slot holding a compiled component. A `ComponentInstance` has [[ComponentInstance]], [[Exports]], [[HostResourceTypes]], and [[GuestResourceClasses]] slots. +A `Component` has the following slots: + 1. [[Component]] - the compiled component. + 1. [[EnabledBuiltins]] - the enabled builtins. + +A `ComponentInstance` has the following slots: + 1. [[ComponentInstance]] - the component instance. + 1. [[Exports]] - the [exports object](#create-the-exports-object). + 1. [[HostResourceTypes]] - map from [abstract type key](#abstract-and-transparent-types) to [host resource type](#host-resource-types-and-values). + 1. [[GuestResourceClasses]] - map from [abstract type key](#abstract-and-transparent-types) to [guest resource class](#guest-resource-classes). To `construct a Component` given |bytes| and |options|: 1. Let |stableBytes| be a copy of the bytes held by |bytes|. -1. If |options| has any non-default values: - 1. Throw `TypeError`. +1. Let |enabledBuiltins| be the result of parsing [`builtins`](#builtin-imports) from |options|. 1. Let |component| be the result of compiling |stableBytes| as a component, per the embedding interface. 1. If compilation fails: 1. Throw a `WebAssembly.CompileError`. 1. Set **this**.[[Component]] to |component|. +1. Set **this**.[[EnabledBuiltins]] to |enabledBuiltins|. -To `construct a ComponentInstance` given a `Component` |component|, |importsObject|, and |compileOptions|: -1. Let |enabledBuiltins| be the result of parsing [`builtins`](#builtin-imports) from |compileOptions|. +To `construct a ComponentInstance` given a `Component` |component|, and |importsObject|: +1. Let |enabledBuiltins| be |component|.[[EnabledBuiltins]]. 1. Let |result| be ? [`instantiate a component from an imports object`](#instantiation) given |component|.[[Component]], |importsObject|, |enabledBuiltins|. 1. Set **this**.[[ComponentInstance]] to |result|.[[ComponentInstance]]. 1. Set **this**.[[Exports]] to |result|.[[Exports]]. @@ -79,16 +105,16 @@ To `construct a ComponentInstance` given a `Component` |component|, |importsObje The `exports` getter returns **this**.[[Exports]]. -`validate` is modified to validate the bytes as a component if the version flag is set to be a component. -`compile`/`instantiate` is modified to asynchronously compile/instantiate the bytes as a component if the version flag is set to be a component. +`validate` is modified to validate the bytes as a component if the `layer` field of the binary indicates a component. +`compile`/`instantiate` are modified to asynchronously compile/instantiate the bytes as a component if the `layer` field indicates a component. ## Names Component import/export `plainname`s contain [`label`s](Explainer.md#import-and-export-definitions) that must be transformed into an identifier for use with JS. -Component import/export `interfacename`s (such as `wasi:http/handler@1.0.0`) are used as-is when converted to JS strings. +Component import/export `interfacename`s (such as `wasi:http/handler@1.0.0`) are used as-is when converted to JS strings or as [module specifiers](#webassembly-esm-integration). -We define `PascalCase(label)` and `CamelCase(label)` below; both are used throughout this spec. +We define `PascalCase(label)` and `CamelCase(label)` below. | `label` | `PascalCase` | `CamelCase` | |---|---|---| @@ -110,7 +136,7 @@ We define `PascalCase(label)` and `CamelCase(label)` below; both are used throug 1. Return the List of Strings produced by splitting |label| on occurrences of U+002D (-). The hyphens themselves are discarded. `Capitalize`(|fragment|): -1. If |fragment| is an `acronym`: +1. If |fragment| is an [`acronym`](Explainer.md#import-and-export-definitions): 1. Return |fragment|. 1. Return |fragment| with its first character uppercased. @@ -137,12 +163,12 @@ The JS name of an import or export declaration is then: 1. Return `PascalCase`(`LabelOf`(|decl|.Name)). 1. Return `CamelCase`(`LabelOf`(|decl|.Name)). -An import's *specifier* is its [`external-id`](Explainer.md#import-and-export-definitions) attribute if it has one, and its JS name otherwise: +An import's specifier is its [`external-id`](Explainer.md#import-and-export-definitions) attribute if it has one, and its JS name otherwise: `JSSpecifier`(|decl|): 1. If |decl| has an `external-id` attribute: 1. Return that attribute's name. -1. Return `JSName(|decl|)`. +1. Return `JSName`(|decl|). ## Types and values @@ -153,14 +179,14 @@ This section specifies that translation as two abstract operations: 1. `ToComponentValue` - convert a JS value to a component value of a given type. Roundtripping from `ToJSValue` back through `ToComponentValue` is designed to be strictly the identity function with the following exceptions: - 1. a `map` with duplicate keys (see [`ToJSValueMap`](#tojsvalue)) - 2. Float NaNs are [canonicalized](CanonicalABI.md#loading) + 1. A `map` with duplicate keys keeps only the last pair per key (see [`ToJSValueMap`](#tojsvalue)) + 1. Float NaNs are [canonicalized](CanonicalABI.md#loading) Every object a conversion creates belongs to the *conversion realm*: the realm of the `WebAssembly` namespace the component was instantiated through. ### ToJSValue -`ToJSValue(componentValue, componentValType)` converts a component value to a JS value. It is infallible +`ToJSValue(componentValue, componentValType)` converts a component value to a JS value. It is infallible. Dispatch on `componentValType`: @@ -265,12 +291,12 @@ Dispatch on `targetComponentType`: 1. Return |n|. `ToComponentValueFloat(jsValue, t)`: -1. let |num| = ? `ToNumber`(|jsValue|). +1. Let |num| be ? `ToNumber`(|jsValue|). 1. If |t| is `f32`: - 1. |num| = |num| rounded to the nearest f32 value (ties to even). + 1. Set |num| to |num| rounded to the nearest f32 value (ties to even). 1. Return |num|. - `NaN` and infinities are accepted (matching WebIDL `unrestricted float`/`unrestricted double`). +`NaN` and infinities are accepted (matching WebIDL `unrestricted float`/`unrestricted double`). `ToComponentValueBytes(jsValue)`: 1. If |jsValue| has a [[TypedArrayName]] internal slot whose value is "Uint8Array": @@ -288,11 +314,11 @@ A `Uint8Array` is copied directly, since that is what `ToJSValue` produces. Anyt 1. If |method| is **undefined**: 1. Throw a `TypeError`. 1. Let |iteratorRecord| be ? `GetIteratorFromMethod`(|jsValue|, |method|). -1. Initialize |list| be an empty component list of type `T`. -1. Repeat +1. Let |list| be an empty component list of type `T`. +1. Repeat: 1. Let |next| be ? `IteratorStepValue`(|iteratorRecord|). - 1. If |next| is done, then return |list|. - 1. Set |list| to |list| with ? `ToComponentValue`(|next|, |T|) appended to the end. + 1. If |next| is **done**, return |list|. + 1. Set |list| to |list| with ? `ToComponentValue`(|next|, `T`) appended to the end. `ToComponentValueRecord(jsValue, fields)`: 1. If |jsValue| is not an Object: @@ -302,7 +328,7 @@ A `Uint8Array` is copied directly, since that is what `ToJSValue` produces. Anyt 1. Let |m| be ? `Get`(|jsValue|, `CamelCase`(f)). 1. If |m| is **undefined** and `T` is not `option<_>`: 1. Throw a `TypeError`. - 1. Set |record|'s `f` field to `ToComponentValue`(|m|, T). + 1. Set |record|'s `f` field to ? `ToComponentValue`(|m|, T). 1. Return |record|. `ToComponentValueFlags(jsValue, labels)`: @@ -319,12 +345,13 @@ An absent property is therefore **false**, matching a `boolean` dictionary membe 1. If |jsValue| is not an Object: 1. Throw a `TypeError`. 1. Let |kind| be ? `ToString`(? `Get`(|jsValue|, "kind")). -1. If |kind| is not the label of one of |cases|: +1. If there is no case of |cases| whose label `L` has `PascalCase`(`L`) equal to |kind|: 1. Throw a `TypeError`. -1. Let |case| be the case of |cases| whose label is |kind|. +1. Let |case| be that case. 1. If |case| has a payload type T: - 1. Set |case|'s payload to `ToComponentValue`(? `Get`(|jsValue|, "value"), T). -1. Return |case|. + 1. Let |payload| be ? `ToComponentValue`(? `Get`(|jsValue|, "value"), T). + 1. Return a variant value of |case| whose payload is |payload|. +1. Return a variant value of |case| with no payload. `ToComponentValueMap(jsValue, K, V)`: 1. If |jsValue| is not an Object: @@ -335,7 +362,7 @@ An absent property is therefore **false**, matching a `boolean` dictionary membe 1. Throw a `TypeError`. 1. Return one pair per own enumerable string-keyed property of |jsValue|, in property order, reading each value with ? `Get` and converting it with `ToComponentValue`(_, V). -If an `Iterable` is not provided, we fall back to converting the object the way WebIDL's `record` would, for compatibility. +If the value is not iterable, we fall back to converting the object the way WebIDL's `record` would, for compatibility. ## Resource types @@ -349,7 +376,7 @@ The component JS-API defines: ### Embedder extensions -We sketch three things here that will be formalized more fully in the [embedding interface](CanonicalABI.md#embedding). +We sketch two operations here that will be formalized more fully in the [embedding interface](CanonicalABI.md#embedding). To `create a resource type for host` given a host function |destructor|: 1. Return a fresh component resource type whose representation is host-defined and whose destructor is |destructor|. @@ -379,9 +406,9 @@ Imported and exported resource types are either abstract or transparently equiva `r1`, `r3`, `r4`, `r6` are the abstract types of this component type, while `r2`, `r5`, and `r7` are transparently equal to one of the abstract types. -Host/guest resource types below are created only for abstract types, and stored in maps on the component instance. The map is keyed by an *abstract type key* which is the import/export declaration for the abstract type. +Host/guest resource types below are created only for abstract types, and stored in maps on the component instance. The map is keyed by an *abstract type key*, which is an import/export declaration for an abstract type. -An *abstract type key* can be found for any import/export declaration by following `eq R` until you reach a `sub resource`. +An *abstract type key* can be found for any import/export type declaration by following `(eq R)` until you reach a `(sub resource)`. ### Host resource types (i.e. imported) @@ -398,7 +425,7 @@ To `brand check` given a JS value |jsValue| and an Object |constructor|: Which case applies is fixed for the lifetime of |constructor|. -The first two cases are real brand checks. The `instanceof` fallback only inspects the prototype chain, so a value that was never created by |constructor| can pass it. In the future we may specify a way for JS to specify custom brand checks. +The first two cases are real brand checks. The `instanceof` fallback only inspects the prototype chain, so a value that was never created by |constructor| can pass it. In the future we may add a way for JS to supply a custom brand check. #### Host resource types and values @@ -418,9 +445,9 @@ A *host resource value* is the `rep` of a host resource type. It too is a Record A host resource value just holds a strong reference to the underlying value. No user-level destructors are run when it is dropped. -One host resource type is created per [abstract type](#abstract-and-transparent-types). Two abstract type imports satisfied by the same JS constructor become distinct component resource types, and a handle for one cannot be passed where the other is expected. +One host resource type is created per imported [abstract type](#abstract-and-transparent-types). Two abstract type imports satisfied by the same JS constructor become distinct component resource types, and a handle for one cannot be passed where the other is expected. -A map from resource type import declaration to host resource type is built by [`read the imports object`](#read-the-imports-object) and stored on a component instance. +A map from imported abstract type to host resource type is built by [`read the imports`](#read-the-imports-object) and stored on a [component instance](#entry-points). #### Conversions for host resources @@ -441,41 +468,24 @@ For a resource type `R` whose abstract type is one of the component's type impor 1. Throw a `TypeError`. 1. Return a host resource value whose [[Type]] is |hostType| and whose [[JSValue]] is |jsValue|. -Converting the same JS value to a host resource type yields fresh handle indices. There is no canonicalization based on reference equality. +Converting the same JS value to a host resource type yields fresh handle indices. There is no canonicalization of indices. ### Guest resource types (i.e. exported) #### Re-exported host resource types -An exported resource type may be transparently equal to an imported resource type (see [abstract types](#abstract-and-transparent-types)). This currently throws, but may be relaxed in the future. +A component's type may export a resource type that is transparently equal to one of its imported resource types (see [abstract types](#abstract-and-transparent-types)). This currently [throws](#create-the-exports-object), but may be relaxed in the future. -An exported resource type that is privately a re-export of an imported type will wrap the original host resource type in a new [guest resource class](#guest-resource-classes). This keeps callers from observing whether an exported resource type is a re-export or defined in the component. +An exported resource type that is only privately a re-export of an imported type, i.e. the component's type declares it as a fresh abstract export, will wrap the original host resource type in a new [guest resource class](#guest-resource-classes). This keeps callers from observing whether an exported resource type is a re-export or defined in the component. #### Guest resource classes -A unique JS *guest resource class* is created for each exported [abstract type](#abstract-and-transparent-types). A map from exported resource type declaration to guest resource class is stored on the component instance. - -A guest resource class is a built-in function object with extra internal slots: - -| Slot | Value | -|---|---| -| [[ResourceType]] | the guest resource type | -| [[ConstructorFunc]] | the component function that implements `new`, or **empty** | -| [[ComponentInstance]] | the component instance the class belongs to | +A unique JS *guest resource class* is created for each exported [abstract type](#abstract-and-transparent-types). A map from [abstract type key](#abstract-and-transparent-types) to guest resource class is stored on the component instance. -To `create guest resource classes` given a component instance |componentInstance|: -1. Let |guestResourceClasses| be an empty map from export declaration to guest resource class. -1. Let |component| be |componentInstance|.[[Component]]. -1. For each type export |export| of |component|'s type, in declaration order, recursing into exported instances: - 1. Let |abstractTypeKey| be the *abstract type key* of |export|.Type. - 1. If |abstractTypeKey| is one of |component|'s type imports: - 1. Throw a `TypeError`. - 1. If |guestResourceClasses|[|abstractTypeKey|] exists: - 1. Continue. - 1. Let |arity| be the parameter count of the `[constructor]` export targeting |variable|, or 0 if there is none. - 1. Let |class| be `create a guest resource class` given |componentInstance|, |export|, `JSName`(|export|) and |arity|. - 1. Set |guestResourceClasses|[|abstractTypeKey|] to |class|. -1. Set |componentInstance|.[[GuestResourceClasses]] to |guestResourceClasses|. +A guest resource class is a built-in function object with the following slots: +1. [[ResourceType]] - the guest resource type. +1. [[ConstructorFunc]] - the component function that implements `new`, or **empty**. +1. [[ComponentInstance]] - the component instance the class belongs to. To `create a guest resource class` given a component instance |componentInstance|, resource type |resourceType|, String |name| and an integer |arity|: 1. Let |prototype| be `OrdinaryObjectCreate`(`%Object.prototype%`). @@ -488,8 +498,7 @@ To `create a guest resource class` given a component instance |componentInstance 1. If |constructor|.[[ConstructorFunc]] is **empty**: 1. Throw a `TypeError`. 1. Let |rep| be ? `invoke a component function` given |constructor|.[[ConstructorFunc]], `[constructor]`, **undefined** and |args|. - 1. Let |ownType| be |constructor|.[[ConstructorFunc]]'s result, or its `ok` payload if that result is a `result`. - 1. Return `create a guest resource instance` given |constructor|, |rep|, **true** and |newTarget|. + 1. Return ? `create a guest resource instance` given |constructor|, |rep|, **true** and |newTarget|. 1. Perform `DefinePropertyOrThrow`(|prototype|, `%Symbol.dispose%`, PropertyDescriptor { [[Value]]: a built-in function that performs `drop a guest resource instance` given its **this** value, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Perform `DefinePropertyOrThrow`(|prototype|, `%Symbol.toStringTag%`, PropertyDescriptor { [[Value]]: |name|, [[Writable]]: **false**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Perform `DefinePropertyOrThrow`(|prototype|, "constructor", PropertyDescriptor { [[Value]]: |constructor|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). @@ -500,25 +509,26 @@ To `create a guest resource class` given a component instance |componentInstance 1. For each `[method]` export |m| of |tagged|: 1. If `JSName`(|m|) is "constructor": 1. Throw a `TypeError`. - 1. Perform `DefinePropertyOrThrow`(|constructor|'s `"prototype"`, `JSName`(|m|), PropertyDescriptor { [[Value]]: `create a JS function for a component function` given |m|.Func, `JSName`(|m|) and |m|'s tag, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). + 1. Let |func| be `create a JS function for a component function` given |m|.Func, `JSName`(|m|) and |m|'s tag. + 1. Perform `DefinePropertyOrThrow`(|constructor|'s `"prototype"`, `JSName`(|m|), PropertyDescriptor { [[Value]]: |func|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. For each `[static]` export |s| of |tagged|: 1. If `JSName`(|s|) is "prototype": 1. Throw a `TypeError`. - 1. Perform `DefinePropertyOrThrow`(|constructor|, `JSName`(|s|), PropertyDescriptor { [[Value]]: `create a JS function for a component function` given |s|.Func, `JSName`(|s|) and |s|'s tag, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). + 1. Let |func| be `create a JS function for a component function` given |s|.Func, `JSName`(|s|) and |s|'s tag. + 1. Perform `DefinePropertyOrThrow`(|constructor|, `JSName`(|s|), PropertyDescriptor { [[Value]]: |func|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Return |constructor|. A method named `constructor` and a static named `prototype` are rejected because they would unexpectedly change JS class semantics. #### Guest resource instances -An instance of a guest resource class holds the same state a handle table entry does, plus the class it belongs to: +An instance of a guest resource class has the following slots: +1. [[ResourceClass]] - the resource class this is an instance of. +1. [[Rep]] - the rep, or **empty** once the handle has been dropped. +1. [[Own]] - whether this instance owns the resource. +1. [[LendCount]] - how many outstanding `borrow`s were lent from this instance. -| Slot | Value | -|---|---| -| [[ResourceClass]] | the resource class this is an instance of | -| [[Rep]] | the rep, or **empty** once the handle has been dropped | -| [[Own]] | whether this instance owns the resource | -| [[LendCount]] | how many outstanding `borrow`s were lent from this instance | +It holds the same state a handle table entry does, plus the class it belongs to. To `create a guest resource instance` given a resource class |class|, |rep|, |own| and an optional |newTarget|: 1. Let |defaultProto| be the value of |class|'s `"prototype"` property. @@ -539,7 +549,7 @@ To `create a guest resource instance` given a resource class |class|, |rep|, |ow #### Conversions for guest resources -The *current lender list* is a per-call spec state. `invoke a component function` establishes it for a JS-to-component call. During the JS-component call, every instance has its [[LendCount]] incremented to protect against being dropped while lent. After the call, the [[LendCount]] is decremented. +The *current lender list* is a per-call spec state. `invoke a component function` establishes it for a JS-to-component call. Each instance lowered as a `borrow` during that call has its [[LendCount]] incremented and is appended to the list, which protects it from being dropped while lent. When the call returns, every [[LendCount]] in the list is decremented. For a resource type `R` whose [abstract type](#abstract-and-transparent-types) is one of the component's type exports: @@ -547,7 +557,7 @@ For a resource type `R` whose [abstract type](#abstract-and-transparent-types) i 1. Let |instance| be the surrounding component instance. 1. Let |abstractTypeKey| be the *abstract type key* of |R|. 1. Let |class| be |instance|.[[GuestResourceClasses]][|abstractTypeKey|]. - 1. Return `create a guest resource instance` given |class|, `R`, |rep| and **true**. + 1. Return `create a guest resource instance` given |class|, |rep| and **true**. - `ToJSValue(rep, borrow)`: 1. Assert: unreachable. 1. This can only happen if an exported function returns a borrow, which is not allowed. @@ -578,7 +588,9 @@ For a resource type `R` whose [abstract type](#abstract-and-transparent-types) i #### Guest resource FinalizationRegistry -There is an un-exposed `FinalizationRegistry` created per-Realm of the WebAssembly namespace object. The callback for it invokes `drop a guest resource instance` with the held value. +There is an unexposed "guest resource `FinalizationRegistry`" created per-Realm of the WebAssembly namespace object. The callback for it invokes `drop a guest resource instance` with the held value. + +TODO: the held value cannot be the instance itself, as registering an object with itself as the held value keeps it alive forever. The [[Rep]], [[Own]] and [[LendCount]] state needs to move into a separate record that the instance references and the registry holds. To `drop a guest resource instance` given |resourceInstance|: 1. If |resourceInstance|.[[Rep]] is **empty** or |resourceInstance|.[[Own]] is **false**: @@ -592,6 +604,8 @@ To `drop a guest resource instance` given |resourceInstance|: 1. Perform `drop a guest resource` given |class|.[[ResourceType]] and |rep|. 1. Return **undefined**. +The [[LendCount]] check can only fail on the `%Symbol.dispose%` path, because a lent instance is kept alive by the current lender list. + ## Instantiation To `instantiate a component` given |component|, a list of component definitions |imports|, and |hostResourceTypes|: @@ -600,7 +614,7 @@ To `instantiate a component` given |component|, a list of component definitions 1. Throw a `WebAssembly.RuntimeError`. 1. Perform ? `create guest resource classes` given |instance|. 1. Let |exportsObject| be ? `create the exports object` given |instance|. -1. Return a Record whose [[ComponentInstance]] is |instance|, [[Exports]] is |exportsObject|, and [[HostResourceTypes]] is |hostResourceTypes|. +1. Return a Record whose [[ComponentInstance]] is |instance|, [[Exports]] is |exportsObject|, [[HostResourceTypes]] is |hostResourceTypes|, and [[GuestResourceClasses]] is |instance|.[[GuestResourceClasses]]. To `instantiate a component from an imports object` given |component|, |importsObject|, and |enabledBuiltins|: 1. Let |imports| and |hostResourceTypes| be ? `read the imports` given |component|, |importsObject|, and |enabledBuiltins|. @@ -608,14 +622,16 @@ To `instantiate a component from an imports object` given |component|, |importsO ### Read the imports object -The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the core JS-API's algorithm of the same name. The resolved JS values are then handed to the per-sort algorithms (`read the function import`, `read the type import`, and the rest) to produce the component definitions used during instantiation. +The top-level `read the imports` algorithm walks the component's imports and resolves each to a JS value via property lookups on the |importsObject|, mirroring the core JS-API's algorithm of the same name. + +The resolved JS values are then handed to the per-sort algorithms (`read the function import`, `read the type import`, and the rest) to produce the component definitions used during instantiation. While walking, the algorithm recognizes the pattern of a resource type import accompanied by `[constructor]`, `[static]`, and `[method]` function imports tied to it. A resource type import is read first and looks for a constructor (see [resource types](#resource-types)). The tagged function imports then read from the constructor and its prototype directly. This allows the common case of importing a class to be satisfied by just passing the constructor. -Passing an exported component definition to a component import via the JS-API/ESM-integration is treated as if the import were a JS value. There is no "direct linking" that bypasses going through JS semantics. This is different from core wasm, where exported functions are linked directly when imported and have stricter type checks. This is intentional to prevent the implementation detail of how a JS function was implemented from leaking. Components can still nested and directly linked inside a single binary. +Passing an exported component definition to a component import via the JS-API/ESM-integration is treated as if the import were a JS value. There is no "direct linking" that bypasses going through JS semantics. This is different from core wasm, where exported functions are linked directly when imported and have stricter type checks. This is intentional to prevent the implementation detail of how a JS function was implemented from leaking. Components can still be nested and directly linked inside a single component binary. To `read the imports` given |component|, |importsObject|, and |enabledBuiltins|: -1. Let |hostResourceTypes| be an empty map from imported resource type declaration to [host resource type record](#host-resource-types-and-values). +1. Let |hostResourceTypes| be an empty map from [abstract type key](#abstract-and-transparent-types) to [host resource type record](#host-resource-types-and-values). 1. If |component| has no imports: 1. Return an empty list and |hostResourceTypes|. 1. Let |imports| be ? `read a scope of imports` given |component|.Imports, |importsObject|, |enabledBuiltins|, and |hostResourceTypes|. @@ -623,7 +639,6 @@ To `read the imports` given |component|, |importsObject|, and |enabledBuiltins|: To `read a scope of imports` given a list of import declarations |importDecls|, |importsObject|, |enabledBuiltins|, and |hostResourceTypes|: 1. Let |definitions| be a new empty list. - 1. For each |importDecl| of |importDecls|, in declaration order: 1. Let |name| be `JSSpecifier`(|importDecl|). 1. Let |builtin| be `resolve a builtin specifier` given |name| and |enabledBuiltins|. @@ -631,9 +646,9 @@ To `read a scope of imports` given a list of import declarations |importDecls|, 1. Let |importValue| be |builtin|. 1. Else: 1. If |importDecl|.Sort is **func** and |importDecl|.Name is tagged `[constructor]`, `[method].` or `[static].`: - 1. Let |R| be the imported resource type declaration named by the tag's `` label. - 1. Assert: |hostResourceTypes|[|R|] exists. (Validation requires that declaration to precede this one in the same scope) - 1. Let |constructorFunction| be |hostResourceTypes|[|R|].[[ConstructorObject]]. + 1. Let |abstractTypeKey| be the *abstract type key* of |R|. + 1. Assert: |hostResourceTypes|[|abstractTypeKey|] exists. (Validation requires that declaration to precede this one in the same scope) + 1. Let |constructorFunction| be |hostResourceTypes|[|abstractTypeKey|].[[ConstructorObject]]. 1. If the tag is `[constructor]`: 1. Let |importValue| be |constructorFunction|. 1. Else if the tag is `[static]`: @@ -644,20 +659,20 @@ To `read a scope of imports` given a list of import declarations |importDecls|, 1. Throw a `WebAssembly.LinkError`. 1. Let |importValue| be ? `Get`(|prototype|, |name|). 1. Else: - 1. If `Type`(|object|) is not Object: + 1. If `Type`(|importsObject|) is not Object: 1. Throw a `TypeError`. - 1. Let |importValue| be ? `Get`(|object|, |name|). + 1. Let |importValue| be ? `Get`(|importsObject|, |name|). 1. Let |resolved| be ? `read an import` given |importDecl|, |importValue| and |hostResourceTypes|. 1. Append |resolved| to |definitions|. 1. Return |definitions|. To `read an import` given |importDecl|, |importValue| and |hostResourceTypes|: -1. Match |decl|.Sort: +1. Match |importDecl|.Sort: 1. **core module**: return ? `read the core module import` given |importDecl|.ModuleType and |importValue|. 1. **func**: return ? `read the function import` given |importDecl|.FuncType, |importValue|, |importDecl|.Name's tag and |hostResourceTypes|. 1. **type**: return ? `read the type import` given |importDecl|, |importValue|, and |hostResourceTypes|. 1. **value**: return ? `read the value import` given |importDecl|.ValType and |importValue|. - 1. **instance**: return ? `read the instance import` given |importDecl|.InstanceType and |importValue|. + 1. **instance**: return ? `read the instance import` given |importDecl|.InstanceType, |importValue| and |hostResourceTypes|. 1. **component**: return ? `read the component import` given |importDecl|.ComponentType and |importValue|. To `read the core module import` given |coreModuleType| and |importValue|: @@ -674,27 +689,26 @@ To `read the component import` given |componentType| and |importValue|: 1. Throw a `WebAssembly.LinkError`. 1. Return |importValue|.[[Component]]. -To `read the instance import` given |instanceType| and |importValue|: +To `read the instance import` given |instanceType|, |importValue| and |hostResourceTypes|: 1. If `Type`(|importValue|) is not Object: 1. Throw a `WebAssembly.LinkError`. -1. Let |definitions| be ? `read a scope of imports` given |instanceType|.Exports, |importValue| and an empty set of enabled builtins. +1. Let |definitions| be ? `read a scope of imports` given |instanceType|.Exports, |importValue|, an empty set of enabled builtins, and |hostResourceTypes|. 1. Return a component instance whose exports are |definitions|. To `read the type import` given |importDecl|, |importValue|, and |hostResourceTypes|: -1. Let |typeBound| be |importDecl|.TypeBound. -1. Let |abstractTypeKey| be the *abstract type key* of |typeBound|. +1. Let |abstractTypeKey| be the *abstract type key* of |importDecl|. 1. If |hostResourceTypes|[|abstractTypeKey|] exists: 1. Return |hostResourceTypes|[|abstractTypeKey|].[[ComponentResourceType]]. 1. If `IsCallable`(|importValue|) is **false**: 1. Throw a `WebAssembly.LinkError`. 1. Let |destructor| be a host function that, given a host resource value, releases its reference to [[JSValue]] and returns. 1. Let |resourceType| be `create a resource type for host` given |destructor|. -1. Let |hostResourceType| be a new host resource type record whose [[ComponentResourceType]] is |resourceType| and whose [[ConstructorObject]] is |importValue|. +1. Let |hostResourceType| be a new host resource type record whose [[ComponentResourceType]] is |resourceType| and [[ConstructorObject]] is |importValue|. 1. Set |hostResourceTypes|[|abstractTypeKey|] to |hostResourceType|. 1. Return |resourceType|. To `read the function import` given |componentFuncType|, |importValue|, |importNameTag| and |hostResourceTypes|: -1. If |importValue| is not callable: +1. If `IsCallable`(|importValue|) is **false**: 1. Throw a `WebAssembly.LinkError`. 1. If |importNameTag| is `[constructor]` and `IsConstructor`(|importValue|) is **false**: 1. Throw a `WebAssembly.LinkError`. @@ -704,7 +718,7 @@ To `read the function import` given |componentFuncType|, |importValue|, |importN 1. Let |callKind|, |receiverRule| and |paramOffset| be determined by |importNameTag|: 1. `[constructor]`: `Construct`, no receiver, offset 0. 1. `[method].`: `Call`, receiver is component argument 0 (the `borrow` self), offset 1. - 1. `[static].`: `Call`, receiver is |hostResourceTypes|[R].[[ConstructorObject]], offset 0. + 1. `[static].`: `Call`, receiver is |hostResourceTypes|[the *abstract type key* of `R`].[[ConstructorObject]], offset 0. 1. otherwise: `Call`, receiver is **undefined**, offset 0. 1. If |resultType| is a `result`: 1. Let |okType| be its `ok` payload type, or **empty** if it has none. @@ -713,9 +727,9 @@ To `read the function import` given |componentFuncType|, |importValue|, |importN 1. Else: 1. Let |okType| be |resultType|, or **empty** if |componentFuncType| has no result. 1. Let |throwing| be **false**. -1. Return a component host function of type |componentFuncType| whose body, given component arguments « |v_0|, ..., |v_{n-1}| », performs: +1. Return a component host function of type |componentFuncType| whose body, given component arguments « |v_0|, ..., |v_{n-1}| » where |n| is |paramTypes|.length, performs: 1. Let |args| be a new empty List. - 1. For each i in [|paramOffset|, n): + 1. For each i in [|paramOffset|, |n|): 1. Append `ToJSValue`(|v_i|, |paramTypes|[i]) to |args|. 1. If |callKind| is `Construct`: 1. Let |completion| be `Construct`(|callable|, |args|). @@ -753,7 +767,7 @@ To `read the value import` given |componentValType| and |importValue|: The component JS-API can provide builtins to imports just as the core JS-API does. -Builtin imports are opt-in via `WebAssemblyCompileOptions` when used in the JS-API. [ESM-integration](#webassembly-esm-integration) enables all builtins by default. +Builtin imports are opt-in via the `builtins` field of `WebAssemblyCompileOptions` when used in the JS-API. [ESM-integration](#webassembly-esm-integration) enables all builtins by default. This spec defines one builtin specifier: @@ -762,41 +776,44 @@ This spec defines one builtin specifier: | `wasm:js/global` | [the global object](#the-global-object) | To `resolve a builtin specifier` given a String |specifier| and a set of Strings |enabledBuiltins|: -1. If |specifier| is not in |enabledBuiltins|: +1. If |specifier| does not start with "wasm:": 1. Return **empty**. -1. If |specifier| is "wasm:js/global": - 1. Return the current realm's global object. +1. Let |name| be the portion of |specifier| after "wasm:". +1. If |name| is not in |enabledBuiltins|: + 1. Return **empty**. +1. If |name| is "js/global": + 1. Return the [conversion realm](#types-and-values)'s global object. 1. Return **empty**. #### The global object -`wasm:js/global` can be used to import JS/web API's off of the global object. It simply resolves to `globalThis`, and then the normal [`read the imports object`](#read-the-imports-object) rules can take it from there. - -The following component imports `Element`, `Element.prototype.getAttribute`, and `btoa`: - -```wat -(component - (import "wasm:js/global" (instance $g - (export "element" (type $element (sub resource))) - (export "[method]element.get-attribute" (func - (param "self" (borrow $element)) (param "name" string) - (result (option string)))) - (export "btoa" (func (param "data" string) (result string))) - )) - ... -) -``` +`wasm:js/global` can be used to import JS/web APIs off of the global object. It simply resolves to the `globalThis` of the [conversion realm](#types-and-values), and then the normal [`read the imports`](#read-the-imports-object) rules can take it from there. ### Create the exports object The `create the exports object` algorithm walks the component's exports and builds a fresh JS object whose properties are the exports. Exported resource types become [guest resource classes](#guest-resource-classes) named `JSName`(|export|), and tagged function exports are mapped onto them just as in `read the imports`: -- `[constructor]`: the function becomes `R`'s constructor behaviour. By strong-uniqueness there can only be one. +- `[constructor]`: the function becomes `R`'s constructor behaviour. Names are strongly-unique, so there can only be one. - `[method].`: the function becomes a method named `JSName`(|export|) on `R.prototype`. - `[static].`: the function becomes a static method named `JSName`(|export|) on `R`. -All other exported components definitions are given JS definitions named `JSName`(|export|) on the exports object. +All other exported component definitions are given JS definitions named `JSName`(|export|) on the exports object. + +To `create guest resource classes` given a component instance |componentInstance|: +1. Let |guestResourceClasses| be an empty map from [abstract type key](#abstract-and-transparent-types) to [guest resource class](#guest-resource-classes). +1. Let |component| be |componentInstance|.[[Component]]. +1. For each type export |export| of |component|'s type, in declaration order, recursing into exported instances: + 1. Let |abstractTypeKey| be the *abstract type key* of |export|. + 1. If |abstractTypeKey| is one of |component|'s type imports: + 1. Throw a `TypeError`. + 1. If |guestResourceClasses|[|abstractTypeKey|] exists: + 1. Continue. + 1. Let |resourceType| be the component resource type |export| refers to in |componentInstance|. + 1. Let |arity| be the parameter count of the `[constructor]` export targeting |export|, or 0 if there is none. + 1. Let |class| be `create a guest resource class` given |componentInstance|, |resourceType|, `JSName`(|export|) and |arity|. + 1. Set |guestResourceClasses|[|abstractTypeKey|] to |class|. +1. Set |componentInstance|.[[GuestResourceClasses]] to |guestResourceClasses|. To `create the exports object` given a |componentInstance|: 1. Let |exportsObject| be `OrdinaryObjectCreate`(**null**). @@ -808,8 +825,8 @@ To `create the exports object` given a |componentInstance|: 1. **core module**: 1. Let |value| be a new `Module` whose [[Module]] is |export|.Module. 1. **type**: - 1. Let |abstractTypeKey| be the *abstract type key* of |export|.TypeBound. - 1. Let |value| be |componentInstance|.[[GuestResourceClasses]][|abstractTypeKey|] + 1. Let |abstractTypeKey| be the *abstract type key* of |export|. + 1. Let |value| be |componentInstance|.[[GuestResourceClasses]][|abstractTypeKey|]. 1. **func**: 1. Let |value| be `create a JS function for a component function` given |export|.Func, |key| and no tag. 1. **value**: @@ -847,17 +864,17 @@ To `invoke a component function` given |componentFunc|, |exportNameTag|, |thisVa 1. Else: 1. Let |okType| be |resultType|, or **empty** if |componentFunc| has no result. 1. Let |throwing| be **false**. +1. If the number of |args| is less than |paramTypes|.length - |paramOffset|: + 1. Throw a `TypeError`. 1. Let |lenders| be a new empty List. 1. Let |previousLenders| be the current lender list. 1. Set the current lender list to |lenders|. -1. After the following returns either normally or abruptly: +1. Once the remaining steps complete, either normally or abruptly, perform: 1. Decrement the [[LendCount]] of every instance in |lenders|. 1. Set the current lender list to |previousLenders|. 1. Let |values| be a new empty List. 1. If |paramOffset| is 1: 1. Append ? `ToComponentValue`(|thisValue|, |paramTypes|[0]) to |values|. -1. If the number of |args| is less than |paramTypes|.length - |paramOffset|: - 1. Throw a `TypeError`. 1. For each i in [0, |paramTypes|.length - |paramOffset|): append ? `ToComponentValue`(|args|[i], |paramTypes|[i + |paramOffset|]) to |values|. 1. Let |componentResult| be the result of invoking |componentFunc| with |values|. 1. If the call traps: @@ -876,21 +893,21 @@ To `create a component error` for an optional component value |e| and component 1. Let |payload| be **undefined**. 1. Else: 1. Let |payload| be `ToJSValue`(|e|, |errorType|). -1. Return a new `ComponentError` whose `data` is |payload| and an implementation defined `message`. +1. Return a new `ComponentError` whose `data` is |payload| and whose `message` is implementation-defined. ## WebAssembly ESM-integration [ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The module loader branches on the `layer` field of the binary to decide whether the bytes decode as a module or a component, so a component can be loaded anywhere a module can be today. -Each component import becomes has a [module specifier](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ModuleSpecifier) given by `JSSpecifier`(|decl|). +Each component import has a [module specifier](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ModuleSpecifier) given by `JSSpecifier`(|decl|). Which binding of the resolved module the component receives depends on the import's type: | Import type | JS equivalent | Value | |---|---|---| -| bare type, function, value | `import v from "Specifier(|decl|)"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | -| instance | `import { a, b } from "Specifier(|decl|)"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per untagged export of the instance type, named `JSName` of that export | -| core module, component | `import source M from "Specifier(|decl|)"` | the module source, as a `Module` or `Component` | +| bare type, function, value | `import v from "JSSpecifier(|decl|)"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | +| instance | `import { a, b } from "JSSpecifier(|decl|)"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per untagged export of the instance type, named `JSName` of that export | +| core module, component | `import source M from "JSSpecifier(|decl|)"` | the module source, as a `Module` or `Component` | Reading the imports snapshots the resolved values, and so components cannot participate in cycles. This matches how core modules work today with ESM-integration. @@ -901,13 +918,12 @@ A component's exports become the bindings of its module namespace object. There ## Follow ups 1. How to dynamically pass a union value? Statically passing a single case of the union works, but not dynamic choice. -1. How to import multiple overloads of a function? -1. How to support class inheritance and casting? Can a component defined resource sub-class an imported resource type? +1. How to import multiple overloads of a function? Can we just use `external-id`? +1. How to support class inheritance and casting? 1. Do we support a reference equality protocol? `ToJSValue` creates a fresh resource instance per lift, so two `borrow`s of one component-defined resource are two JS objects that do not compare equal. Reps are opaque and reusable after a drop, so an identity map would need careful invalidation. 1. Do we let a JS constructor supply its own brand check? -1. Should a `ComponentInstance` expose whether it is [locked down](#lockdown-after-a-trap)? Today JS can only find out by calling in and catching a `WebAssembly.RuntimeError`. 1. How to import/export properties with getters/setters? 1. How does a component feature test an import? 1. How does a component pass one of its own functions to a JS callback, e.g. `add-event-listener`? -1. What is the precise timing of [Get][Set] during lifting/lowering if a wasm trap happens. +1. What is the precise timing of `Get`/`Set` during lifting/lowering if a wasm trap happens. 1. Top-level await, and async start functions. From 90f0b48758b85f7b146a760bc64b437ea81cc6d7 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Wed, 16 Sep 2026 15:49:55 -0500 Subject: [PATCH 5/7] [js-api] Update for get/set --- design/mvp/JS-Reference.md | 121 +++++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md index 4a28cdde..87cd1d1f 100644 --- a/design/mvp/JS-Reference.md +++ b/design/mvp/JS-Reference.md @@ -126,11 +126,10 @@ We define `PascalCase(label)` and `CamelCase(label)` below. | `a1-2-3` | `A123` | `a123` | `LabelOf`(|name|), where |name| is a `plainname`, returns the label that names the definition in JS: -1. If |name| is `[method]r.n` or `[static]r.n`: +1. Let |stripped| be |name| with every `[...]` annotation removed. +1. If |stripped| is `r.n`: 1. Return `n`. -1. If |name| is `[constructor]r`: - 1. Return `r`. -1. Return |name|. +1. Return |stripped|. `Fragments`(|label|): 1. Return the List of Strings produced by splitting |label| on occurrences of U+002D (-). The hyphens themselves are discarded. @@ -506,20 +505,26 @@ To `create a guest resource class` given a component instance |componentInstance 1. Let |tagged| be the `[constructor]`, `[method]` and `[static]` function exports in |componentInstance|'s scope that target |resourceType|. 1. If |tagged| has a `[constructor]` export |c|: 1. Set |constructor|.[[ConstructorFunc]] to |c|.Func. -1. For each `[method]` export |m| of |tagged|: - 1. If `JSName`(|m|) is "constructor": +1. For each `[method]` or `[static]` export |e| of |tagged|, in declaration order: + 1. If |e| is tagged `[method]`: + 1. Let |target| be |prototype|. + 1. Let |reserved| be "constructor". + 1. Else if |e| is tagged `[static]`: + 1. Let |target| be |constructor| + 1. Let |reserved| be "prototype". + 1. If `JSName`(|e|) is |reserved|: 1. Throw a `TypeError`. - 1. Let |func| be `create a JS function for a component function` given |m|.Func, `JSName`(|m|) and |m|'s tag. - 1. Perform `DefinePropertyOrThrow`(|constructor|'s `"prototype"`, `JSName`(|m|), PropertyDescriptor { [[Value]]: |func|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). -1. For each `[static]` export |s| of |tagged|: - 1. If `JSName`(|s|) is "prototype": - 1. Throw a `TypeError`. - 1. Let |func| be `create a JS function for a component function` given |s|.Func, `JSName`(|s|) and |s|'s tag. - 1. Perform `DefinePropertyOrThrow`(|constructor|, `JSName`(|s|), PropertyDescriptor { [[Value]]: |func|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). + 1. If |e| is tagged `[get]` or `[set]`: + 1. Perform `define an accessor for a component function` given |target|, |e| and **false**. + 1. Else: + 1. Let |func| be `create a JS function for a component function` given |e|.Func, `JSName`(|e|) and |e|'s tag. + 1. Perform `DefinePropertyOrThrow`(|target|, `JSName`(|e|), PropertyDescriptor { [[Value]]: |func|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). 1. Return |constructor|. A method named `constructor` and a static named `prototype` are rejected because they would unexpectedly change JS class semantics. +`[get]` and `[set]` exports become the two halves of one accessor property, on `prototype` when tagged `[method]` and on the class itself when tagged `[static]`. Validation requires a `[set]` to be preceded in the same scope by the `[get]` it pairs with, so the getter is always defined first and the setter only fills in the accessor's [[Set]] field. + #### Guest resource instances An instance of a guest resource class has the following slots: @@ -641,35 +646,47 @@ To `read a scope of imports` given a list of import declarations |importDecls|, 1. Let |definitions| be a new empty list. 1. For each |importDecl| of |importDecls|, in declaration order: 1. Let |name| be `JSSpecifier`(|importDecl|). + 1. Let |staticReceiver| be **undefined**. 1. Let |builtin| be `resolve a builtin specifier` given |name| and |enabledBuiltins|. 1. If |builtin| is not **empty**: 1. Let |importValue| be |builtin|. 1. Else: - 1. If |importDecl|.Sort is **func** and |importDecl|.Name is tagged `[constructor]`, `[method].` or `[static].`: + 1. If |importDecl|.Sort is **func** and |importDecl|.Name is tagged `[constructor]`, `[method]([get]|[set])?.` or `[static]([get]|[set])?.`: 1. Let |abstractTypeKey| be the *abstract type key* of |R|. 1. Assert: |hostResourceTypes|[|abstractTypeKey|] exists. (Validation requires that declaration to precede this one in the same scope) 1. Let |constructorFunction| be |hostResourceTypes|[|abstractTypeKey|].[[ConstructorObject]]. 1. If the tag is `[constructor]`: 1. Let |importValue| be |constructorFunction|. - 1. Else if the tag is `[static]`: - 1. Let |importValue| be ? `Get`(|constructorFunction|, |name|). - 1. Else: - 1. Let |prototype| be ? `Get`(|constructorFunction|, "prototype"). - 1. If `Type`(|prototype|) is not Object: - 1. Throw a `WebAssembly.LinkError`. - 1. Let |importValue| be ? `Get`(|prototype|, |name|). + 1. Else if the tag is `[method]` or `[static]`: + 1. If the tag is `[static]`: + 1. Let |lookupTarget| be |constructorFunction|. + 1. Set |staticReceiver| to |lookupTarget|. + 1. Else: + 1. Let |lookupTarget| to ? `Get`(|constructorFunction|, "prototype"). + 1. If `Type`(|lookupTarget|) is not Object: + 1. Throw a `WebAssembly.LinkError`. + + 1. If |importDecl|.Name is also tagged `[get]` or `[set]`: + 1. Let |importValue| be ? `find an accessor` given |lookupTarget|, |name| and that annotation. + 1. Else: + 1. Let |importValue| be ? `Get`(|lookupTarget|, |name|). 1. Else: 1. If `Type`(|importsObject|) is not Object: 1. Throw a `TypeError`. - 1. Let |importValue| be ? `Get`(|importsObject|, |name|). - 1. Let |resolved| be ? `read an import` given |importDecl|, |importValue| and |hostResourceTypes|. + + 1. If |importDecl|.Sort is **func** and |importDecl|.Name is tagged `[get]` or `[set]`: + 1. Set |staticReceiver| to |importsObject|. + 1. Let |importValue| be ? `find an accessor` given |importsObject|, |name| and that annotation. + 1. Else: + 1. Let |importValue| be ? `Get`(|importsObject|, |name|). + 1. Let |resolved| be ? `read an import` given |importDecl|, |importValue|, |staticReceiver| and |hostResourceTypes|. 1. Append |resolved| to |definitions|. 1. Return |definitions|. -To `read an import` given |importDecl|, |importValue| and |hostResourceTypes|: +To `read an import` given |importDecl|, |importValue|, |staticReceiver| and |hostResourceTypes|: 1. Match |importDecl|.Sort: 1. **core module**: return ? `read the core module import` given |importDecl|.ModuleType and |importValue|. - 1. **func**: return ? `read the function import` given |importDecl|.FuncType, |importValue|, |importDecl|.Name's tag and |hostResourceTypes|. + 1. **func**: return ? `read the function import` given |importDecl|.FuncType, |importValue|, |importDecl|.Name's tag and |staticReceiver|. 1. **type**: return ? `read the type import` given |importDecl|, |importValue|, and |hostResourceTypes|. 1. **value**: return ? `read the value import` given |importDecl|.ValType and |importValue|. 1. **instance**: return ? `read the instance import` given |importDecl|.InstanceType, |importValue| and |hostResourceTypes|. @@ -707,19 +724,34 @@ To `read the type import` given |importDecl|, |importValue|, and |hostResourceTy 1. Set |hostResourceTypes|[|abstractTypeKey|] to |hostResourceType|. 1. Return |resourceType|. -To `read the function import` given |componentFuncType|, |importValue|, |importNameTag| and |hostResourceTypes|: +To `find an accessor` given an object |target|, a property key |key| and |kind|, which is either `[get]` or `[set]`: +1. Let |object| be |target|. +1. Repeat, while |object| is not **null**: + 1. Let |desc| be ? |object|.[[GetOwnProperty]](|key|). + 1. If |desc| is not **undefined**: + 1. If `IsAccessorDescriptor`(|desc|) is **false**: + 1. Return **undefined**. + 1. If |kind| is `[get]`, return |desc|.[[Get]]. + 1. Return |desc|.[[Set]]. + 1. Set |object| to ? |object|.[[GetPrototypeOf]](). +1. Return **undefined**. + +The walk stops at the first own property it finds, as an ordinary property access does. A data property that shadows an accessor further up the chain therefore resolves to **undefined** and becomes a `LinkError`. + +To `read the function import` given |componentFuncType|, |importValue|, |importNameTag| and |staticReceiver|: 1. If `IsCallable`(|importValue|) is **false**: 1. Throw a `WebAssembly.LinkError`. 1. If |importNameTag| is `[constructor]` and `IsConstructor`(|importValue|) is **false**: 1. Throw a `WebAssembly.LinkError`. 1. Let |callable| be |importValue|. -1. Let |paramTypes| be |componentFuncType|.Params. -1. Let |resultType| be |componentFuncType|.Result. + 1. Let |callKind|, |receiverRule| and |paramOffset| be determined by |importNameTag|: 1. `[constructor]`: `Construct`, no receiver, offset 0. - 1. `[method].`: `Call`, receiver is component argument 0 (the `borrow` self), offset 1. - 1. `[static].`: `Call`, receiver is |hostResourceTypes|[the *abstract type key* of `R`].[[ConstructorObject]], offset 0. - 1. otherwise: `Call`, receiver is **undefined**, offset 0. + 1. `[method]`-tagged: `Call`, receiver is component argument 0 (the `borrow` self), offset 1. + 1. `[static]`-tagged: `Call`, receiver is |staticReceiver|, offset 0. + 1. no-tag: `Call`, receiver is |staticReceiver|, offset 0. +1. Let |paramTypes| be |componentFuncType|.Params. +1. Let |resultType| be |componentFuncType|.Result. 1. If |resultType| is a `result`: 1. Let |okType| be its `ok` payload type, or **empty** if it has none. 1. Let |errorType| be its `error` payload type, or **empty** if it has none. @@ -797,8 +829,10 @@ Exported resource types become [guest resource classes](#guest-resource-classes) - `[constructor]`: the function becomes `R`'s constructor behaviour. Names are strongly-unique, so there can only be one. - `[method].`: the function becomes a method named `JSName`(|export|) on `R.prototype`. - `[static].`: the function becomes a static method named `JSName`(|export|) on `R`. +- `[method][get].` and `[method][set].`: the functions become the getter and setter of an accessor property named `JSName`(|export|) on `R.prototype`. +- `[static][get].` and `[static][set].`: the same, but on `R`. -All other exported component definitions are given JS definitions named `JSName`(|export|) on the exports object. +A `[get]` or `[set]` export that is not attached to a resource type becomes an accessor property on the exports object itself. All other exported component definitions are given JS definitions named `JSName`(|export|) on the exports object. To `create guest resource classes` given a component instance |componentInstance|: 1. Let |guestResourceClasses| be an empty map from [abstract type key](#abstract-and-transparent-types) to [guest resource class](#guest-resource-classes). @@ -818,7 +852,10 @@ To `create guest resource classes` given a component instance |componentInstance To `create the exports object` given a |componentInstance|: 1. Let |exportsObject| be `OrdinaryObjectCreate`(**null**). 1. For each |export| of |componentInstance|.Exports, in declaration order: - 1. If |export|.Name is tagged `[constructor]`, `[method].` or `[static].`: + 1. If |export|.Name is tagged `[constructor]`, `[method].` or `[static].`, with or without a `[get]` or `[set]` annotation: + 1. Continue. + 1. If |export|.Name is tagged `[get]` or `[set]`: + 1. Perform `define an accessor for a component function` given |exportsObject|, |export| and **true**. 1. Continue. 1. Let |key| be `JSName`(|export|). 1. Match |export|.Sort: @@ -840,7 +877,7 @@ To `create the exports object` given a |componentInstance|: 1. Return |exportsObject|. To `create a JS function for a component function` given |componentFunc|, |name| and |exportNameTag|: -1. Let |paramOffset| be 1 if |exportNameTag| is `[method].`, else 0. +1. Let |paramOffset| be 1 if |exportNameTag| is tagged `[method]`, else 0. 1. If |componentFunc|.Result is a `result`: 1. Let |okType| be its `ok` payload type, or **empty** if it has none. 1. Else: @@ -851,12 +888,22 @@ To `create a JS function for a component function` given |componentFunc|, |name| 1. Return **undefined**. 1. Return `ToJSValue`(|componentResult|, |okType|). -A `[method]` export takes its **this** value as the component function's first parameter, which validation guarantees is the `borrow` self, mirroring how `read the function import` maps component argument 0 onto a JS receiver. A `[static]` export ignores its **this** value. +To `define an accessor for a component function` given an object |target|, a function export |export| and a Boolean |enumerable|: +1. Let |key| be `JSName`(|export|). +1. Let |prefix| be "get " if |export|.Name is tagged `[get]`, and "set " otherwise. +1. Let |name| be the string-concatenation of |prefix| and |key|. +1. Let |func| be `create a JS function for a component function` given |export|.Func, |name|, and |export|'s tag. +1. If |export|.Name is tagged `[get]`: + 1. Perform `DefinePropertyOrThrow`(|target|, |key|, PropertyDescriptor { [[Get]]: |func|, [[Set]]: **undefined**, [[Enumerable]]: |enumerable|, [[Configurable]]: **true** }). +1. Else: + 1. Perform `DefinePropertyOrThrow`(|target|, |key|, PropertyDescriptor { [[Set]]: |func| }). + +The `[set]` case defines a partial descriptor, so it only replaces the [[Set]] field of the func property the matching `[get]` export already defined. The `"get "`/`"set "` prefix on the function name follows how JS names accessor functions. To `invoke a component function` given |componentFunc|, |exportNameTag|, |thisValue| and a List of JS values |args|: 1. Let |paramTypes| be |componentFunc|.Params. 1. Let |resultType| be |componentFunc|.Result. -1. Let |paramOffset| be 1 if |exportNameTag| is `[method].`, else 0. +1. Let |paramOffset| be 1 if |exportNameTag| is tagged `[method]`, else 0. 1. If |resultType| is a `result`: 1. Let |okType| be its `ok` payload type, or **empty** if it has none. 1. Let |errorType| be its `error` payload type, or **empty** if it has none. @@ -922,7 +969,7 @@ A component's exports become the bindings of its module namespace object. There 1. How to support class inheritance and casting? 1. Do we support a reference equality protocol? `ToJSValue` creates a fresh resource instance per lift, so two `borrow`s of one component-defined resource are two JS objects that do not compare equal. Reps are opaque and reusable after a drop, so an identity map would need careful invalidation. 1. Do we let a JS constructor supply its own brand check? -1. How to import/export properties with getters/setters? +1. Should a `[get]`/`[set]` import fall back to a `Get`/`Set` on the target when the property is not an accessor? That would let data properties, `Proxy` traps and module namespace bindings satisfy a property import. 1. How does a component feature test an import? 1. How does a component pass one of its own functions to a JS callback, e.g. `add-event-listener`? 1. What is the precise timing of `Get`/`Set` during lifting/lowering if a wasm trap happens. From 99f191863d99dcd014e41a4be5ec3ecaff3e2e69 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Wed, 16 Sep 2026 19:36:43 -0500 Subject: [PATCH 6/7] [js-api] Formatting fixes --- design/mvp/JS-Explainer.md | 170 ++++++++++++++++++++++++------------- design/mvp/JS-Reference.md | 14 +-- 2 files changed, 118 insertions(+), 66 deletions(-) diff --git a/design/mvp/JS-Explainer.md b/design/mvp/JS-Explainer.md index 31ef69db..a3a607e3 100644 --- a/design/mvp/JS-Explainer.md +++ b/design/mvp/JS-Explainer.md @@ -8,39 +8,15 @@ See the [reference](./JS-Reference.md) for an in-depth walkthrough. ## Walkthrough -### Values at a glance - -Components and JS maintain separate type/value systems, so any value crossing the boundary needs a defined translation in both directions. - -| Component type | JS | -|---|---| -| `bool` | Boolean | -| `s8`-`s32`, `u8`-`u32` | Number, an exact integer | -| `s64`, `u64` | BigInt | -| `f32`, `f64` | Number, including NaN and infinities | -| `char` | String of exactly one Unicode scalar value | -| `string` | String, well formed | -| `list` | `Uint8Array` | -| `list`, `list`, `tuple` | Array | -| `record { a-b: T }` | null-prototype object, `{ aB }` | -| `flags "a" "b"` | null-prototype object of Booleans, `{ a, b }` | -| `enum "a" "b"` | String, the label verbatim | -| `option` | `null`, or the payload | -| `variant`, `option>` | `{ kind, value }` | -| `result` | thrown and caught in return position, else `{ kind, value }` | -| `map` | `Map` | -| `own`, `borrow` | the original JS value for an imported resource type, an instance of its class for an exported one | -| `future`, `stream`, `error-context` | not yet specified | - -See [ToJSValue](./JS-Reference.md#tojsvalue) and [ToComponentValue](./JS-Reference.md#tocomponentvalue) for detailed algorithms. - -### A greeter +### Greeter: exporting a function Let's start with a component that imports nothing: ```wat (component - (export "greet" (func (param "who" string) (result string))) + (export "greet" + (func (param "who" string) (result string)) + ) ) ``` @@ -50,7 +26,7 @@ const { instance } = await WebAssembly.instantiate(bytes); instance.exports.greet("world"); // "hello, world" ``` -`exports` holds one property per export and `greet` is an ordinary function. Component names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. +`exports` holds one property per export and `greet` is an ordinary JS function. Component names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. Arguments are coerced to their expected type, and if that fails a `TypeError` is thrown: @@ -61,21 +37,28 @@ instance.exports.greet(); // TypeError Passing too few arguments is a `TypeError`. Extra arguments are ignored. -### A logger +### Logger: importing a function Now a component that imports: ```wat (component - (import "log" (func (param "message" string))) + (import "log" + (func (param "message" string)) + ) (export "run" (func)) ) ``` -The import `log` must be a JS callable object, and will be called with a JS String. We can provide the `console.log` builtin here: +The import `log` must be a JS callable object, and will be called with a JS String. + +We can provide the `console.log` builtin as here: ```js -const { instance } = await WebAssembly.instantiate(bytes, { log: console.log }); +const imports = { log: console.log }; + +const { instance } = + await WebAssembly.instantiate(bytes, imports); instance.exports.run(); // logs "hello" ``` @@ -85,10 +68,38 @@ Or provide a custom implementation: ```js const lines = []; const log = (message) => { lines.push(message); }; +const imports = { log }; -const { instance } = await WebAssembly.instantiate(bytes, { log }); +const { instance } = + await WebAssembly.instantiate(bytes, imports); ``` +### Values at a glance + +Components and JS maintain separate type/value systems, so any value crossing the boundary needs a defined translation in both directions. + +| Component type | JS | +|---|---| +| `bool` | Boolean | +| `s8`-`s32`, `u8`-`u32` | Number, an exact integer | +| `s64`, `u64` | BigInt | +| `f32`, `f64` | Number, including NaN and infinities | +| `char` | String of exactly one Unicode scalar value | +| `string` | String, well formed | +| `list` | `Uint8Array` | +| `list`, `list`, `tuple` | Array | +| `record { a-b: T }` | null-prototype object, `{ aB }` | +| `flags "a" "b"` | null-prototype object of Booleans, `{ a, b }` | +| `enum "a" "b"` | String, the label verbatim | +| `option` | `null`, or the payload | +| `variant`, `option>` | `{ kind, value }` | +| `result` | thrown and caught in return position, else `{ kind, value }` | +| `map` | `Map` | +| `own`, `borrow` | the original JS value for an imported resource type, an instance of its class for an exported one | +| `future`, `stream`, `error-context` | not yet specified | + +See [ToJSValue](./JS-Reference.md#tojsvalue) and [ToComponentValue](./JS-Reference.md#tocomponentvalue) for detailed algorithms. + ### Loading with ESM [ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary, so a component loads anywhere a core module does today. @@ -98,7 +109,8 @@ Each component import becomes a JS import, and its module specifier is the impor ```wat (component (import "slugify" - (external-id "https://esm.unpkg.com/slugify@1.6.6") + (external-id + "https://esm.unpkg.com/slugify@1.6.6") (func (param "text" string) (result string)) ) (export "run" (func)) @@ -108,7 +120,9 @@ Each component import becomes a JS import, and its module specifier is the impor ```html ``` @@ -122,17 +136,23 @@ An imported JS function that throws where the component asked for a plain return ```wat (component - (import "lookup" (func (param "key" string) (result string (error string)))) - (export "parse" (func (param "text" string) (result u32 (error string)))) + (import "lookup" + (func (param "key" string) (result string (error string))) + ) + (export "parse" + (func (param "text" string) (result u32 (error string))) + ) ) ``` `parse` tries to parse its `text` argument as an integer, and if that fails performs a fallible lookup. ```js -const { instance } = await WebAssembly.instantiate(bytes, { +const imports = { lookup: (key) => { throw `no such key: ${key}`; }, -}); +}; +const { instance } = + await WebAssembly.instantiate(bytes, imports); instance.exports.parse("42"); // 42 @@ -150,12 +170,15 @@ In the second call, parsing fails and leads to a call to `lookup` which throws a Components see JS values as resources. A resource type import is satisfied by passing a constructor function. -Whenever a JS value must be converted to a resource type, an `instanceof` check is performed against the imported constructor. If the constructor is actually a [WebIDL interface object](https://webidl.spec.whatwg.org/#interface-object) or an [exported component resource constructor](#exporting-a-resource), a precise [brand check](./JS-Reference.md#brand-checks) is performed. +Whenever a JS value must be converted to a resource type, an `instanceof` check is performed against the imported constructor. If the constructor is a [WebIDL interface object](https://webidl.spec.whatwg.org/#interface-object) or an [exported component resource constructor](#exporting-a-resource), a precise [brand check](./JS-Reference.md#brand-checks) is performed. Any imported function whose name is tagged `[constructor]`, `[method]`, or `[static]` is looked up on the imported constructor instead of the imports object: - 1. `[constructor]R` - `R` - 1. `[method]R.M` - `R.prototype.M` - 1. `[static]R.S` - `R.S` + +| name | import lookup | +|---|---| +| `[constructor]R` | `R` | +| `[method]R.M` | `R.prototype.M` | +| `[static]R.S` | `R.S` | The above allows most JS classes to be imported as a resource by just passing the constructor function: @@ -166,19 +189,33 @@ The above allows most JS classes to be imported as a resource by just passing th ) (import "[method]element.query-selector" - (func (param "self" (borrow $element)) (param "selectors" string) (result (option (own $element)))) + (func + (param "self" (borrow $element)) + (param "selectors" string) + (result (option (own $element))) + ) ) (import "[method]element.get-attribute" - (func (param "self" (borrow $element)) (param "name" string) (result (option string))) + (func + (param "self" (borrow $element)) + (param "name" string) + (result (option string)) + ) ) (export "find" - (func (param "root" (borrow $element)) (param "selectors" string) (result (option string))) + (func + (param "root" (borrow $element)) + (param "selectors" string) + (result (option string)) + ) ) ) ``` ```js -const { instance } = await WebAssembly.instantiate(bytes, { element: Element }); +const imports = { element: Element }; +const { instance } = + await WebAssembly.instantiate(bytes, imports); instance.exports.find(document.body, "h1"); // "page-title" or null ``` @@ -191,23 +228,34 @@ The example above still needs someone to write `{ element: Element }`. A compone (component (import "wasm:js/global" (instance $g - (export "btoa" (func (param "data" string) (result string))) + (export "btoa" + (func (param "data" string) (result string)) + ) (export "element" (type $element (sub resource))) - (export "[method]element.get-attribute" (func - (param "self" (borrow $element)) (param "name" string) - (result (option string)))) + (export "[method]element.get-attribute" + (func + (param "self" (borrow $element)) + (param "name" string) + (result (option string)) + ) + ) ) ) (alias export $g "element" (type $el)) - (export "encode-id" (func (param "el" (borrow $el)) (result (option string)))) + (export "encode-id" + (func + (param "el" (borrow $el)) + (result (option string)) + ) + ) ) ``` ```js -const c = new WebAssembly.Component(bytes, { builtins: ["js/global"] }); -const { exports } = new WebAssembly.ComponentInstance(c); +const exports = + await WebAssembly.instantiate(bytes, { builtins: ["js/global"] }).exports; exports.encodeId(document.body); ``` @@ -230,10 +278,14 @@ A resource type exported from a component becomes a JS class: ```wat (component (export "counter" (type $counter (sub resource))) - (export "[constructor]counter" (func (result (own $counter)))) - (export "[method]counter.increment" (func - (param "self" (borrow $counter)) - (result u32)) + (export "[constructor]counter" + (func (result (own $counter))) + ) + (export "[method]counter.increment" + (func + (param "self" (borrow $counter)) + (result u32) + ) ) ) ``` diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md index 87cd1d1f..4dc1e569 100644 --- a/design/mvp/JS-Reference.md +++ b/design/mvp/JS-Reference.md @@ -9,10 +9,10 @@ This is the in-depth reference for the WebAssembly Component JS-API. See the [ex 1. Components can import and use most web and JS APIs 2. Components can export an API usable by JS 3. Components interact with the web platform in similar ways to JS: - a. Components can feature test whether APIs are present - b. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill - c. Components are tolerant of web API evolution - d. Component misuse of a web API results in failure at that call-site, not a link time error + 1. Components can feature test whether APIs are present + 1. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill + 1. Components are tolerant of web API evolution + 1. Component misuse of a web API results in failure at that call-site, not a link time error 4. Components have improved performance when calling web APIs compared to today ## Non-goals @@ -952,9 +952,9 @@ Which binding of the resolved module the component receives depends on the impor | Import type | JS equivalent | Value | |---|---|---| -| bare type, function, value | `import v from "JSSpecifier(|decl|)"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | -| instance | `import { a, b } from "JSSpecifier(|decl|)"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per untagged export of the instance type, named `JSName` of that export | -| core module, component | `import source M from "JSSpecifier(|decl|)"` | the module source, as a `Module` or `Component` | +| bare type, function, value | `import v from "JSSpecifier(decl)"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | +| instance | `import { a, b } from "JSSpecifier(decl)"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per untagged export of the instance type, named `JSName` of that export | +| core module, component | `import source M from "JSSpecifier(decl)"` | the module source, as a `Module` or `Component` | Reading the imports snapshots the resolved values, and so components cannot participate in cycles. This matches how core modules work today with ESM-integration. From 98ed653fba079ee86d61466e6ca5426e326faf6f Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Wed, 16 Sep 2026 20:12:26 -0500 Subject: [PATCH 7/7] [js-api] Use regexes instead of ad-hoc matching --- design/mvp/JS-Reference.md | 122 ++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md index 4dc1e569..4b3ba405 100644 --- a/design/mvp/JS-Reference.md +++ b/design/mvp/JS-Reference.md @@ -125,11 +125,19 @@ We define `PascalCase(label)` and `CamelCase(label)` below. | `URL` | `URL` | `url` | | `a1-2-3` | `A123` | `a123` | +Every `plainname` matches exactly one of the four patterns below, and no `interfacename` matches any of them. The algorithms in this document dispatch on these patterns and read their named captures. `