Skip to content

Interface version canonicalization #536

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ rules, but rather merge the minimal need-to-know elements of both, with just
enough detail to create a prototype. A complete definition of the binary format
and validation will be present in the [formal specification](../../spec/).

See [Gated Features](Explainer.md#gated-features) for an explanation of 🪙 and 🔧.
See [Gated Features](Explainer.md#gated-features) for an explanation of emoji
annotations like 🪙 and 🔧.


## Component Definitions
Expand Down Expand Up @@ -371,10 +372,13 @@ flags are set.
(See [Import and Export Definitions](Explainer.md#import-and-export-definitions)
in the explainer.)
```ebnf
import ::= in:<importname'> ed:<externdesc> => (import in ed)
export ::= en:<exportname'> si:<sortidx> ed?:<externdesc>? => (export en si ed?)
importname' ::= 0x00 len:<u32> in:<importname> => in (if len = |in|)
exportname' ::= 0x00 len:<u32> en:<exportname> => en (if len = |en|)
import ::= in:<importname'> ed:<externdesc> => (import in ed)
export ::= en:<exportname'> si:<sortidx> ed?:<externdesc>? => (export en si ed?)
importname' ::= 0x00 len:<u32> in:<importname> => in (if len = |in|)
| 0x01 len:<u32> in:<importname> vs:<versionsuffix'> => in vs (if len = |in|) 🔗
exportname' ::= 0x00 len:<u32> en:<exportname> => en (if len = |en|)
| 0x01 len:<u32> en:<exportname> vs:<versionsuffix'> => in vs (if len = |in|) 🔗
versionsuffix' ::= len:<u32> vs:<semversuffix> => (versionsuffix vs) (if len = |vs|) 🔗
```

Notes:
Expand All @@ -399,7 +403,11 @@ Notes:
`(result (own $R))`, where `$R` is the resource labeled `r`.
* Validation of `[method]` names requires the first parameter of the function
to be `(param "self" (borrow $R))`, where `$R` is the resource labeled `r`.
* `<valid semver>` is as defined by [https://semver.org](https://semver.org/)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<valid semver> wasn't referenced in this file.

* 🔗 Validation requires that `versionsuffix` is preceded by an `interfaceversion`
matching `canonversion` and that the concatenation of the `canonversion` and
the `versionsuffix` results in a `valid semver` as defined by
[https://semver.org](https://semver.org/). A `versionsuffix` is otherwise
ignored for validation except to improve diagnostic messages.
* `<integrity-metadata>` is as defined by the
[SRI](https://www.w3.org/TR/SRI/#dfn-integrity-metadata) spec.

Expand Down Expand Up @@ -494,7 +502,9 @@ named once.

* The opcodes (for types, canon built-ins, etc) should be re-sorted
* The two `list` type codes should be merged into one with an optional immediate.
* The `0x00` prefix byte of `importname'` and `exportname'` will be removed or repurposed.
* The `0x00` variant of `importname'` and `exportname'` will be removed. Any
remaining variant(s) will be renumbered or the prefix byte will be removed or
repurposed.


[`core:byte`]: https://webassembly.github.io/spec/core/binary/values.html#binary-byte
Expand Down
162 changes: 115 additions & 47 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ more user-focused explanation, take a look at the
* [Start definitions](#-start-definitions)
* [Import and export definitions](#import-and-export-definitions)
* [Name uniqueness](#name-uniqueness)
* [Canonical interface name](#-canonical-interface-name)
* [Component invariants](#component-invariants)
* [JavaScript embedding](#JavaScript-embedding)
* [JS API](#JS-API)
Expand All @@ -55,6 +56,7 @@ implemented, considered stable and included in a future milestone:
* 🧵: threading built-ins
* 🔧: fixed-length lists
* 📝: the `error-context` type
* 🔗: canonical interface names

(Based on the previous [scoping and layering] proposal to the WebAssembly CG,
this repo merges and supersedes the [module-linking] and [interface-types]
Expand Down Expand Up @@ -294,7 +296,8 @@ sort ::= core <core:sort>
| type
| component
| instance
inlineexport ::= (export <exportname> <sortidx>)
inlineexport ::= (export "<exportname>" <sortidx>)
| (export "<exportname>" <versionsuffix> <sortidx>) 🔗
```
Because component-level function, type and instance definitions are different
than core-level function, type and instance definitions, they are put into
Expand Down Expand Up @@ -574,8 +577,10 @@ instancedecl ::= core-prefix(<core:type>)
| <alias>
| <exportdecl>
| <value> 🪙
importdecl ::= (import <importname> bind-id(<externdesc>))
exportdecl ::= (export <exportname> bind-id(<externdesc>))
importdecl ::= (import "<importname>" bind-id(<externdesc>))
| (import "<importname>" <versionsuffix> bind-id(<externdesc>)) 🔗
exportdecl ::= (export "<exportname>" bind-id(<externdesc>))
| (export "<exportname>" <versionsuffix> bind-id(<externdesc>)) 🔗
externdesc ::= (<sort> (type <u32>) )
| core-prefix(<core:moduletype>)
| <functype>
Expand Down Expand Up @@ -988,6 +993,10 @@ and `$C1` is a subtype of `$C2`:
)
```

🔗 Note that [canonical interface names](#-canonical-interface-name) may be
annotated with a `versionsuffix` which is ignored for type checking except to
improve diagnostic messages.

When we next consider type imports and exports, there are two distinct
subcases of `typebound` to consider: `eq` and `sub`.

Expand Down Expand Up @@ -2242,8 +2251,11 @@ the identifier `$x`). In the case of exports, the `<id>?` right after the
preceding definition being exported (e.g., `(export $x "x" (func $f))` binds a
new identifier `$x`).
```ebnf
import ::= (import "<importname>" bind-id(<externdesc>))
export ::= (export <id>? "<exportname>" <sortidx> <externdesc>?)
import ::= (import "<importname>" bind-id(<externdesc>))
| (import "<importname>" <versionsuffix> bind-id(<externdesc>)) 🔗
export ::= (export <id>? "<exportname>" <sortidx> <externdesc>?)
| (export <id>? "<exportname>" <versionsuffix> <sortidx> <externdesc>?) 🔗
versionsuffix ::= (versionsuffix "<semversuffix>") 🔗
```
All import names are required to be [strongly-unique]. Separately, all export
names are also required to be [strongly-unique]. The rest of the grammar for
Expand All @@ -2257,47 +2269,53 @@ interpreted as a *lexical* grammar defining a single token and thus whitespace
is not automatically inserted, all terminals are single-quoted, and everything
unquoted is a meta-character.
```ebnf
exportname ::= <plainname>
| <interfacename>
importname ::= <exportname>
| <depname>
| <urlname>
| <hashname>
plainname ::= <label>
| '[async]' <label> 🔀
| '[constructor]' <label>
| '[method]' <label> '.' <label>
| '[async method]' <label> '.' <label> 🔀
| '[static]' <label> '.' <label>
| '[async static]' <label> '.' <label> 🔀
label ::= <fragment>
| <label> '-' <fragment>
fragment ::= <word>
| <acronym>
word ::= [a-z] [0-9a-z]*
acronym ::= [A-Z] [0-9A-Z]*
interfacename ::= <namespace> <label> <projection> <version>?
| <namespace>+ <label> <projection>+ <version>? 🪺
namespace ::= <words> ':'
words ::= <word>
| <words> '-' <word>
projection ::= '/' <label>
version ::= '@' <valid semver>
depname ::= 'unlocked-dep=<' <pkgnamequery> '>'
| 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
pkgnamequery ::= <pkgpath> <verrange>?
pkgname ::= <pkgpath> <version>?
pkgpath ::= <namespace> <words>
| <namespace>+ <words> <projection>* 🪺
verrange ::= '@*'
| '@{' <verlower> '}'
| '@{' <verupper> '}'
| '@{' <verlower> ' ' <verupper> '}'
verlower ::= '>=' <valid semver>
verupper ::= '<' <valid semver>
urlname ::= 'url=<' <nonbrackets> '>' (',' <hashname>)?
nonbrackets ::= [^<>]*
hashname ::= 'integrity=<' <integrity-metadata> '>'
exportname ::= <plainname>
| <interfacename>
importname ::= <exportname>
| <depname>
| <urlname>
| <hashname>
plainname ::= <label>
| '[async]' <label> 🔀
| '[constructor]' <label>
| '[method]' <label> '.' <label>
| '[async method]' <label> '.' <label> 🔀
| '[static]' <label> '.' <label>
| '[async static]' <label> '.' <label> 🔀
label ::= <fragment>
| <label> '-' <fragment>
fragment ::= <word>
| <acronym>
word ::= [a-z] [0-9a-z]*
acronym ::= [A-Z] [0-9A-Z]*
interfacename ::= <namespace> <label> <projection> <interfaceversion>?
| <namespace>+ <label> <projection>+ <interfaceversion>? 🪺
namespace ::= <words> ':'
words ::= <word>
| <words> '-' <word>
projection ::= '/' <label>
interfaceversion ::= '@' <valid semver>
| '@' <canonversion> 🔗
canonversion ::= [1-9] [0-9]* 🔗
| '0.' [1-9] [0-9]* 🔗
| '0.0.' [1-9] [0-9]* 🔗
semversuffix ::= [0-9A-Za-z.+-]* 🔗
depname ::= 'unlocked-dep=<' <pkgnamequery> '>'
| 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
pkgnamequery ::= <pkgpath> <verrange>?
pkgname ::= <pkgpath> <pkgversion>?
pkgversion ::= '@' <valid semver>
pkgpath ::= <namespace> <words>
| <namespace>+ <words> <projection>* 🪺
verrange ::= '@*'
| '@{' <verlower> '}'
| '@{' <verupper> '}'
| '@{' <verlower> ' ' <verupper> '}'
verlower ::= '>=' <valid semver>
verupper ::= '<' <valid semver>
urlname ::= 'url=<' <nonbrackets> '>' (',' <hashname>)?
nonbrackets ::= [^<>]*
hashname ::= 'integrity=<' <integrity-metadata> '>'
```
Components provide six options for naming imports:
* a **plain name** that leaves it up to the developer to "read the docs"
Expand Down Expand Up @@ -2372,7 +2390,9 @@ tooling as "registries":
parameter of [`WebAssembly.instantiate()`])

The `valid semver` production is as defined by the [Semantic Versioning 2.0]
spec and is meant to be interpreted according to that specification. The
spec and is meant to be interpreted according to that specification. The use of
`valid semver` in `interfaceversion` is temporary for backward compatibility;
see [Canonical interface name](#-canonical-interface-name) below (🔗). The
`verrange` production embeds a minimal subset of the syntax for version ranges
found in common package managers like `npm` and `cargo` and is meant to be
interpreted with the same [semantics][SemVerRange]. (Mostly this
Expand Down Expand Up @@ -2539,6 +2559,53 @@ annotations. For example, the validation rules for `[constructor]foo` require
for details.


### 🔗 Canonical Interface Name

An `interfacename` (as defined above) is **canonical** iff it either:

- has no `interfaceversion`
- has an `interfaceversion` matching the `canonversion` production

The purpose of `canonversion` is to simplify the matching of compatible import
and export versions. For example, if a guest imports some interface from
`wasi:http/[email protected]` and a host provides the (subtype-compatible) interface
`wasi:http/[email protected]`, we'd like to make it easy for the host to link with the
guest. The `canonversion` for both of these interfaces would be `0.2`, so this
linking could be done by matching canonical interface names literally.
Symmetrically, if a host provides `wasi:http/[email protected]` and a guest imports
`wasi:http/[email protected]`, so long as the guest only uses the subset of
functions defined in `wasi:http/[email protected]` (which is checked by normal
component type validation), linking succeeds. Thus, including only the
canonicalized version in the name allows both backwards and (limited)
forwards compatibility using only trivial string equality (as well as the
type checking already required).

Any `valid semver` (as used in WIT) can be canonicalized by splitting it into
two parts - the `canonversion` prefix and the remaining `semversuffix`. Using
the `<major>.<minor>.<patch>` syntax of [Semantic Versioning 2.0], the split
point is chosen as follows:

- if `major` > 0, split immediately after `major`
- `1.2.3` &rarr; `1` / `.2.3`
- otherwise if `minor` > 0, split immediately after `minor`
- `0.2.6-rc.1` &rarr; `0.2` / `.6-rc.1`
- otherwise, split immediately after `patch`
- `0.0.1-alpha` &rarr; `0.0.1` / `-alpha`

When a version is canonicalized, any `semversuffix` that was split off of the
version should be preserved in the `versionsuffix` field of any resulting
`import`s and `export`s. This gives component runtimes and other tools access to
the original version for error messages, documentation, and other development
purposes. Where a `versionsuffix` is present the preceding `interfacename` must
have a `canonversion`, and the concatenation of the `canonversion` and
`versionsuffix` must be a `valid semver`.

For compatibility with older versions of this spec, non-canonical
`interfacename`s (with `interfaceversion`s matching any `valid semver`) are
temporarily permitted. These non-canonical names may trigger warnings and will
start being rejected some time after after [WASI Preview 3] is released.


## Component Invariants

As a consequence of the shared-nothing design described above, all calls into
Expand Down Expand Up @@ -2894,6 +2961,7 @@ For some use-case-focused, worked examples, see:
[`rectype`]: https://webassembly.github.io/gc/core/text/types.html#text-rectype
[shared-everything-threads]: https://github.com/WebAssembly/shared-everything-threads
[WASI Preview 2]: https://github.com/WebAssembly/WASI/tree/main/wasip2#readme
[WASI Preview 3]: https://github.com/WebAssembly/WASI/tree/main/wasip2#looking-forward-to-preview-3
[reference types]: https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md

[Strongly-unique]: #name-uniqueness
Expand Down