Skip to content

Add (implements "I") to plainname imports to allow multiple imports#613

Open
ricochet wants to merge 8 commits into
mainfrom
implements
Open

Add (implements "I") to plainname imports to allow multiple imports#613
ricochet wants to merge 8 commits into
mainfrom
implements

Conversation

@ricochet
Copy link
Copy Markdown
Contributor

@ricochet ricochet commented Feb 25, 2026

Add (implements "I") annotation to be added to plainname imports which enables multiple imports of the same interface

Extend the WIT extern-type grammar to allow use-path as a third case,
enabling import id: use-path and export id: use-path to create
plain-named imports/exports whose instance type matches a named interface.

This allows importing the same interface multiple times under different
plain names (e.g., import primary: store; import secondary: store;),
encoded using the (implements "...") annotation pattern.

Fixes #287

Copy link
Copy Markdown
Member

@lukewagner lukewagner left a comment

Choose a reason for hiding this comment

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

Awesome, thanks for writing this up! The PR looks great; just minor nits. I think what'll be important for this change is seeing how it "plays out" in the producer and consumer tooling, so I think we'll want to keep this PR open until we get some experience from tool implementations, but so far so good from my perspective.

Comment thread design/mvp/Binary.md Outdated
Comment thread design/mvp/Explainer.md Outdated
Comment thread design/mvp/Explainer.md Outdated
Comment thread design/mvp/Explainer.md Outdated
Comment thread design/mvp/WIT.md Outdated
Comment thread design/mvp/WIT.md Outdated
Comment thread design/mvp/WIT.md Outdated
ricochet added a commit to ricochet/wasm-tools that referenced this pull request Feb 27, 2026
Add parsing, validation, and uniqueness rules for the new
`[implements=<interface>]label` extern name form from the component
model `implements` proposal. An implements name labels an instance
import/export that implements a named interface.

Uniqueness: `[implements=<I>]L` conflicts with bare label `L` and with
`[method]L.L` / `[static]L.L` (the existing l.l edge case), but is
strongly unique from interface names, constructors, and normal
method/static names.

See implements feature:
WebAssembly/component-model#613
Extend the WIT `extern-type` grammar to allow `use-path` as a third case,
enabling `import id: use-path` and `export id: use-path` to create
plain-named imports/exports whose instance type matches a named interface.

This allows importing the same interface multiple times under different
plain names (e.g., `import primary: store; import secondary: store;`),
encoded using the `[implements=<interfacename>]label` annotation pattern.

Fixes #287

Co-authored-by: Luke Wagner <mail@lukewagner.name>
ricochet added a commit to ricochet/wasmtime that referenced this pull request Feb 28, 2026
DO NOT MERGE until wasm-tools release with
bytecodealliance/wasm-tools#2453
Points wasm-tools to PR branch  `wasmparser-implements`

