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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
21 changes: 15 additions & 6 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,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 +402,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 +501,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
93 changes: 78 additions & 15 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 Down Expand Up @@ -294,7 +295,7 @@ sort ::= core <core:sort>
| type
| component
| instance
inlineexport ::= (export <exportname> <sortidx>)
inlineexport ::= (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 +575,8 @@ instancedecl ::= core-prefix(<core:type>)
| <alias>
| <exportdecl>
| <value> 🪙
importdecl ::= (import <importname> bind-id(<externdesc>))
exportdecl ::= (export <exportname> bind-id(<externdesc>))
importdecl ::= (import "<importname>" <versionsuffix>? bind-id(<externdesc>))
exportdecl ::= (export "<exportname>" <versionsuffix>? bind-id(<externdesc>))
externdesc ::= (<sort> (type <u32>) )
| core-prefix(<core:moduletype>)
| <functype>
Expand Down Expand Up @@ -988,6 +989,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 +2247,9 @@ 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>" <versionsuffix>? bind-id(<externdesc>))
export ::= (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 Down Expand Up @@ -2276,17 +2282,24 @@ fragment ::= <word>
| <acronym>
word ::= [a-z] [0-9a-z]*
acronym ::= [A-Z] [0-9A-Z]*
interfacename ::= <namespace> <label> <projection> <version>?
| <namespace>+ <label> <projection>+ <version>? 🪺
interfacename ::= <namespace> <label> <projection> <interfaceversion>?
| <namespace>+ <label> <projection>+ <interfaceversion>? 🪺
namespace ::= <words> ':'
words ::= <word>
| <words> '-' <word>
projection ::= '/' <label>
version ::= '@' <valid semver>
# FIXME: surrounding alignment
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO after final review

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.+-]*
Copy link
Contributor Author

@lann lann Jul 10, 2025

Choose a reason for hiding this comment

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

This just checks for valid semver characters. The "real" validation is covered in prose below, i.e. <canonversion><semversuffix> must match valid semver.

I had a version of this as [.+-][0-9A-Za-z.+-]+ which was less ambiguous but also implied that versionsuffix and the binary 0x01 variant couldn't be used with an empty suffix like you'd get with full version 0.0.1. Given that we're saying we'll remove non-canonical interface versions I think allowing the empty suffix will ultimately be simpler.

depname ::= 'unlocked-dep=<' <pkgnamequery> '>'
| 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
pkgnamequery ::= <pkgpath> <verrange>?
pkgname ::= <pkgpath> <version>?
pkgname ::= <pkgpath> <pkgversion>?
pkgversion ::= '@' <valid semver>
pkgpath ::= <namespace> <words>
| <namespace>+ <words> <projection>* 🪺
verrange ::= '@*'
Expand Down Expand Up @@ -2372,12 +2385,14 @@ 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
`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
interpretation is the usual SemVer-spec-defined ordering, but note the
particular behavior of pre-release tags.)
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 interpretation is the usual
SemVer-spec-defined ordering, but note the particular behavior of pre-release
tags.)

The `plainname` production captures several language-neutral syntactic hints
that allow bindings generators to produce more idiomatic bindings in their
Expand Down Expand Up @@ -2539,6 +2554,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 +2956,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