Skip to content

Add (implements "...") component name support#2453

Merged
alexcrichton merged 29 commits into
bytecodealliance:mainfrom
ricochet:wasmparser-implements
May 13, 2026
Merged

Add (implements "...") component name support#2453
alexcrichton merged 29 commits into
bytecodealliance:mainfrom
ricochet:wasmparser-implements

Conversation

@ricochet
Copy link
Copy Markdown
Contributor

@ricochet ricochet commented Feb 27, 2026

Add parsing, validation, and uniqueness rules for the new
(implements "...") extern name form from the component
model implements proposal. An implements name labels an instance
import/export that implements a named interface.

See implements feature:
WebAssembly/component-model#613

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
@ricochet ricochet requested a review from a team as a code owner February 27, 2026 20:45
@ricochet ricochet requested review from dicej and removed request for a team February 27, 2026 20:45
@ricochet ricochet marked this pull request as draft February 27, 2026 20:48
@ricochet ricochet force-pushed the wasmparser-implements branch from e875d9d to 6944e34 Compare February 28, 2026 20:02
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 ricochet force-pushed the wasmparser-implements branch 2 times, most recently from ce9d98b to 9334ce8 Compare March 1, 2026 00:12
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.
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.
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.
@ricochet ricochet force-pushed the wasmparser-implements branch from 3c7046d to 7c5a1ba Compare March 1, 2026 02:46
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
@alexcrichton alexcrichton requested review from alexcrichton and removed request for dicej March 2, 2026 18:27
Copy link
Copy Markdown
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

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

Thanks again for this! I've left some comments below, the most consequential one being about the AST structure of wit-parser itself. I'd like to still look more closely at some details once that's settled, but also wanted to give some initial feedback. I'll need to catch up on the wasmtime/wit-bindgen discussions too

Comment thread crates/wasmparser/src/validator/names.rs Outdated
Comment thread crates/wit-smith/src/generate.rs
Comment thread crates/wit-parser/src/lib.rs Outdated
Comment thread crates/wit-parser/src/decoding.rs
Comment thread crates/wit-parser/src/decoding.rs Outdated
@alexcrichton
Copy link
Copy Markdown
Member

Oh, another point -- I forgot this in my first pass so wanted to leave a note here as well -- can you add a new feature to crates/wasmparser/src/features.rs? We'll want to gate usage of this new syntax by default (aka require an opt-in in the Wasmtime CLI) while it's being implemented in various places.

@alexcrichton
Copy link
Copy Markdown
Member

@ricochet do you mind if I pick this up this week and work on getting this landed?

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.
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.
Gate the new parsing behind this feature.
Use a new `WorldKey` instead of `WorldItem`.
Helps run-on-file-change tools not infinite loop
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.
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
@alexcrichton alexcrichton force-pushed the wasmparser-implements branch from 9f8dde1 to cec17d3 Compare May 12, 2026 19:28
@alexcrichton alexcrichton changed the title [wasmparser] Add [implements=<I>]L component name support [wasmparser] Add (implements "...") component name support May 12, 2026
@alexcrichton alexcrichton marked this pull request as ready for review May 12, 2026 21:49
@alexcrichton
Copy link
Copy Markdown
Member

Ok I've performed major updates to this PR. Everything is now up-to-date, the history's a bit messy, but hey it's all there. This PR is coupled with updates to WebAssembly/component-model#613 as well. Major changes are:

  • The binary format is completely different. No more [implements=...] but instead it's (implement "...") as a separate binary form. This is done for future extensibility for @external-id("...").
  • The AST representation of everything subsequently changed quite a bit. For example type reflection now yields a ComponentItem instead of a ComponentEntity enum directly. The item form contains implements: Option<String> to carry this annotation.
  • The AST for wit-parser ended up actually reverting to what it was before this PR. That'll make consumer's lives much easier, hopefully. Notably the way this is encoded is that a WorldKey::Name'd thing points at a WorldItem::Interface value, and the interface has a name as opposed to not having a name.
  • All sorts of plumbing in all sorts of locations have been updated to account for this change. For example decoding/encoding the worlds from wasm works, wit-component support is now implemented for all forms, fuzzing works, etc. This, as always, took a few iterations and some more primitives to figure out the right approach, but I feel the end state is pretty good.
  • The generate_nominal_type_ids method is becoming more load bearing. It's updated for (implements "...") directives to duplicate the interface in question.

@alexcrichton
Copy link
Copy Markdown
Member

My plan is to perform a final once-over on review tomorrow, and then I'll ask for another sign-off from someone since so much of this is now self-authored by me. I've verified the fuzzer runs for ~1h locally finding no issues so I'm reasonably confident in the coverage and feature. After a bit of time to bikeshed the binary format details upstream in the spec I plan on landing and then working on the Wasmtime side of things.

@alexcrichton alexcrichton changed the title [wasmparser] Add (implements "...") component name support Add (implements "...") component name support May 13, 2026
@alexcrichton
Copy link
Copy Markdown
Member

Ok I've done my own once-over on everything now and I feel this is in a good spot. @dicej would you be willing to look over this as well to confirm you don't find anything awry? I realize this is a pretty big PR, though, so no worries if you're too busy

Add a test case that broke in Wasmtime, and add a lot of words for why
things are the way they are.
Comment thread crates/wit-parser/src/resolve/mod.rs Outdated
Comment thread crates/wit-parser/src/resolve/mod.rs Outdated
@alexcrichton alexcrichton enabled auto-merge May 13, 2026 20:41
@alexcrichton alexcrichton added this pull request to the merge queue May 13, 2026
Merged via the queue into bytecodealliance:main with commit 649c4ea May 13, 2026
37 checks passed
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.

3 participants