Add support for the component model `[implements=<I>]L`
(spec PR [bytecodealliance#613](WebAssembly/component-model#613)),
which allows components to import/export the same
interface multiple times under different plain names.

A component can import the same interface twice under different labels,
each bound to a distinct host implementation:

```wit
import primary: wasi:keyvalue/store;
import secondary: wasi:keyvalue/store;
```

Guest code sees two separate namespaces with identical shapes:

```rust
let val = primary::get("my-key");       // calls the primary store
let val = secondary::get("my-key");     // calls the secondary store
```

From the host, wit-bindgen generates a separate Host trait per label:

```rust
impl primary::Host for MyState {
    fn get(&mut self, key: String) -> String {
        self.primary_db.get(&key).cloned().unwrap_or_default()
    }
}

impl secondary::Host for MyState {
    fn get(&mut self, key: String) -> String {
        self.secondary_db.get(&key).cloned().unwrap_or_default()
    }
}

primary::add_to_linker(&mut linker, |state| state)?;
secondary::add_to_linker(&mut linker, |state| state)?;
```

The linker also supports registering by plain label without knowing the annotation:

```rust
// Component imports [implements=<wasi:keyvalue/store>]primary
// but the host just registers "primary" — label fallback handles it
linker.root().instance("primary")?.func_wrap("get", /* ... */)?;
```

Users can also register to the linker with the full encoded `implements` name

```rust
let mut linker = Linker::<()>::new(engine);

linker
    .root()
    .instance("[implements=<wasi:keyvalue/store>]primary")?
    .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?;
```

Semver matching works inside the implements annotation, just like regular interface imports:

```rust
// Host provides v1.0.1
linker
    .root()
    .instance("[implements=<wasi:keyvalue/store@1.0.1>]primary")?
    .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?;

// Component requests v1.0.0, matches via semver
let component = Component::new(&engine, r#"(component
    (type $store (instance
        (export "get" (func (param "key" string) (result string)))
    ))
    (import "[implements=<wasi:keyvalue/store@1.0.0>]primary" (instance (type $store)))
)"#)?;
linker.instantiate(&mut store, &component)?; // works, 1.0.1 is semver-compatible with 1.0.0
```

## Changes

### Runtime name resolution

- Add three-tier lookup in NameMap::get: exact → semver → label fallback
- Add implements_label_key() helper for extracting plain labels from `[implements=<I>]L`
- Add unit tests for all lookup tiers

### Code generation for multi-import/export

- Track first-seen implements imports/exports per `InterfaceId`
- Duplicate imports: re-export types via `pub use super::{first}::*`,
  generate fresh Host trait + add_to_linker
- Duplicate exports: same pattern with fresh Guest/GuestIndices,
  plus regenerate resource wrapper structs to reference the local Guest type
- Use `name_world_key_with_item` for export instance name lookups
- Guard `populate_world_and_interface_options` with `entry()` to avoid
  overwriting link options for duplicate interfaces
ricochet added a commit to ricochet/wasmtime that referenced this pull request Mar 1, 2026
DO NOT MERGE until wasm-tools release with
bytecodealliance/wasm-tools#2453
Points wasm-tools to PR branch  `wasmparser-implements`

Add support for the component model `[implements=<I>]L`
(spec PR [bytecodealliance#613](WebAssembly/component-model#613)),
which allows components to import/export the same
interface multiple times under different plain names.

A component can import the same interface twice under different labels,
each bound to a distinct host implementation:

```wit
import primary: wasi:keyvalue/store;
import secondary: wasi:keyvalue/store;
```

Guest code sees two separate namespaces with identical shapes:

```rust
let val = primary::get("my-key");       // calls the primary store
let val = secondary::get("my-key");     // calls the secondary store
```

Host Import-side codegen: shared trait + label-parameterized add_to_linker

For imports, wit-bindgen generates one Host trait per interface (not per
label). The add_to_linker function takes a name: &str parameter so the
same trait implementation can be registered under different instance labels.
Duplicate implements imports don't generate separate modules — only the
first import produces bindings.

```rust
struct PrimaryBackend;
impl primary::Host for PrimaryBackend {
    fn get(&mut self, key: String) -> String {
        self.primary_db.get(&key).cloned().unwrap_or_default()
    }
}

struct SecondaryBackend;
impl primary::Host for SecondaryBackend {
    fn get(&mut self, key: String) -> String {
        self.secondary_db.get(&key).cloned().unwrap_or_default()
    }
}

// Same add_to_linker, different labels and host_getter closures
primary::add_to_linker(&mut linker, "primary", |s| &mut s.primary)?;
primary::add_to_linker(&mut linker, "secondary", |s| &mut s.secondary)?;
```

Export-side codegen: per-label modules with shared types

For exports, each label gets its own module with fresh Guest/GuestIndices
types but re-exports shared interface types from the first module via
`pub use super::{first}::*`.

Runtime name resolution

The linker supports registering by plain label without knowing the annotation:

```rust
// Component imports [implements=<wasi:keyvalue/store>]primary
// but the host just registers "primary" — label fallback handles it
linker.root().instance("primary")?.func_wrap("get", /* ... */)?;

Users can also register to the linker with the full encoded implements name:

linker
    .root()
    .instance("[implements=<wasi:keyvalue/store>]primary")?
    .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?;
```

Semver matching works inside the implements annotation, just like
regular interface imports:

```rust
// Host provides v1.0.1
linker
    .root()
    .instance("[implements=<wasi:keyvalue/store@1.0.1>]primary")?
    .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?;

// Component requests v1.0.0, matches via semver
let component = Component::new(&engine, r#"(component
    (type $store (instance
        (export "get" (func (param "key" string) (result string)))
    ))
    (import "[implements=<wasi:keyvalue/store@1.0.0>]primary" (instance (type $store)))
)"#)?;
linker.instantiate(&mut store, &component)?; // works, 1.0.1 is semver-compatible with 1.0.0
```

- Add three-tier lookup in NameMap::get: exact → semver → label fallback
- Add implements_label_key() helper for extracting plain labels from
  `[implements=<I>]L`
- Add unit tests for all lookup tiers

- Track first-seen implements imports per `InterfaceId`
- One `Host` trait per interface; `generate_add_to_linker` takes
  `named: bool` — when true, emits `name: &str` parameter instead of
  hardcoding the instance name
- Duplicate `implements` imports: just record the label in
  `implements_labels`, no module generation
- `world_add_to_linker`: iterate over `implements_labels` to emit one
  `add_to_linker` call per label, passing label as name argument
- Guard `populate_world_and_interface_options` with `entry()` to avoid
  overwriting link options for duplicate interfaces

- Duplicate exports: re-export types via `pub use super::{first}::*`,
  generate fresh `Guest`/`GuestIndices`, plus regenerate resource wrapper
  structs to reference the local `Guest` type
- Use `name_world_key_with_item` for export instance name lookups
Copy link
Copy Markdown
Member

@lukewagner lukewagner left a comment

Choose a reason for hiding this comment

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

Thanks!

Comment thread test/wasm-tools/implements.wast Outdated
Comment thread test/wasm-tools/implements.wast Outdated
@lukewagner
Copy link
Copy Markdown
Member

Oh, I forgot: we probably should have an emoji-gate for this associated with the new rules in the grammar in Explainer.md and the descriptions in WIT.md.

Comment thread design/mvp/WIT.md
@ricochet
Copy link
Copy Markdown
Contributor Author

ricochet commented Mar 3, 2026

Oh, I forgot: we probably should have an emoji-gate for this associated with the new rules in the grammar in Explainer.md and the descriptions in WIT.md.

Since this is about adding labels, let's use 🏷️ aka the :label: emoji

Comment thread design/mvp/WIT.md Outdated
package local:demo;

interface store {
get: func(key: string) -> option<string>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized that there's a pretty nuanced technical design point (that iiuc the current impl is getting getting right atm) that this example should explicate via the generated WAT:

Could you expand this example to include a bucket resource type (returned by an open function, like in the current wasi-keyvalue draft) inside the existing store interface. That should end up with two resource types being imported in the WAT.

Next, after the WAT, could you add a second variation of this example which factors out the bucket type into a types interface (like we do in wasi-http) that is used by the store interface. And this should result in one resource type import that is aliased into the two store imports.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Done now, if you'd like to review

Comment thread design/mvp/WIT.md
Comment thread design/mvp/Explainer.md Outdated
* Use a dedicated `(implements "...")` annotation in the text format and
  dedicated bytes in the binary format.
* Update tests/docs/etc to use the new syntax.
@alexcrichton alexcrichton changed the title Add [implements=<I>]L plainname for multiple imports Add (implement "I") plainname for multiple imports May 12, 2026
@alexcrichton alexcrichton changed the title Add (implement "I") plainname for multiple imports Add (implement "I") to plainname imports to allow multiple imports May 12, 2026
@alexcrichton alexcrichton changed the title Add (implement "I") to plainname imports to allow multiple imports Add (implements "I") to plainname imports to allow multiple imports May 12, 2026
@alexcrichton
Copy link
Copy Markdown
Collaborator

Ok I've gone through this PR and updated everything to the best of my ability. This update notably takes into account Luke's previous comment which overhauls how this is encoded in the binary format. I've additionally updated the sibling wasm-tools PR through which I've validated and verified some of the wat and tests here. I've additionally implemented all the various bits of wasm-tools such that fuzzing is now clean and such.

My hope/expectation is that integrating this into bindings generators will be pretty easy, assuming the generate_nominal_type_ids method is used. One of the major features that this exposes, which never came up prior, is importing the same interface multiple times and exporting multiple times. I realize that sounds obvious but for any bindings generator which has been using TypeId or InterfaceId as keys in maps this can wreak havoc without the "nominal" form of a graph. Effectively this, in my opinion at least, really puts a nail in the coffin of bindings generators taking a WIT AST as-is and generating bindings from it. Essentially it's required to post-process into a format more akin to the actual wasm representation (e.g. interfaces are duplicated, as well as types defined in those interfaces). This is all the purpose of generate_nominal_type_ids, which notably got an update for this feature as well.

Copy link
Copy Markdown
Member

@lukewagner lukewagner left a comment

Choose a reason for hiding this comment

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

Looks great, thanks! One minor suggestion:

Comment thread design/mvp/Binary.md Outdated
pnodet pushed a commit to pnodet/wasm-tools that referenced this pull request May 14, 2026
* [wasmparser] Add `[implements=<I>]L` component name support

Add parsing, validation, and uniqueness rules for the new
`[implements=<interface>]label` extern name form from the component
model `implements` proposal. An implements name labels an instance
import/export that implements a named interface.

Uniqueness: `[implements=<I>]L` conflicts with bare label `L` and with
`[method]L.L` / `[static]L.L` (the existing l.l edge case), but is
strongly unique from interface names, constructors, and normal
method/static names.

See implements feature:
WebAssembly/component-model#613

* [wit-parser] Add `implements` syntax for named interface imports/exports

Support the `import label: iface;` and `export label: iface;` WIT
syntax, which encodes as `[implements=<I>]label` in the binary format.

This allows importing or exporting the same interface multiple times
under different names.

Changes include:
  - Add `implements: Option<InterfaceId>` field to  `WorldItem::Interface`
  - Parse `NamedPath` variant in the WIT AST with disambiguation against
    fully-qualified `namespace:package/interface` paths
  - Decode `[implements=<I>]label` names in all binary decoding paths
    via a new `decode_world_instance` helper
  - Thread `implements` through world elaboration and ID remapping
  - Add `name_world_key_with_item` for binary encoding

On the API change for `name_world_key_with_item`, I opted to introduce
this new fn that is used only at the few call sites that need it vs updating
the ~50 sites for name_world_key.

* [wit-component] Add implements encoding support to wit-component

Update component encoding to use `name_world_key_with_item` at sites
that produce component-level extern names, so that `implements`
imports
and exports are encoded as `[implements=<I>]L` in the binary format.

Five call sites are changed from `name_world_key` to the
implements-aware variant: import_map key construction, component export
names, ImportedResourceDrop lookups, and both direct and indirect
InterfaceFunc lookups.

* [wit-dylib] add implements to WorldItem::Interface

* [wit-smith] Add implements interface generation

Teach wit-smith to generate `ImplementsInterface` items in worlds,
producing `%label: path;` WIT syntax which encodes as
`[implements=<I>]L` in the component binary. This enables fuzzing of
the implements feature through the existing roundtrip_wit fuzzer.

* Generate nominal interface/type ids in `wit-component`

This commit is a long-overdue change in the componentization process in
`wit-component` to ensure that when a component is generated there are
unique `TypeId` and `InterfaceId` entries corresponding to all
types/interfaces which map to the final component. This is distinct from
the structure of WIT where the same `InterfaceId`, for example, can be
both imported and exported. When generating a component this means that
two distinct interfaces are imported/exported. Previously
`wit-component` has had a lot of very carefully filled out and handled
maps and such to ensure everything works, and this should make things
significantly easier because the possibility over overlap is now
nonexistent.

* Leverage nominal types/interfaces in encoding

This commit builds on the previous commit to simplify internal
structures within the encoding process of a component. Notably there's
no longer any need to have separate maps for imports/exports and instead
everything is located in a single set of maps. Not major changes, but
this is hoped to unlock future changes and additionally pave the way to
simplifying some internals in the future.

* Add `cm-implements` feature

Gate the new parsing behind this feature.

* Shift where `implements` is in the AST

Use a new `WorldKey` instead of `WorldItem`.

* Remove hand-rolled parsing

* Only write blessed files if they change

Helps run-on-file-change tools not infinite loop

* Reimplement how `implements` is represented

In the component model binary format this is no longer packed into
import/export names but is instead an auxiliary piece of metadata on
imports/exports. This required quite a few updates to parsing, AST
structures, etc, throughout. Additionally within `wit-parser` this is
now represented with no AST changes from before. Instead
`WorldKey::Name` is used with a `WorldItem::Interface`, and the only
difference from the previous "anonymous interfaces" is that the
interface pointed to has a name. This is what's used to indicate an
`implements` value is desired.

Initial plumbing within `wit-component` is done, but tests are not fully
passing yet.

* Handle `import a: b` in `generate_nominal_type_ids`

This commit updates the `generate_nominal_type_ids` to handle the new
form of imports that are showing up with the `implements` form. This is
going to be necessary for bindings generators to handle this neatly and
this is also required for wit-component after bytecodealliance#2516

* Fix recording stability of named interfaces

* Fix the order nominalization happens in

* Use the 2024 edition for rustfmt in this workspace

Possible now that MSRV is high enough

* Fix configured build

* Reduce the diff with `main`

* Improve ergonomics of wasm-encoder

* Undo another interim change

* More diff minimizing

* Add failing test

* Don't permute imports/exports

Add a test case that broke in Wasmtime, and add a lot of words for why
things are the way they are.

* Update binary encoding

* Review comments

---------

Co-authored-by: Alex Crichton <alex@alexcrichton.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support multiple imports of the same interface with different names

4 participants