diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index b7e0e251..41ce7bcb 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -391,13 +391,14 @@ in the explainer.) ```ebnf import ::= in: ed: => (import in ed) export ::= en: si: ed?:? => (export en si ed?) -importname' ::= 0x00 len: in: => in (if len = |in|) - | 0x01 len: in: => in (if len = |in|) - | 0x02 len: in: vs: => in vs (if len = |in|) πŸ”— -exportname' ::= 0x00 len: in: => in (if len = |in|) - | 0x01 len: in: => in (if len = |in|) - | 0x02 len: in: vs: => in vs (if len = |in|) πŸ”— -versionsuffix ::= len: vs: => (versionsuffix vs) (if len = |vs|) πŸ”— +importname' ::= 0x00 len: in: => in (if len = |in|) + | 0x01 len: in: => in (if len = |in|) + | 0x02 len: in: opts:vec() => in opts (if len = |in|) 🏷️/πŸ”— +exportname' ::= 0x00 len: in: => in (if len = |in|) + | 0x01 len: in: => in (if len = |in|) + | 0x02 len: in: opts:vec() => in opts (if len = |in|) 🏷️/πŸ”— +nameopt ::= 0x00 len: n: => (implements i) 🏷️ + | 0x01 len: vs: => (versionsuffix vs) πŸ”— ``` Notes: @@ -415,11 +416,15 @@ Notes: release](##binary-format-warts-to-fix-in-a-10-release). * The ``s of a component must all be [strongly-unique]. Separately, the ``s of a component must also all be [strongly-unique]. -* Validation requires that annotated `plainname`s only occur on `func` imports - or exports and that the first label of a `[constructor]`, `[method]` or - `[static]` matches the `plainname` of a preceding `resource` import or - export, respectively, in the same scope (component, component type or - instance type). +* Validation requires that `[constructor]`, `[method]` and `[static]` annotated + `plainname`s only occur on `func` imports or exports and that the first label + of a `[constructor]`, `[method]` or `[static]` matches the `plainname` of a + preceding `resource` import or export, respectively, in the same scope + (component, component type or instance type). +* 🏷️ Validation requires that `implements`-annotated imports or exports are + `instance`-typed. +* 🏷️ Validation requires that `implements`-annotated imports or exports have a + `` name. * Validation of `[constructor]` names requires a `func` type whose result type is either `(own $R)` or `(result (own $R) E?)` where `$R` is a resource type labeled `r`. diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 38fdb224..16499da2 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -59,6 +59,7 @@ implemented, considered stable and included in a future milestone: * πŸ”—: canonical interface names * 🐘: [memory64] * πŸ—ΊοΈ: the `map` type +* 🏷️: `implements` annotations for plain-named interface imports/exports (Based on the previous [scoping and layering] proposal to the WebAssembly CG, this repo merges and supersedes the [module-linking] and [interface-types] @@ -2681,6 +2682,31 @@ annotations trigger additional type-validation rules (listed in * Similarly, an import or export named `[method]R.foo` must be a function whose first parameter must be `(param "self" (borrow $R))`. +🏷️ When an instance import or export is named `L` and annotated with +`(implements "I")`, it indicates that the instance implements interface `I` but +is given the plain name `L`. This enables a component to import or export the +same interface multiple times with different plain names. For example: + +```wat +(component + (import "primary" (implements "wasi:keyvalue/store") (instance ...)) + (import "secondary" (implements "wasi:keyvalue/store") (instance ...)) +) +``` + +Here, both imports implement `wasi:keyvalue/store` but have distinct plain +names `primary` and `secondary`. Bindings generators can use the +`implements` annotation to know which interface the instance implements, +enabling them to share value type bindings across both imports. (Note that +resource types defined in the interface, such as `bucket`, are treated as +distinct for each import, since each may have a different implementation.) + +The `interfacename` also helps hosts and clients of a component. A host that +sees `(implements "wasi:keyvalue/store>")` knows to supply a +`wasi:keyvalue/store` implementation for that import, even though the import +name is something else. Similarly, a client composing components can use the +annotation to match compatible imports and exports across components. + When a function's type is `async`, bindings generators are expected to emit whatever asynchronous language construct is appropriate (such as an `async` function in JS, Python or Rust). See the [concurrency explainer] for @@ -2822,11 +2848,11 @@ Values]) are **strongly-unique**: * Strip any `[...]` annotation prefix from both names. * The names are strongly-unique if the resulting strings are unequal. -Thus, the following names are strongly-unique: -* `foo`, `foo-bar`, `[constructor]foo`, `[method]foo.bar`, `[method]foo.baz` +Thus, the following set of names are strongly-unique and can thus all be imports (or exports) of the same component (or component type or instance type): +* `foo`, `foo-bar`, `[constructor]foo`, `[method]foo.bar`, `[method]foo.baz`, `foo:bar/baz` but attempting to add *any* of the following names would be a validation error: -* `foo`, `foo-BAR`, `[constructor]foo-BAR`, `[method]foo.foo`, `[method]foo.BAR` +* `foo`, `foo-BAR`, `[constructor]foo-BAR`, `[method]foo.foo`, `[method]foo.BAR`, `foo:bar/baz`, `bar` Note that additional validation rules involving types apply to names with annotations. For example, the validation rules for `[constructor]foo` require diff --git a/design/mvp/WIT.md b/design/mvp/WIT.md index 18ce3d50..2a4934aa 100644 --- a/design/mvp/WIT.md +++ b/design/mvp/WIT.md @@ -25,7 +25,7 @@ document, a pseudo-formal [grammar specification][lexical-structure], and additionally a specification of the [package format][package-format] of a WIT package suitable for distribution. -See [Gated Features] for an explanation of πŸ”§. +See [Gated Features] for an explanation of πŸ”§ and 🏷️. [IDL]: https://en.wikipedia.org/wiki/Interface_description_language [components]: https://github.com/webassembly/component-model @@ -366,6 +366,57 @@ world union-my-world-b { } ``` +🏷️ When a world being included contains plain-named imports or exports that +reference a named interface (using the `id: use-path` syntax), the `with` +keyword renames the plain-name label while preserving the underlying +`(implements "I")` annotation in the encoding. For example: + +```wit +package local:demo; + +interface store { + get: func(key: string) -> option; +} + +world base { + import cache: store; +} + +world extended { + import cache: func(); + include base with { cache as my-cache } +} +``` + +In this case, `extended` requires a `with` during its `include` because +the plain-name `cache` is already taken in the world. The import here is renamed +to `my-cache` that implements `local:demo/store`, equivalent to writing +`import my-cache: store;` directly. + +Unlike interface names (which are automatically de-duplicated when two +`include`s import the same interface), plain names cannot be de-duplicated +and will conflict. For example: + +```wit +world base-a { + import cache: store; +} + +world base-b { + import cache: store; +} + +world conflict { + include base-a; + include base-b; // error: plain name 'cache' conflicts +} + +world resolved { + include base-a; + include base-b with { cache as other-cache } // ok: renamed to avoid conflict +} +``` + `with` cannot be used to rename interface names, however, so the following world would be invalid: ```wit @@ -1382,7 +1433,41 @@ export-item ::= 'export' id ':' extern-type import-item ::= 'import' id ':' extern-type | 'import' use-path ';' -extern-type ::= func-type ';' | 'interface' '{' interface-items* '}' +extern-type ::= func-type ';' + | 'interface' '{' interface-items* '}' + | use-path ';' 🏷️ +``` + +🏷️ The third case of `extern-type` allows a named interface to be imported or +exported with a custom [plain name]. For example: + +```wit +world my-world { + import primary: wasi:keyvalue/store; + import secondary: wasi:keyvalue/store; + export my-handler: wasi:http/handler; +} +``` + +Here, `primary` and `secondary` are two distinct imports that both have the +instance type of the `wasi:keyvalue/store` interface. The plain name of the +import is the `id` before the colon (e.g., `primary`), not the interface name. +This contrasts with `import wasi:keyvalue/store;` (without the `id :` prefix), +which would create a single import using the full interface name +`wasi:keyvalue/store`. Similarly, the export `my-handler` has the instance type +of `wasi:http/handler` but uses the plain name `my-handler` instead of the full +interface name, which is useful when a component wants to export the same +interface multiple times or simply use a more descriptive name. + +Note that the `use-path` form can have an ambiguity with the nested packages +feature (πŸͺΊ) where `a:b` could mean two things. To resolve this `a:b` is lexed +as a single token instead of separate tokens, meaning: + +```wit +world w { + import a:b; // error: can't import a package + import a: b; // ok, assuming `b` names an interface in scope +} ``` Note that worlds can import types and define their own types to be exported @@ -2073,6 +2158,129 @@ This duplication is useful in the case of cross-package references or split packages, allowing a compiled `world` definition to be fully self-contained and able to be used to compile a component without additional type information. +🏷️ When a world imports or exports a named interface with a custom plain name +(using the `id: use-path` syntax), the encoding uses the `(implements "I")` +annotation defined in [Explainer.md](Explainer.md#import-and-export-definitions) to indicate which +interface the instance implements. Note though that each copy implements a +unique version of the interface in question. For example, the following WIT: + +```wit +package local:demo; + +interface store { + resource bucket { + constructor(name: string); + get: func(key: string) -> option; + } +} + +world w { + import one: store; + import two: store; +} +``` + +is encoded as: + +```wat +(component + (type (export "store") (component + (export "local:demo/store" (instance + (export "bucket" (type $b (sub resource))) + (export "[constructor]bucket" (func (param "name" string) (result (own $b)))) + (export "[method]bucket.get" (func (param "self" (borrow $b)) (param "key" string) (result (option string)))) + )) + )) + (type (export "w") (component + (export "local:demo/w" (component + (import "one" (implements "local:demo/store") (instance + (export "bucket" (type $b (sub resource))) + (export "[constructor]bucket" (func (param "name" string) (result (own $b)))) + (export "[method]bucket.get" (func (param "self" (borrow $b)) (param "key" string) (result (option string)))) + )) + (import "two" (implements "local:demo/store") (instance + (export "bucket" (type $b (sub resource))) + (export "[constructor]bucket" (func (param "name" string) (result (own $b)))) + (export "[method]bucket.get" (func (param "self" (borrow $b)) (param "key" string) (result (option string)))) + )) + )) + )) +) +``` + +The `(implements "local:demo/store")` prefix tells bindings generators and +toolchains which interface each plain-named instance import implements, while +the labels `one` and `two` provide distinct plain names. This is a case of +the general `(implements ..)` pattern described in +[Explainer.md](Explainer.md#import-and-export-definitions). Also note here that +two copies of the `"bucket"` resource are imported for the `local:demo/w` world. +This is because the interfaces `one` and `two` duplicate the `store` interface. +Note that this can import just a single `bucket` resource by extracting out the +resource definition into a separate interface. For example: + +```wit +package local:demo; + +interface types { + resource bucket { + get: func(key: string) -> option; + } +} + +interface store { + use types.{bucket}; + open: func(name: string) -> bucket; +} + +world w { + import one: store; + import two: store; +} +``` + +is encoded as: + +```wat +(component + (type (export "types") (component + (export "local:demo/types" (instance + (export "bucket" (type $b (sub resource))) + (export "[method]bucket.get" (func (param "self" (borrow $b)) (param "key" string) (result (option string)))) + )) + )) + (type (export "store") (component + (import "local:demo/types" (instance $types + (export "bucket" (type $b (sub resource))) + )) + (alias export $types "bucket" (type $b)) + (export "local:demo/store" (instance + (export "bucket" (type $b' (eq $b))) + (export "open" (func (param "name" string) (result (own $b')))) + )) + )) + (type (export "w") (component + (export "local:demo/w" (component + (import "local:demo/types" (instance $types + (export "bucket" (type $b (sub resource))) + )) + (alias export $types "bucket" (type $b)) + (import "one" (implements "local:demo/store") (instance + (export "bucket" (type $b' (eq $b))) + (export "open" (func (param "name" string) (result (own $b')))) + )) + (import "two" (implements "local:demo/store") (instance + (export "bucket" (type $b' (eq $b))) + (export "open" (func (param "name" string) (result (own $b')))) + )) + )) + )) +) +``` + +Where in this example the `local:demo/w` world imports only a single `bucket` +resource under the `local:demo/types` interface. This resource is then used +by the `one` and `two` export. + Putting this all together, the following WIT definitions: ```wit diff --git a/test/validation/implements.wast b/test/validation/implements.wast new file mode 100644 index 00000000..7df0024c --- /dev/null +++ b/test/validation/implements.wast @@ -0,0 +1,108 @@ +;; valid usages of `implements` +(component + (component + (import "a" (implements "a:b/c") (instance)) + (import "b" (implements "a:b/c") (instance)) + (import "c" (implements "a:b/c@1.0.0") (instance)) + (import "my-label" (implements "ns:pkg/iface") (instance)) + (import "a:b/c" (instance)) + (import "a:b/c@1.0.0" (instance)) + + (instance $a) + + (export "a" (implements "a:b/c") (instance $a)) + (export "b" (implements "a:b/c") (instance $a)) + (export "c" (implements "a:b/c@1.0.0") (instance $a)) + (export "my-label" (implements "ns:pkg/iface") (instance $a)) + (export "a:b/c" (instance $a)) + (export "a:b/c@1.0.0" (instance $a)) + ) + + (type (instance + (export "a" (implements "a:b/c") (instance)) + )) + (type (component + (import "a" (implements "a:b/c") (instance)) + (export "a" (implements "a:b/c") (instance)) + )) + + (instance $a) + (instance + (export "a" (implements "a:b/c") (instance $a)) + ) +) + +;; invalid, `not-valid` should be something like `a:b/c` +(assert_invalid + (component (import "a" (implements "not-valid") (instance))) + "must be an interface") + +;; invalid, `""` must be a valid kebab-name. +(assert_invalid + (component (import "a" (implements "") (instance))) + "not a valid name") + +;; invalid, can't import the same plain-name twice. +(assert_invalid + (component + (import "a" (implements "a:b/c") (instance)) + (import "a" (implements "a:b/c") (instance)) + ) + "conflicts with previous name") + +;; invalid, can't import the same plain-name twice. +(assert_invalid + (component + (import "a" (implements "a1:b/c") (instance)) + (import "a" (implements "a2:b/c") (instance)) + ) + "conflicts with previous name") + +;; invalid, can't import the same plain-name twice. +(assert_invalid + (component + (import "a" (implements "a:b/c") (instance)) + (import "a" (implements "a:b/c@1.0.0") (instance)) + ) + "conflicts with previous name") + +;; invalid, can't import the same plain-name twice. +(assert_invalid + (component + (import "a" (instance)) + (import "a" (implements "a:b/c") (instance)) + ) + "conflicts with previous name") + +;; invalid, `implements` can only be used by imports/exports typed as `instance` +(assert_invalid + (component + (import "a" (implements "a:b/c") (func)) + ) + "only instance names can have an `implements`") + +;; invalid, `implements` can only be used by imports/exports named with a +;; plainname. +(assert_invalid + (component + (import "a1:b/c" (implements "a2:b/c") (instance)) + ) + "name `a1:b/c` is not valid with `implements`") + +;; validity checks apply to other locations of `implements`, such as +;; component/instance types and bag-of-exports. +(assert_invalid + (component (type (component (import "a" (implements "not-valid") (instance))))) + "must be an interface") +(assert_invalid + (component (type (component (export "a" (implements "") (instance))))) + "not a valid name") +(assert_invalid + (component (type (instance (export "a" (implements "a:b/c") (func))))) + "only instance names") +(assert_invalid + (component + (instance) + (instance (export "x" (implements "a") (instance 0))) + ) + "must be an interface")