From cffa858037e482602329b160e1d866f16d19cf6f Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Sat, 28 Feb 2026 20:49:50 -0500 Subject: [PATCH] Add implements clause support to wac Support the component model's implements name form, which allows importing/exporting the same interface multiple times under different plain-name labels (e.g., primary/backup stores). --- Cargo.lock | 85 +++++++++---------- Cargo.toml | 14 +-- crates/wac-graph/src/encoding.rs | 80 +++++++++++++++-- crates/wac-graph/src/graph.rs | 41 ++++++--- .../graphs/implements-exports/encoded.wat | 27 ++++++ .../graphs/implements-exports/graph.json | 8 ++ .../graphs/implements-exports/producer.wat | 11 +++ .../graphs/implements-imports/consumer.wat | 10 +++ .../graphs/implements-imports/encoded.wat | 28 ++++++ .../graphs/implements-imports/graph.json | 14 +++ .../graphs/implements-satisfied/consumer.wat | 10 +++ .../graphs/implements-satisfied/encoded.wat | 50 +++++++++++ .../graphs/implements-satisfied/graph.json | 17 ++++ .../graphs/implements-satisfied/producer.wat | 10 +++ crates/wac-types/Cargo.toml | 3 + crates/wac-types/src/package.rs | 50 ++++++----- crates/wac-types/tests/implements.rs | 84 ++++++++++++++++++ 17 files changed, 449 insertions(+), 93 deletions(-) create mode 100644 crates/wac-graph/tests/graphs/implements-exports/encoded.wat create mode 100644 crates/wac-graph/tests/graphs/implements-exports/graph.json create mode 100644 crates/wac-graph/tests/graphs/implements-exports/producer.wat create mode 100644 crates/wac-graph/tests/graphs/implements-imports/consumer.wat create mode 100644 crates/wac-graph/tests/graphs/implements-imports/encoded.wat create mode 100644 crates/wac-graph/tests/graphs/implements-imports/graph.json create mode 100644 crates/wac-graph/tests/graphs/implements-satisfied/consumer.wat create mode 100644 crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat create mode 100644 crates/wac-graph/tests/graphs/implements-satisfied/graph.json create mode 100644 crates/wac-graph/tests/graphs/implements-satisfied/producer.wat create mode 100644 crates/wac-types/tests/implements.rs diff --git a/Cargo.lock b/Cargo.lock index 9525c6b5..075b9a78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,9 +520,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -3962,9 +3962,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -3993,12 +3993,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -4069,7 +4063,7 @@ dependencies = [ "wac-types", "warg-client", "warg-protocol", - "wasmprinter 0.247.0", + "wasmprinter 0.252.0", "wat", "wit-component", "wit-parser", @@ -4090,10 +4084,10 @@ dependencies = [ "serde_json", "thiserror", "wac-types", - "wasm-encoder 0.247.0", + "wasm-encoder 0.252.0", "wasm-metadata", - "wasmparser 0.247.0", - "wasmprinter 0.247.0", + "wasmparser 0.252.0", + "wasmprinter 0.252.0", "wat", "wit-component", "wit-parser", @@ -4120,10 +4114,10 @@ dependencies = [ "tokio", "wac-graph", "wac-resolver", - "wasm-encoder 0.247.0", + "wasm-encoder 0.252.0", "wasm-metadata", - "wasmparser 0.247.0", - "wasmprinter 0.247.0", + "wasmparser 0.252.0", + "wasmprinter 0.252.0", ] [[package]] @@ -4149,7 +4143,7 @@ dependencies = [ "warg-crypto", "warg-protocol", "warg-server", - "wasmprinter 0.247.0", + "wasmprinter 0.252.0", "wat", "wit-component", "wit-parser", @@ -4164,8 +4158,9 @@ dependencies = [ "indexmap 2.14.0", "semver", "serde 1.0.228", - "wasm-encoder 0.247.0", - "wasmparser 0.247.0", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wat", ] [[package]] @@ -4467,19 +4462,19 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.247.0", + "wasmparser 0.252.0", ] [[package]] name = "wasm-metadata" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +checksum = "2b7e08e02a3cd55bf778009d4cd6faae50da011f293644daf78a531a32d6d142" dependencies = [ "anyhow", "auditable-serde", @@ -4490,8 +4485,8 @@ dependencies = [ "serde_json", "spdx", "url", - "wasm-encoder 0.247.0", - "wasmparser 0.247.0", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", ] [[package]] @@ -4520,9 +4515,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ "bitflags 2.5.0", "hashbrown 0.17.0", @@ -4543,33 +4538,33 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b221eaf3943d2f562f022ccee3d8c631e144b41cb7c8850c24b437923ee64e" +checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.247.0", + "wasmparser 0.252.0", ] [[package]] name = "wast" -version = "247.0.0" +version = "252.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579d2d47eb33b0cdf9b14723cb115f1e1b7d6e77aac6f0816e5b7c7aeaa418ff" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.0", - "wasm-encoder 0.247.0", + "wasm-encoder 0.252.0", ] [[package]] name = "wat" -version = "1.247.0" +version = "1.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f4091c56437e86f2b57fa2fac72c4f528957a605b3f44f7c0b3b19a17ac5ee" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" dependencies = [ "wast", ] @@ -4843,9 +4838,9 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +checksum = "76db0662b590f45d33d0e363fa13539a5a1eecd35d5a12fe208c335461c1053d" dependencies = [ "anyhow", "bitflags 2.5.0", @@ -4854,18 +4849,18 @@ dependencies = [ "serde 1.0.228", "serde_derive", "serde_json", - "wasm-encoder 0.247.0", + "wasm-encoder 0.252.0", "wasm-metadata", - "wasmparser 0.247.0", + "wasmparser 0.252.0", "wat", "wit-parser", ] [[package]] name = "wit-parser" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" dependencies = [ "anyhow", "hashbrown 0.17.0", @@ -4876,8 +4871,8 @@ dependencies = [ "serde 1.0.228", "serde_derive", "serde_json", - "unicode-xid", - "wasmparser 0.247.0", + "unicode-ident", + "wasmparser 0.252.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b8a0096d..11aab633 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,13 +63,13 @@ wac-parser = { path = "crates/wac-parser", version = "0.10.1", default-features wac-resolver = { path = "crates/wac-resolver", version = "0.10.1", default-features = false } wac-graph = { path = "crates/wac-graph", version = "0.10.1" } wac-types = { path = "crates/wac-types", version = "0.10.1" } -wit-parser = "0.247.0" -wasmparser = "0.247.0" -wit-component = "0.247.0" -wasm-encoder = "0.247.0" -wasmprinter = "0.247.0" -wasm-metadata = "0.247.0" -wat = "1.245.1" +wit-parser = "0.252.0" +wasmparser = "0.252.0" +wit-component = "0.252.0" +wasm-encoder = "0.252.0" +wasmprinter = "0.252.0" +wasm-metadata = "0.252.0" +wat = "1.252.0" anyhow = "1.0.81" clap = { version = "4.5.4", features = ["derive"] } semver = { version = "1.0.22", features = ["serde"] } diff --git a/crates/wac-graph/src/encoding.rs b/crates/wac-graph/src/encoding.rs index 2b10b023..7d138ee8 100644 --- a/crates/wac-graph/src/encoding.rs +++ b/crates/wac-graph/src/encoding.rs @@ -1,6 +1,7 @@ use crate::PackageId; use indexmap::IndexMap; use petgraph::graph::NodeIndex; +use std::borrow::Cow; use std::collections::HashMap; use wac_types::{ CoreExtern, DefinedType, DefinedTypeId, Enum, Flags, FuncTypeId, InterfaceId, ItemKind, @@ -8,11 +9,49 @@ use wac_types::{ WorldId, }; use wasm_encoder::{ - Alias, ComponentBuilder, ComponentCoreTypeEncoder, ComponentExportKind, + Alias, ComponentBuilder, ComponentCoreTypeEncoder, ComponentExportKind, ComponentExternName, ComponentOuterAliasKind, ComponentType, ComponentTypeEncoder, ComponentTypeRef, ComponentValType, EntityType, GlobalType, InstanceType, MemoryType, ModuleType, TableType, TagKind, TagType, TypeBounds, }; +use wasmparser::names::{ComponentName, ComponentNameKind}; + +/// Determines whether an instance import/export needs a component model +/// `(implements "…")` directive, and if so returns the interface name to put +/// inside it. +/// +/// An instance extern is encoded one of two ways depending on its name: +/// +/// * Regular interface extern: the extern name *is* the interface name, e.g. +/// `(import "foo:bar/iface" (instance …))`. Nothing to disambiguate, so this +/// returns `None`. +/// +/// * `implements` extern: the extern name is a plain-name label and the +/// interface it implements is carried separately, e.g. +/// `(import "primary" (implements "foo:bar/iface") (instance …))`. This is +/// how a component imports/exports the same interface more than once under +/// different labels. Here this returns `Some("foo:bar/iface")`, i.e. the +/// string the caller should encode as the `implements` directive. +/// +/// `name` is the extern name (`primary`) and `interface` identifies the +/// instance's interface within `types`, whose own name (`foo:bar/iface`) is the +/// returned value. +pub(crate) fn implements_directive<'a>( + types: &'a Types, + name: &str, + interface: InterfaceId, +) -> Option<&'a str> { + // Only a plain-name label is encoded with an `implements` directive; a + // regular interface extern already names its interface directly. + let is_label = matches!( + ComponentName::new(name, 0).as_ref().map(|n| n.kind()), + Ok(ComponentNameKind::Label(_)) + ); + if !is_label { + return None; + } + types[interface].id.as_deref() +} /// A type used to abstract the API differences between a component builder, /// component type, and instance type from `wasm-encoder`. @@ -65,7 +104,8 @@ impl Encodable { } } - fn import_type(&mut self, name: &str, ty: ComponentTypeRef) { + fn import_type<'a>(&mut self, name: impl Into>, ty: ComponentTypeRef) { + let name = name.into(); match self { Encodable::Component(t) => { t.import(name, ty); @@ -730,13 +770,22 @@ impl<'a> TypeEncoder<'a> { } ItemKind::Instance(id) => { let import_index = state.current.encodable.instance_count(); + let implements = implements_directive(self.0, name, id); + let extern_name = ComponentExternName { + name: Cow::Borrowed(name), + implements: implements.map(Cow::Borrowed), + }; state .current .encodable - .import_type(name, ComponentTypeRef::Instance(index)); - if let Some(iid) = &self.0[id].id { - log::debug!("instance index {import_index} ({iid}) is available for aliasing"); - state.current.instances.insert(iid.clone(), import_index); + .import_type(extern_name, ComponentTypeRef::Instance(index)); + if implements.is_none() { + if let Some(iid) = &self.0[id].id { + log::debug!( + "instance index {import_index} ({iid}) is available for aliasing" + ); + state.current.instances.insert(iid.clone(), import_index); + } } } _ => panic!("expected only types, functions, and instance types"), @@ -827,9 +876,19 @@ impl<'a> TypeEncoder<'a> { let ty = kind.ty(); let index = self.ty(state, ty, Some(name)); + // Preserve the `(implements "I")` directive when re-encoding an + // instance export that uses a plain-name label. + let implements = match kind { + ItemKind::Instance(id) => implements_directive(self.0, name, id), + _ => None, + }; + let extern_name = ComponentExternName { + name: Cow::Borrowed(name), + implements: implements.map(Cow::Borrowed), + }; let index = Self::export_type( state, - name, + extern_name, match kind { ItemKind::Type(_) => ComponentTypeRef::Type(TypeBounds::Eq(index)), ItemKind::Func(_) => ComponentTypeRef::Func(index), @@ -880,7 +939,12 @@ impl<'a> TypeEncoder<'a> { index } - fn export_type(state: &mut State, name: &str, ty: ComponentTypeRef) -> u32 { + fn export_type<'b>( + state: &mut State, + name: impl Into>, + ty: ComponentTypeRef, + ) -> u32 { + let name = name.into(); match &mut state.current.encodable { Encodable::Component(t) => { let index = t.type_count(); diff --git a/crates/wac-graph/src/graph.rs b/crates/wac-graph/src/graph.rs index ace9cbaa..e22c5384 100644 --- a/crates/wac-graph/src/graph.rs +++ b/crates/wac-graph/src/graph.rs @@ -20,8 +20,8 @@ use wac_types::{ Type, TypeAggregator, Types, ValueType, }; use wasm_encoder::{ - Alias, ComponentBuilder, ComponentExportKind, ComponentNameSection, ComponentTypeRef, - ComponentValType, NameMap, TypeBounds, + Alias, ComponentBuilder, ComponentExportKind, ComponentExternName, ComponentNameSection, + ComponentTypeRef, ComponentValType, NameMap, TypeBounds, }; use wasmparser::{ names::{ComponentName, ComponentNameKind}, @@ -1695,12 +1695,26 @@ impl<'a> CompositionGraphEncoder<'a> { } fn import(&self, state: &mut State, name: &str, types: &Types, kind: ItemKind) -> u32 { + // Determine whether this is a component model `(implements "I")` import. + // The same interface may be imported multiple times under different + // labels, so implements imports are exempt from the shared-dependency + // deduplication below and carry an `implements` directive when + // re-encoded. + let implements = match kind { + ItemKind::Instance(id) => crate::encoding::implements_directive(types, name, id), + _ => None, + }; + // Check to see if this is an import of an interface that's already been - // imported; this can happen based on importing of shared dependencies - if let ItemKind::Instance(id) = kind { - if let Some(id) = &types[id].id { - if let Some(index) = state.current.instances.get(id) { - return *index; + // imported; this can happen based on importing of shared dependencies. + // Implements imports are skipped here since the same interface can be + // imported multiple times under distinct labels. + if implements.is_none() { + if let ItemKind::Instance(id) = kind { + if let Some(id) = &types[id].id { + if let Some(index) = state.current.instances.get(id) { + return *index; + } } } } @@ -1718,8 +1732,12 @@ impl<'a> CompositionGraphEncoder<'a> { // Encode the type and import let ty = encoder.ty(state, kind.ty(), None); + let extern_name = ComponentExternName { + name: Cow::Borrowed(name), + implements: implements.map(Cow::Borrowed), + }; let index = state.builder().import( - name, + extern_name, match kind { ItemKind::Type(_) => ComponentTypeRef::Type(TypeBounds::Eq(ty)), ItemKind::Func(_) => ComponentTypeRef::Func(ty), @@ -1741,7 +1759,10 @@ impl<'a> CompositionGraphEncoder<'a> { state.current.type_indexes.insert(ty, index); } ItemKind::Instance(id) => { - if let Some(id) = &types[id].id { + // Implements imports are not registered for shared-dependency + // aliasing: distinct labels of the same interface must remain + // distinct instances rather than collapsing to one. + if let (None, Some(id)) = (&implements, &types[id].id) { log::debug!( "interface `{id}` is available for aliasing as instance index {index}" ); @@ -1774,7 +1795,7 @@ impl<'a> CompositionGraphEncoder<'a> { let encoder = TypeEncoder::new(&self.0.types); let ty = encoder.component(state, package.ty()); state.builder().import( - &Self::package_import_name(package), + Self::package_import_name(package), ComponentTypeRef::Component(ty), ) }; diff --git a/crates/wac-graph/tests/graphs/implements-exports/encoded.wat b/crates/wac-graph/tests/graphs/implements-exports/encoded.wat new file mode 100644 index 00000000..88f04ccb --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-exports/encoded.wat @@ -0,0 +1,27 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (type (;1;) + (component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (export (;1;) "primary" (implements "test:test/iface") (instance (type 0))) + (export (;2;) "backup" (implements "test:test/iface") (instance (type 0))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 1))) + (instance (;1;) (instantiate 0 + (with "test:test/iface" (instance 0)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-exports/graph.json b/crates/wac-graph/tests/graphs/implements-exports/graph.json new file mode 100644 index 00000000..efc573ab --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-exports/graph.json @@ -0,0 +1,8 @@ +{ + "packages": [ + { "name": "test:producer", "path": "producer.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-exports/producer.wat b/crates/wac-graph/tests/graphs/implements-exports/producer.wat new file mode 100644 index 00000000..a0fa6360 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-exports/producer.wat @@ -0,0 +1,11 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (export "primary" (implements "test:test/iface") (instance 0)) + (export "backup" (implements "test:test/iface") (instance 0)) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports/consumer.wat b/crates/wac-graph/tests/graphs/implements-imports/consumer.wat new file mode 100644 index 00000000..a3326427 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports/consumer.wat @@ -0,0 +1,10 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports/encoded.wat b/crates/wac-graph/tests/graphs/implements-imports/encoded.wat new file mode 100644 index 00000000..592b1419 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports/encoded.wat @@ -0,0 +1,28 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) + (type (;1;) + (component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 1))) + (instance (;2;) (instantiate 0 + (with "primary" (instance 0)) + (with "backup" (instance 1)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports/graph.json b/crates/wac-graph/tests/graphs/implements-imports/graph.json new file mode 100644 index 00000000..3fc81179 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports/graph.json @@ -0,0 +1,14 @@ +{ + "packages": [ + { + "name": "test:consumer", + "path": "consumer.wat" + } + ], + "nodes": [ + { + "type": "instantiation", + "package": 0 + } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-satisfied/consumer.wat b/crates/wac-graph/tests/graphs/implements-satisfied/consumer.wat new file mode 100644 index 00000000..a3326427 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-satisfied/consumer.wat @@ -0,0 +1,10 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat b/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat new file mode 100644 index 00000000..e5a9fa7c --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat @@ -0,0 +1,50 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (type (;1;) + (component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (export (;1;) "test:test/iface" (instance (type 0))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 1))) + (instance (;1;) (instantiate 0 + (with "test:test/iface" (instance 0)) + ) + ) + (instance (;2;) (instantiate 0 + (with "test:test/iface" (instance 0)) + ) + ) + (alias export 1 "test:test/iface" (instance (;3;))) + (alias export 2 "test:test/iface" (instance (;4;))) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) + ) + ) + (import "unlocked-dep=" (component (;1;) (type 2))) + (instance (;5;) (instantiate 1 + (with "backup" (instance 4)) + (with "primary" (instance 3)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-satisfied/graph.json b/crates/wac-graph/tests/graphs/implements-satisfied/graph.json new file mode 100644 index 00000000..6fd74434 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-satisfied/graph.json @@ -0,0 +1,17 @@ +{ + "packages": [ + { "name": "test:consumer", "path": "consumer.wat" }, + { "name": "test:producer", "path": "producer.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 1 }, + { "type": "instantiation", "package": 1 }, + { "type": "alias", "source": 0, "export": "test:test/iface" }, + { "type": "alias", "source": 1, "export": "test:test/iface" }, + { "type": "instantiation", "package": 0 } + ], + "arguments": [ + { "source": 2, "target": 4, "name": "primary" }, + { "source": 3, "target": 4, "name": "backup" } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-satisfied/producer.wat b/crates/wac-graph/tests/graphs/implements-satisfied/producer.wat new file mode 100644 index 00000000..23478183 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-satisfied/producer.wat @@ -0,0 +1,10 @@ +(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (export "test:test/iface" (instance 0)) +) diff --git a/crates/wac-types/Cargo.toml b/crates/wac-types/Cargo.toml index bc497222..98a44618 100644 --- a/crates/wac-types/Cargo.toml +++ b/crates/wac-types/Cargo.toml @@ -18,5 +18,8 @@ serde = { workspace = true, optional = true } wasmparser = { workspace = true } wasm-encoder = { workspace = true } +[dev-dependencies] +wat = { workspace = true } + [features] serde = ["dep:serde"] diff --git a/crates/wac-types/src/package.rs b/crates/wac-types/src/package.rs index 6ca479c2..dadb025c 100644 --- a/crates/wac-types/src/package.rs +++ b/crates/wac-types/src/package.rs @@ -224,13 +224,13 @@ impl Package { Payload::ComponentImportSection(s) => { imports.reserve(s.count() as usize); for import in s { - imports.push(import?.name.0); + imports.push(import?.name.name); } } Payload::ComponentExportSection(s) => { exports.reserve(s.count() as usize); for export in s { - exports.push(export?.name.0); + exports.push(export?.name.name); } } _ => {} @@ -406,18 +406,14 @@ impl<'a> TypeConverter<'a> { } fn import(&mut self, name: &str) -> Result { - let import = self - .wasm_types - .component_entity_type_of_import(name) - .unwrap(); + let wasm_types = self.wasm_types.clone(); + let import = wasm_types.component_item_for_import(name).unwrap(); self.entity(name, import) } fn export(&mut self, name: &str) -> Result { - let export = self - .wasm_types - .component_entity_type_of_export(name) - .unwrap(); + let wasm_types = self.wasm_types.clone(); + let export = wasm_types.component_item_for_export(name).unwrap(); self.entity(name, export) } @@ -529,13 +525,13 @@ impl<'a> TypeConverter<'a> { exports: IndexMap::with_capacity(instance_ty.exports.len()), }); - for (name, ty) in &instance_ty.exports { - let export = self.entity(name, *ty)?; + for (name, item) in &instance_ty.exports { + let export = self.entity(name, item)?; if let wasm::ComponentEntityType::Type { referenced, created, - } = ty + } = &item.ty { self.use_or_own(Owner::Interface(id), name, *referenced, *created); @@ -559,8 +555,8 @@ impl<'a> TypeConverter<'a> { Ok(id) } - fn entity(&mut self, name: &str, ty: wasm::ComponentEntityType) -> Result { - match ty { + fn entity(&mut self, name: &str, item: &wasm::ComponentItem) -> Result { + match item.ty { wasm::ComponentEntityType::Module(id) => Ok(ItemKind::Module(self.module_type(id)?)), wasm::ComponentEntityType::Value(ty) => { Ok(ItemKind::Value(self.component_val_type(ty)?)) @@ -571,9 +567,17 @@ impl<'a> TypeConverter<'a> { wasm::ComponentEntityType::Func(id) => { Ok(ItemKind::Func(self.component_func_type(id)?)) } - wasm::ComponentEntityType::Instance(id) => Ok(ItemKind::Instance( - self.component_instance_type(Some(name), id)?, - )), + wasm::ComponentEntityType::Instance(id) => { + // A `(implements "I")` import is given a plain-name label (e.g. + // `primary`) in the binary, but the interface it implements is + // carried in the `implements` metadata. Use that interface name + // as the interface id so it resolves to the named interface + // rather than the opaque label. + let iface_name = item.implements.as_deref().unwrap_or(name); + Ok(ItemKind::Instance( + self.component_instance_type(Some(iface_name), id)?, + )) + } wasm::ComponentEntityType::Component(id) => { Ok(ItemKind::Component(self.component_type(Some(name), id)?)) } @@ -635,13 +639,13 @@ impl<'a> TypeConverter<'a> { exports: IndexMap::with_capacity(component_ty.exports.len()), }); - for (name, ty) in &component_ty.imports { - let import = self.entity(name, *ty)?; + for (name, item) in &component_ty.imports { + let import = self.entity(name, item)?; if let wasm::ComponentEntityType::Type { referenced, created, - } = ty + } = &item.ty { self.use_or_own(Owner::World(id), name, *referenced, *created); } @@ -650,8 +654,8 @@ impl<'a> TypeConverter<'a> { assert!(prev.is_none()); } - for (name, ty) in &component_ty.exports { - let ty = self.entity(name, *ty)?; + for (name, item) in &component_ty.exports { + let ty = self.entity(name, item)?; let prev = self.types[id].exports.insert(name.clone(), ty); assert!(prev.is_none()); } diff --git a/crates/wac-types/tests/implements.rs b/crates/wac-types/tests/implements.rs new file mode 100644 index 00000000..6c50633e --- /dev/null +++ b/crates/wac-types/tests/implements.rs @@ -0,0 +1,84 @@ +use wac_types::{ItemKind, Package, Types}; + +/// A component that imports the same interface twice under two different +/// plain-name labels using the component model `(implements "I")` directive. +const IMPLEMENTS_IMPORTS: &str = r#"(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) +)"#; + +/// Parsing a component with `(implements "I")` imports should preserve both +/// labeled imports and resolve their interface id to the implemented interface +/// name rather than the opaque label. +#[test] +fn implements_import_interface_id() { + let bytes = wat::parse_str(IMPLEMENTS_IMPORTS).expect("failed to parse WAT"); + let mut types = Types::default(); + let package = Package::from_bytes("test:consumer", None, bytes, &mut types) + .expect("failed to parse component"); + + let world = &types[package.ty()]; + + // Both labeled imports are present, keyed by their plain-name label. + assert!( + world.imports.contains_key("primary"), + "expected `primary` implements import" + ); + assert!( + world.imports.contains_key("backup"), + "expected `backup` implements import" + ); + + // Each import is an instance whose interface id is the implemented + // interface, not the label. + for name in ["primary", "backup"] { + let ItemKind::Instance(id) = world.imports[name] else { + panic!("expected instance kind for import `{name}`"); + }; + assert_eq!( + types[id].id.as_deref(), + Some("test:test/iface"), + "implements import `{name}` should resolve to interface `test:test/iface`" + ); + } +} + +/// A regular interface import and an `(implements "I")` import of the same +/// interface should coexist, both resolving to the same interface id. +#[test] +fn implements_with_regular_import() { + let wat = r#"(component + (type (;0;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "test:test/iface" (instance (;0;) (type 0))) + (import "secondary" (implements "test:test/iface") (instance (;1;) (type 0))) + )"#; + + let bytes = wat::parse_str(wat).expect("failed to parse WAT"); + let mut types = Types::default(); + let package = Package::from_bytes("test:consumer", None, bytes, &mut types) + .expect("failed to parse component"); + + let world = &types[package.ty()]; + + for name in ["test:test/iface", "secondary"] { + let ItemKind::Instance(id) = *world + .imports + .get(name) + .unwrap_or_else(|| panic!("expected import `{name}` to be present")) + else { + panic!("expected instance kind for import `{name}`"); + }; + assert_eq!(types[id].id.as_deref(), Some("test:test/iface")); + } +}