From ffbcaab033bbb2404c239e1bbe9e031cae21da2a Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Fri, 21 Aug 2026 13:16:54 -0700 Subject: [PATCH 1/4] fix --- crates/wit-component/src/encoding.rs | 53 ++++++++- crates/wit-component/src/encoding/wit.rs | 80 +++++++++++--- crates/wit-component/tests/components.rs | 2 + .../components/canonical-names/component.wat | 73 +++++++++++++ .../canonical-names/component.wit.print | 5 + .../components/canonical-names/module.wat | 8 ++ .../components/canonical-names/module.wit | 20 ++++ crates/wit-parser/src/lib.rs | 63 +++++++++++ crates/wit-parser/src/resolve/mod.rs | 102 ++++++++++++++++-- src/bin/wasm-tools/component.rs | 58 +++++++++- tests/cli/help-component-new-short.wat.stdout | 3 + tests/cli/help-component-new.wat.stdout | 9 ++ tests/cli/help-component-wit-short.wat.stdout | 3 + tests/cli/help-component-wit.wat.stdout | 10 ++ 14 files changed, 454 insertions(+), 35 deletions(-) create mode 100644 crates/wit-component/tests/components/canonical-names/component.wat create mode 100644 crates/wit-component/tests/components/canonical-names/component.wit.print create mode 100644 crates/wit-component/tests/components/canonical-names/module.wat create mode 100644 crates/wit-component/tests/components/canonical-names/module.wit diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index b5c08a048b..ebba9efb0c 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -609,12 +609,23 @@ impl<'a> EncodingState<'a> { let instance_type_idx = self .component .type_instance(Some(&format!("ty-{name}")), &ty); + + let (import_name, version_suffix) = if self.info.encoder.emit_canonical_names { + let canon_name = resolve + .canon_id_of(interface_id) + .unwrap_or_else(|| name.to_string()); + let suffix = resolve.version_suffix_of(interface_id); + (canon_name, suffix) + } else { + (name.to_string(), None) + }; + let instance_idx = self.component.import( wasm_encoder::ComponentExternName { - name: name.into(), + name: import_name.into(), implements: info.implements.as_deref().map(|s| s.into()), external_id: info.external_id.as_deref().map(|s| s.into()), - version_suffix: None, + version_suffix: version_suffix.map(|s| s.into()), }, ComponentTypeRef::Instance(instance_type_idx), ); @@ -762,7 +773,11 @@ impl<'a> EncodingState<'a> { let world = &resolve.worlds[self.info.encoder.metadata.world]; for export_name in exports { - let export_string = resolve.name_world_key(export_name); + let export_string = if self.info.encoder.emit_canonical_names { + resolve.name_canon_world_key(export_name) + } else { + resolve.name_world_key(export_name) + }; match &world.exports[export_name] { WorldItem::Function(func) => { let ty = self @@ -993,12 +1008,21 @@ impl<'a> EncodingState<'a> { component_index, imports, ); + let export_version_suffix = if self.info.encoder.emit_canonical_names { + if let WorldKey::Interface(id) = key { + resolve.version_suffix_of(*id) + } else { + None + } + } else { + None + }; let idx = self.component.export( wasm_encoder::ComponentExternName { name: export_name.into(), implements: resolve.implements_value(key, item).map(|s| s.into()), external_id: resolve.external_id_value(key, item).map(|s| s.into()), - version_suffix: None, + version_suffix: export_version_suffix.map(|s| s.into()), }, ComponentExportKind::Instance, instance_index, @@ -3290,6 +3314,7 @@ pub struct ComponentEncoder { pub(super) reject_legacy_names: bool, debug_names: bool, shim_return_call_ref: bool, + emit_canonical_names: bool, } impl ComponentEncoder { @@ -3357,6 +3382,20 @@ impl ComponentEncoder { self } + /// Sets whether to emit canonical interface names in the component binary. + /// + /// When enabled, import/export names use canonical version prefixes (e.g., + /// `wasi:cli/exit@0.2` instead of `wasi:cli/exit@0.2.1`) and the + /// `version_suffix` field is populated. This also forces merging of + /// imports that share the same canonical version prefix. + /// This flag subsumes the `merge_imports_based_on_semver` flag. + /// + /// This is disabled by default. + pub fn emit_canonical_names(&mut self, emit: bool) -> &mut Self { + self.emit_canonical_names = emit; + self + } + /// Sets whether to reject the historical mangling/name scheme for core wasm /// imports/exports as they map to the component model. /// @@ -3509,7 +3548,11 @@ impl ComponentEncoder { bail!("a module is required when encoding a component"); } - if self.merge_imports_based_on_semver.unwrap_or(true) { + if self.emit_canonical_names { + self.metadata + .resolve + .merge_world_imports_based_on_canonical_version(self.metadata.world)?; + } else if self.merge_imports_based_on_semver.unwrap_or(true) { self.metadata .resolve .merge_world_imports_based_on_semver(self.metadata.world)?; diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index a70f90cc38..4add7c3d4b 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -25,7 +25,16 @@ use wit_parser::*; /// The binary returned can be [`decode`d](crate::decode) to recover the WIT /// package provided. pub fn encode(resolve: &Resolve, package: PackageId) -> Result> { - let mut component = encode_component(resolve, package)?; + encode_with_options(resolve, package, false) +} + +/// Same as [`encode`] but with an option to emit canonical interface names. +pub fn encode_with_options( + resolve: &Resolve, + package: PackageId, + canonical_names: bool, +) -> Result> { + let mut component = encode_component_with_options(resolve, package, canonical_names)?; component.raw_custom_section(&crate::base_producers().raw_custom_section()); Ok(component.finish()) } @@ -48,11 +57,16 @@ pub fn encode(resolve: &Resolve, package: PackageId) -> Result> { /// /// The binary returned can be [`decode`d](crate::decode) to recover the WIT /// package provided. -pub fn encode_component(resolve: &Resolve, package: PackageId) -> Result { +pub fn encode_component_with_options( + resolve: &Resolve, + package: PackageId, + canonical_names: bool, +) -> Result { let mut encoder = Encoder { component: ComponentBuilder::default(), resolve, package, + canonical_names, }; encoder.run()?; @@ -67,6 +81,15 @@ pub fn encode_component(resolve: &Resolve, package: PackageId) -> Result Result { + encode_world_with_options(resolve, world_id, false) +} + +/// Same as [`encode_world`] but with an option to emit canonical names. +pub fn encode_world_with_options( + resolve: &Resolve, + world_id: WorldId, + canonical_names: bool, +) -> Result { let mut component = InterfaceEncoder::new(resolve); let world = &resolve.worlds[world_id]; log::trace!("encoding world {}", world.name); @@ -93,9 +116,10 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result Result unreachable!(), }; - component - .outer - .export(component_extern_name(resolve, key, export), ty); + component.outer.export( + component_extern_name(resolve, key, export, canonical_names), + ty, + ); } Ok(component.outer) @@ -125,12 +150,27 @@ fn component_extern_name( resolve: &Resolve, key: &WorldKey, item: &WorldItem, + canonical_names: bool, ) -> wasm_encoder::ComponentExternName<'static> { - ComponentExternName { - name: resolve.name_world_key(key).into(), - implements: resolve.implements_value(key, item).map(|s| s.into()), - external_id: resolve.external_id_value(key, item).map(|s| s.into()), - version_suffix: None, + if canonical_names { + let name = resolve.name_canon_world_key(key); + let version_suffix = match key { + WorldKey::Interface(id) => resolve.version_suffix_of(*id), + WorldKey::Name(_) => None, + }; + ComponentExternName { + name: name.into(), + implements: resolve.implements_value(key, item).map(|s| s.into()), + external_id: resolve.external_id_value(key, item).map(|s| s.into()), + version_suffix: version_suffix.map(|s| s.into()), + } + } else { + ComponentExternName { + name: resolve.name_world_key(key).into(), + implements: resolve.implements_value(key, item).map(|s| s.into()), + external_id: resolve.external_id_value(key, item).map(|s| s.into()), + version_suffix: None, + } } } @@ -138,6 +178,7 @@ struct Encoder<'a> { component: ComponentBuilder, resolve: &'a Resolve, package: PackageId, + canonical_names: bool, } impl Encoder<'_> { @@ -153,7 +194,8 @@ impl Encoder<'_> { // For each `world` encode it directly as a component and then create a // wrapper component that exports that component. for (name, &world) in self.resolve.packages[self.package].worlds.iter() { - let component_ty = encode_world(self.resolve, world)?; + let component_ty = + encode_world_with_options(self.resolve, world, self.canonical_names)?; let world = &self.resolve.worlds[world]; let mut wrapper = ComponentType::new(); @@ -197,11 +239,15 @@ impl Encoder<'_> { for interface in interfaces { encoder.interface = Some(interface); let iface = &self.resolve.interfaces[interface]; - let name = self.resolve.id_of(interface).unwrap(); + let name = if self.canonical_names { + self.resolve.canon_id_of(interface).unwrap() + } else { + self.resolve.id_of(interface).unwrap() + }; if interface == id { let idx = encoder.encode_instance(interface)?; log::trace!("exporting self as {idx}"); - encoder.outer.export(name, ComponentTypeRef::Instance(idx)); + encoder.outer.export(&name, ComponentTypeRef::Instance(idx)); } else { encoder.push_instance(); for (_, id) in iface.types.iter() { @@ -212,7 +258,7 @@ impl Encoder<'_> { encoder.outer.ty().instance(&instance); encoder.import_map.insert(interface, encoder.instances); encoder.instances += 1; - encoder.outer.import(name, ComponentTypeRef::Instance(idx)); + encoder.outer.import(&name, ComponentTypeRef::Instance(idx)); } } diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs index b4b12adcf0..35cdb095bf 100644 --- a/crates/wit-component/tests/components.rs +++ b/crates/wit-component/tests/components.rs @@ -116,6 +116,7 @@ fn run_test(path: &Path) -> Result<()> { .debug_names(true) .shim_return_call_ref(config.return_call_ref) .realloc_via_memory_grow(config.realloc_via_memory_grow) + .emit_canonical_names(config.merge_imports_based_on_canonical_version) .module(&module)?; for adapter in adapters { let (name, wasm) = read_name_and_module("adapt-", &adapter?, &resolve, pkg_id)?; @@ -247,6 +248,7 @@ struct Config { use_built_in_libdl: bool, return_call_ref: bool, realloc_via_memory_grow: bool, + merge_imports_based_on_canonical_version: bool, } /// Reads the configuration for the test located at `path`. diff --git a/crates/wit-component/tests/components/canonical-names/component.wat b/crates/wit-component/tests/components/canonical-names/component.wat new file mode 100644 index 0000000000..a28ebdba45 --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/component.wat @@ -0,0 +1,73 @@ +(component + (type $ty-a:b/c@0.1.1 (;0;) + (instance + (type (;0;) (func (param "x" string))) + (export (;0;) "x" (func (type 0))) + (type (;1;) (func)) + (export (;1;) "y" (func (type 1))) + ) + ) + (import "a:b/c@0.1" (versionsuffix ".1") (instance $a:b/c@0.1 (;0;) (type $ty-a:b/c@0.1.1))) + (core module $main (;0;) + (type (;0;) (func (param i32 i32))) + (type (;1;) (func)) + (import "a:b/c@0.1.1" "x" (func (;0;) (type 0))) + (import "a:b/c@0.1.1" "y" (func (;1;) (type 1))) + (memory (;0;) 1) + (export "memory" (memory 0)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + (processed-by "my-fake-bindgen" "123.45") + ) + ) + (core module $wit-component-shim-module (;1;) + (type (;0;) (func (param i32 i32))) + (table (;0;) 1 1 funcref) + (export "0" (func $indirect-a:b/c@0.1.1-x)) + (export "$imports" (table 0)) + (func $indirect-a:b/c@0.1.1-x (;0;) (type 0) (param i32 i32) + local.get 0 + local.get 1 + i32.const 0 + call_indirect (type 0) + ) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core instance $wit-component-shim-instance (;0;) (instantiate $wit-component-shim-module)) + (alias core export $wit-component-shim-instance "0" (core func $indirect-a:b/c@0.1.1-x (;0;))) + (alias export $a:b/c@0.1 "y" (func $y (;0;))) + (core func $y (;1;) (canon lower (func $y))) + (core instance $a:b/c@0.1.1 (;1;) + (export "x" (func $indirect-a:b/c@0.1.1-x)) + (export "y" (func $y)) + ) + (core instance $main (;2;) (instantiate $main + (with "a:b/c@0.1.1" (instance $a:b/c@0.1.1)) + ) + ) + (alias core export $main "memory" (core memory $memory (;0;))) + (core module $wit-component-fixup (;2;) + (type (;0;) (func (param i32 i32))) + (import "actual" "0" (func $0 (;0;) (type 0))) + (import "shim" "$imports" (table (;0;) 1 1 funcref)) + (elem (;0;) (i32.const 0) func $0) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (alias export $a:b/c@0.1 "x" (func $x (;1;))) + (core func $"#core-func2 indirect-a:b/c@0.1.1-x" (@name "indirect-a:b/c@0.1.1-x") (;2;) (canon lower (func $x) (memory $memory) string-encoding=utf8)) + (core instance $actual (;3;) + (export "0" (func $"#core-func2 indirect-a:b/c@0.1.1-x")) + ) + (core instance $fixup (;4;) (instantiate $wit-component-fixup + (with "actual" (instance $actual)) + (with "shim" (instance $wit-component-shim-instance)) + ) + ) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/components/canonical-names/component.wit.print b/crates/wit-component/tests/components/canonical-names/component.wit.print new file mode 100644 index 0000000000..1a2ed3c569 --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/component.wit.print @@ -0,0 +1,5 @@ +package root:component; + +world root { + import a:b/c@0.1.1; +} diff --git a/crates/wit-component/tests/components/canonical-names/module.wat b/crates/wit-component/tests/components/canonical-names/module.wat new file mode 100644 index 0000000000..fb6c8baf25 --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/module.wat @@ -0,0 +1,8 @@ +;;! merge-imports-based-on-canonical-version = true + +(module + (import "a:b/c@0.1.1" "x" (func (param i32 i32))) + (import "a:b/c@0.1.1" "y" (func)) + + (memory (export "memory") 1) +) diff --git a/crates/wit-component/tests/components/canonical-names/module.wit b/crates/wit-component/tests/components/canonical-names/module.wit new file mode 100644 index 0000000000..a1606ac7ad --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/module.wit @@ -0,0 +1,20 @@ +package foo:foo; + +world module { + import a:b/c@0.1.0; + import a:b/c@0.1.1; +} + +package a:b@0.1.0 { + interface c { + x: func(x: string); + } +} + +package a:b@0.1.1 { + interface c { + x: func(x: string); + y: func(); + } +} + diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index d1aece5a81..f95db12682 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -308,6 +308,28 @@ impl PackageName { } version.to_string() } + + /// Splits a semver version into a canonical version prefix and a version + /// suffix according to the component model spec. + /// + /// The split point is: + /// - If `major > 0`: split after major (e.g. `1.2.3` -> `("1", ".2.3")`) + /// - If `major == 0` and `minor > 0`: split after minor + /// (e.g. `0.2.6-rc.1` -> `("0.2", ".6-rc.1")`) + /// - Otherwise: split after patch (e.g. `0.0.1-alpha` -> `("0.0.1", "-alpha")`) + pub fn canon_version_split(version: &Version) -> (String, String) { + let s = version.to_string(); + let split_pos = if version.major > 0 { + version.major.to_string().len() + } else if version.minor > 0 { + 2 + version.minor.to_string().len() + } else { + 4 + version.patch.to_string().len() + }; + let prefix = s[..split_pos].to_string(); + let suffix = s[split_pos..].to_string(); + (prefix, suffix) + } } impl fmt::Display for PackageName { @@ -1572,4 +1594,45 @@ mod test { assert_eq!(t1, found[1]); assert_eq!(t2, found[2]); } + + #[test] + fn test_canon_version_split() { + use semver::Version; + + let v = Version::parse("1.2.3").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("1".to_string(), ".2.3".to_string()) + ); + + let v = Version::parse("0.2.6-rc.1").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("0.2".to_string(), ".6-rc.1".to_string()) + ); + + let v = Version::parse("0.1.0").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("0.1".to_string(), ".0".to_string()) + ); + + let v = Version::parse("0.0.1-alpha").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("0.0.1".to_string(), "-alpha".to_string()) + ); + + let v = Version::parse("0.0.0").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("0.0.0".to_string(), "".to_string()) + ); + + let v = Version::parse("1.0.0-beta.1").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("1".to_string(), ".0.0-beta.1".to_string()) + ); + } } diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index 388e2767b7..79868bb207 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1550,6 +1550,60 @@ impl Resolve { } } + /// Returns the canonical interface ID using [`PackageName::canon_version_split`]. + /// + /// For example, for a package at version `0.2.1` with interface name `types`, + /// this returns `"wasi:http/types@0.2"`. + pub fn canon_id_of(&self, interface: InterfaceId) -> Option { + let interface = &self.interfaces[interface]; + Some(self.canon_id_of_name(interface.package.unwrap(), interface.name.as_ref()?)) + } + + /// Returns the canonical interface name using [`PackageName::canon_version_split`]. + pub fn canon_id_of_name(&self, pkg: PackageId, name: &str) -> String { + let package = &self.packages[pkg]; + let mut base = String::new(); + base.push_str(&package.name.namespace); + base.push(':'); + base.push_str(&package.name.name); + base.push('/'); + base.push_str(name); + if let Some(version) = &package.name.version { + base.push('@'); + let (prefix, _) = PackageName::canon_version_split(version); + base.push_str(&prefix); + } + base + } + + /// Same as [`Resolve::name_world_key`] except that `WorldKey::Interface` + /// uses [`Resolve::canon_id_of`]. + pub fn name_canon_world_key(&self, key: &WorldKey) -> String { + match key { + WorldKey::Name(s) => s.to_string(), + WorldKey::Interface(i) => self + .canon_id_of(*i) + .expect("unexpected anonymous interface"), + } + } + + /// Returns the version suffix for the given interface's package version, + /// using [`PackageName::canon_version_split`]. + /// + /// For example, for a package at version `0.2.1`, returns `Some(".1")`. + /// Returns `None` if the suffix is empty or there is no version. + pub fn version_suffix_of(&self, interface: InterfaceId) -> Option { + let iface = &self.interfaces[interface]; + let pkg = &self.packages[iface.package?]; + let version = pkg.name.version.as_ref()?; + let (_, suffix) = PackageName::canon_version_split(version); + if suffix.is_empty() { + None + } else { + Some(suffix) + } + } + /// Returns the component model `implements` value for the world import of /// `key` and `item`. /// @@ -2408,6 +2462,23 @@ impl Resolve { /// 0.2.1. If, however, 0.3.0 where imported then the final result would /// import both 0.2.0 and 0.3.0. pub fn merge_world_imports_based_on_semver(&mut self, world_id: WorldId) -> anyhow::Result<()> { + self.merge_world_imports_inner(world_id, false) + } + + /// Same as [`Resolve::merge_world_imports_based_on_semver`] but groups by + /// canonical version prefix from [`PackageName::canon_version_split`]. + pub fn merge_world_imports_based_on_canonical_version( + &mut self, + world_id: WorldId, + ) -> anyhow::Result<()> { + self.merge_world_imports_inner(world_id, true) + } + + fn merge_world_imports_inner( + &mut self, + world_id: WorldId, + use_canonical_version: bool, + ) -> anyhow::Result<()> { let world = &self.worlds[world_id]; // The first pass here is to build a map of "semver tracks" where they @@ -2418,14 +2489,14 @@ impl Resolve { // At the same time a `to_remove` set is maintained to remember what // interfaces are being removed from `from` and `into`. All of // `to_remove` are placed with a known other version. - let mut semver_tracks = HashMap::new(); + let mut semver_tracks: HashMap<(String, String), (&Version, InterfaceId)> = HashMap::new(); let mut to_remove = HashSet::new(); for (key, _) in world.imports.iter() { let iface_id = match key { WorldKey::Interface(id) => *id, WorldKey::Name(_) => continue, }; - let (track, version) = match self.semver_track(iface_id) { + let (track, version) = match self.semver_track(iface_id, use_canonical_version) { Some(track) => track, None => continue, }; @@ -2435,7 +2506,7 @@ impl Resolve { track.0, track.1, ); - match semver_tracks.entry(track.clone()) { + match semver_tracks.entry(track) { Entry::Vacant(e) => { e.insert((version, iface_id)); } @@ -2456,7 +2527,7 @@ impl Resolve { // the results of the loop above. let mut replacements = HashMap::new(); for id in to_remove { - let (track, _) = self.semver_track(id).unwrap(); + let (track, _) = self.semver_track(id, use_canonical_version).unwrap(); let (_, latest) = semver_tracks[&track]; let prev = replacements.insert(id, latest); assert!(prev.is_none()); @@ -2575,16 +2646,25 @@ impl Resolve { /// tuple returned is a "semver track" for the specific interface. The /// version listed in `PackageName` will be modified so all /// semver-compatible versions are listed the same way. - /// - /// The second element in the returned tuple is this interface's package's - /// version. - fn semver_track(&self, id: InterfaceId) -> Option<((PackageName, String), &Version)> { + fn semver_track( + &self, + id: InterfaceId, + use_canonical_version: bool, + ) -> Option<((String, String), &Version)> { let iface = &self.interfaces[id]; let pkg = &self.packages[iface.package?]; let version = pkg.name.version.as_ref()?; - let mut name = pkg.name.clone(); - name.version = Some(PackageName::version_compat_track(version)); - Some(((name, iface.name.clone()?), version)) + let version_prefix = if use_canonical_version { + let (prefix, _) = PackageName::canon_version_split(version); + prefix + } else { + PackageName::version_compat_track_string(version) + }; + let pkg_key = format!( + "{}:{}@{}", + pkg.name.namespace, pkg.name.name, version_prefix + ); + Some(((pkg_key, iface.name.clone()?), version)) } /// If `ty` is a definition where it's a `use` from another interface, then diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 5452f9b4a0..d34df0431c 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -208,9 +208,19 @@ struct ComponentEncoderOpts { /// semver ranges. /// /// This is enabled by default. - #[arg(long, require_equals = true, value_name = "true|false")] + #[arg(long, require_equals = true, value_name = "true|false", conflicts_with = "merge_imports_based_on_canonical_version")] merge_imports_based_on_semver: Option>, + /// Merges imports based on canonical version prefixes and emits canonical + /// interface names with version suffixes. + /// + /// When enabled, import/export names use canonical version prefixes (e.g., + /// `wasi:cli/exit@0.2` instead of `wasi:cli/exit@0.2.1`) and the + /// `version_suffix` field is populated in the binary. This also forces + /// merging of imports that share the same canonical version prefix. + #[clap(long, conflicts_with = "merge_imports_based_on_semver")] + merge_imports_based_on_canonical_version: bool, + /// Reject usage of the "legacy" naming scheme of `wit-component` and /// require the new naming scheme to be used. /// @@ -277,7 +287,8 @@ impl ComponentEncoderOpts { self.merge_imports_based_on_semver, true, )) - .realloc_via_memory_grow(self.realloc_via_memory_grow); + .realloc_via_memory_grow(self.realloc_via_memory_grow) + .emit_canonical_names(self.merge_imports_based_on_canonical_version); for (name, wasm) in self.adapters.iter() { encoder.adapter(name, wasm)?; } @@ -816,6 +827,7 @@ pub struct WitOpts { conflicts_with = "exportize", conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "merge_world_imports_based_on_canonical_version", conflicts_with = "generate_nominal_type_ids" )] importize: bool, @@ -840,6 +852,7 @@ pub struct WitOpts { conflicts_with = "exportize", conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "merge_world_imports_based_on_canonical_version", conflicts_with = "generate_nominal_type_ids", value_name = "WORLD" )] @@ -859,6 +872,7 @@ pub struct WitOpts { conflicts_with = "importize_world", conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "merge_world_imports_based_on_canonical_version", conflicts_with = "generate_nominal_type_ids" )] exportize: bool, @@ -889,6 +903,7 @@ pub struct WitOpts { conflicts_with = "importize_world", conflicts_with = "exportize", conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "merge_world_imports_based_on_canonical_version", conflicts_with = "generate_nominal_type_ids", value_name = "WORLD" )] @@ -908,10 +923,30 @@ pub struct WitOpts { conflicts_with = "exportize", conflicts_with = "exportize_world", conflicts_with = "generate_nominal_type_ids", + conflicts_with = "merge_world_imports_based_on_canonical_version", value_name = "WORLD" )] merge_world_imports_based_on_semver: Option, + /// Updates the world specified to deduplicate all of its imports based on + /// canonical version prefixes. + /// + /// This option can be used to read a WIT world from a package and update it + /// to deduplicate WIT imports based on their version. This happens by + /// default in the `component new` subcommand for example and this flag can + /// be used to explore outside of that command what's happening to the WIT. + #[clap( + long, + conflicts_with = "importize", + conflicts_with = "importize_world", + conflicts_with = "exportize", + conflicts_with = "exportize_world", + conflicts_with = "generate_nominal_type_ids", + conflicts_with = "merge_world_imports_based_on_semver", + value_name = "WORLD" + )] + merge_world_imports_based_on_canonical_version: Option, + /// Generates unique type IDs for nominal types in the world provided. /// /// This option can be used to affect the `--json` output of this command, @@ -928,6 +963,7 @@ pub struct WitOpts { conflicts_with = "exportize", conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "merge_world_imports_based_on_canonical_version", value_name = "WORLD" )] generate_nominal_type_ids: Option, @@ -996,6 +1032,24 @@ impl WitOpts { .context("failed to merge world imports based on semver")?; let resolve = mem::take(resolve); decoded = DecodedWasm::Component(resolve, world_id); + } else if let Some(world) = &self.merge_world_imports_based_on_canonical_version { + let (resolve, world_id) = match &mut decoded { + DecodedWasm::Component(..) => { + bail!( + "the `--merge-world-imports-based-on-canonical-version` flag is \ + not compatible with a component input" + ); + } + DecodedWasm::WitPackage(resolve, id) => { + let world = resolve.select_world(&[*id], Some(world))?; + (resolve, world) + } + }; + resolve + .merge_world_imports_based_on_canonical_version(world_id) + .context("failed to merge world imports based on canonical version")?; + let resolve = mem::take(resolve); + decoded = DecodedWasm::Component(resolve, world_id); } else if let Some(world) = &self.generate_nominal_type_ids { self.generate_nominal_type_ids(&mut decoded, world)?; } diff --git a/tests/cli/help-component-new-short.wat.stdout b/tests/cli/help-component-new-short.wat.stdout index 3e29a05d33..b76dc99737 100644 --- a/tests/cli/help-component-new-short.wat.stdout +++ b/tests/cli/help-component-new-short.wat.stdout @@ -31,6 +31,9 @@ Options: --merge-imports-based-on-semver[=] Indicates whether imports into the final component are merged based on semver ranges [possible values: true, false] + --merge-imports-based-on-canonical-version + Merges imports based on canonical version prefixes and emits canonical + interface names with version suffixes --reject-legacy-names Reject usage of the "legacy" naming scheme of `wit-component` and require the new naming scheme to be used diff --git a/tests/cli/help-component-new.wat.stdout b/tests/cli/help-component-new.wat.stdout index 6dfa02391d..99a4eae3f4 100644 --- a/tests/cli/help-component-new.wat.stdout +++ b/tests/cli/help-component-new.wat.stdout @@ -102,6 +102,15 @@ Options: [possible values: true, false] + --merge-imports-based-on-canonical-version + Merges imports based on canonical version prefixes and emits canonical + interface names with version suffixes. + + When enabled, import/export names use canonical version prefixes + (e.g., `wasi:cli/exit@0.2` instead of `wasi:cli/exit@0.2.1`) and the + `version_suffix` field is populated in the binary. This also forces + merging of imports that share the same canonical version prefix. + --reject-legacy-names Reject usage of the "legacy" naming scheme of `wit-component` and require the new naming scheme to be used. diff --git a/tests/cli/help-component-wit-short.wat.stdout b/tests/cli/help-component-wit-short.wat.stdout index ad838100ee..34e12519ae 100644 --- a/tests/cli/help-component-wit-short.wat.stdout +++ b/tests/cli/help-component-wit-short.wat.stdout @@ -52,6 +52,9 @@ Options: --merge-world-imports-based-on-semver Updates the world specified to deduplicate all of its imports based on semver versions + --merge-world-imports-based-on-canonical-version + Updates the world specified to deduplicate all of its imports based on + canonical version prefixes --generate-nominal-type-ids Generates unique type IDs for nominal types in the world provided --features diff --git a/tests/cli/help-component-wit.wat.stdout b/tests/cli/help-component-wit.wat.stdout index aa80ec05e3..3862d1df64 100644 --- a/tests/cli/help-component-wit.wat.stdout +++ b/tests/cli/help-component-wit.wat.stdout @@ -133,6 +133,16 @@ Options: can be used to explore outside of that command what's happening to the WIT. + --merge-world-imports-based-on-canonical-version + Updates the world specified to deduplicate all of its imports based on + canonical version prefixes. + + This option can be used to read a WIT world from a package and update + it to deduplicate WIT imports based on their version. This happens by + default in the `component new` subcommand for example and this flag + can be used to explore outside of that command what's happening to the + WIT. + --generate-nominal-type-ids Generates unique type IDs for nominal types in the world provided. From 43279e9b8cc9db1a1d62f81937ebb29f2054737c Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Mon, 24 Aug 2026 15:37:28 -0700 Subject: [PATCH 2/4] fix test --- .../components/canonical-names/component.wat | 24 +++++++++++++++++++ .../canonical-names/component.wit.print | 2 ++ .../components/canonical-names/module.wat | 2 ++ .../components/canonical-names/module.wit | 1 + 4 files changed, 29 insertions(+) diff --git a/crates/wit-component/tests/components/canonical-names/component.wat b/crates/wit-component/tests/components/canonical-names/component.wat index a28ebdba45..d12b6ee9ec 100644 --- a/crates/wit-component/tests/components/canonical-names/component.wat +++ b/crates/wit-component/tests/components/canonical-names/component.wat @@ -11,10 +11,19 @@ (core module $main (;0;) (type (;0;) (func (param i32 i32))) (type (;1;) (func)) + (type (;2;) (func (param i32 i32 i32 i32) (result i32))) (import "a:b/c@0.1.1" "x" (func (;0;) (type 0))) (import "a:b/c@0.1.1" "y" (func (;1;) (type 1))) (memory (;0;) 1) + (export "a:b/c@0.1.0#x" (func 2)) + (export "cabi_realloc" (func 3)) (export "memory" (memory 0)) + (func (;2;) (type 0) (param i32 i32) + unreachable + ) + (func (;3;) (type 2) (param i32 i32 i32 i32) (result i32) + unreachable + ) (@producers (processed-by "wit-component" "$CARGO_PKG_VERSION") (processed-by "my-fake-bindgen" "123.45") @@ -67,6 +76,21 @@ (with "shim" (instance $wit-component-shim-instance)) ) ) + (type (;1;) (func (param "x" string))) + (alias core export $main "a:b/c@0.1.0#x" (core func $a:b/c@0.1.0#x (;3;))) + (alias core export $main "cabi_realloc" (core func $cabi_realloc (;4;))) + (func $"#func2 x" (@name "x") (;2;) (type 1) (canon lift (core func $a:b/c@0.1.0#x) (memory $memory) (realloc $cabi_realloc) string-encoding=utf8)) + (component $a:b/c@0.1-shim-component (;0;) + (type (;0;) (func (param "x" string))) + (import "import-func-x" (func (;0;) (type 0))) + (type (;1;) (func (param "x" string))) + (export (;1;) "x" (func 0) (func (type 1))) + ) + (instance $a:b/c@0.1-shim-instance (;1;) (instantiate $a:b/c@0.1-shim-component + (with "import-func-x" (func $"#func2 x")) + ) + ) + (export $"#instance2 a:b/c@0.1" (@name "a:b/c@0.1") (;2;) "a:b/c@0.1" (versionsuffix ".0") (instance $a:b/c@0.1-shim-instance)) (@producers (processed-by "wit-component" "$CARGO_PKG_VERSION") ) diff --git a/crates/wit-component/tests/components/canonical-names/component.wit.print b/crates/wit-component/tests/components/canonical-names/component.wit.print index 1a2ed3c569..d05e5c7da5 100644 --- a/crates/wit-component/tests/components/canonical-names/component.wit.print +++ b/crates/wit-component/tests/components/canonical-names/component.wit.print @@ -2,4 +2,6 @@ package root:component; world root { import a:b/c@0.1.1; + + export a:b/c@0.1.0; } diff --git a/crates/wit-component/tests/components/canonical-names/module.wat b/crates/wit-component/tests/components/canonical-names/module.wat index fb6c8baf25..ca39f2e6cd 100644 --- a/crates/wit-component/tests/components/canonical-names/module.wat +++ b/crates/wit-component/tests/components/canonical-names/module.wat @@ -4,5 +4,7 @@ (import "a:b/c@0.1.1" "x" (func (param i32 i32))) (import "a:b/c@0.1.1" "y" (func)) + (func (export "a:b/c@0.1.0#x") (param i32 i32) unreachable) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) unreachable) (memory (export "memory") 1) ) diff --git a/crates/wit-component/tests/components/canonical-names/module.wit b/crates/wit-component/tests/components/canonical-names/module.wit index a1606ac7ad..b282197e47 100644 --- a/crates/wit-component/tests/components/canonical-names/module.wit +++ b/crates/wit-component/tests/components/canonical-names/module.wit @@ -3,6 +3,7 @@ package foo:foo; world module { import a:b/c@0.1.0; import a:b/c@0.1.1; + export a:b/c@0.1.0; } package a:b@0.1.0 { From 85c1f2715bf2bccc12b5f1cc0082a0e81e726693 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Mon, 24 Aug 2026 16:43:11 -0700 Subject: [PATCH 3/4] fix --- crates/wit-component/src/encoding.rs | 2 +- crates/wit-component/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index ebba9efb0c..6c7b0bed7f 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -99,7 +99,7 @@ const TLS_BASE_SET: &str = "$set-tls-base"; pub(crate) mod fixup; mod wit; -pub use wit::{encode, encode_world}; +pub use wit::{encode, encode_with_options, encode_world}; mod types; use types::{InstanceTypeEncoder, RootTypeEncoder, TypeEncodingMaps, ValtypeEncoder}; diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 11f57201e5..9a38220e4d 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -17,7 +17,7 @@ mod printing; mod targets; mod validation; -pub use encoding::{ComponentEncoder, LibraryInfo, encode}; +pub use encoding::{ComponentEncoder, LibraryInfo, encode, encode_with_options}; pub use linking::Linker; pub use printing::*; pub use targets::*; From 33eb861b91cc3351c162beaf4cdb7799f836c819 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Mon, 24 Aug 2026 19:42:36 -0700 Subject: [PATCH 4/4] support implements with suffix --- crates/wasmparser/src/validator/component.rs | 8 ++++- crates/wit-component/src/encoding.rs | 30 ++++++++++++------- crates/wit-component/src/encoding/wit.rs | 13 +++++--- crates/wit-parser/src/resolve/mod.rs | 13 +++++--- src/bin/wasm-tools/component.rs | 7 ++++- .../implements-versionsuffix.wast | 18 +++++++++++ tests/cli/merge-canon-with-implements.wit | 24 +++++++++++++++ .../merge-canon-with-implements.wit.stdout | 21 +++++++++++++ .../implements-versionsuffix.wast.json | 24 +++++++++++++++ .../implements-versionsuffix.wast/0.print | 14 +++++++++ .../implements-versionsuffix.wast/1.print | 6 ++++ 11 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 tests/cli/component-model/implements-versionsuffix.wast create mode 100644 tests/cli/merge-canon-with-implements.wit create mode 100644 tests/cli/merge-canon-with-implements.wit.stdout create mode 100644 tests/snapshots/cli/component-model/implements-versionsuffix.wast.json create mode 100644 tests/snapshots/cli/component-model/implements-versionsuffix.wast/0.print create mode 100644 tests/snapshots/cli/component-model/implements-versionsuffix.wast/1.print diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs index 3a984b6859..b73dcd6c73 100644 --- a/crates/wasmparser/src/validator/component.rs +++ b/crates/wasmparser/src/validator/component.rs @@ -4682,7 +4682,13 @@ impl ComponentNameContext { let implements = ComponentName::new_with_features(implements, offset, *features) .with_context(|| format!("`{implements}` is not a valid name"))?; match implements.kind() { - ComponentNameKind::Interface(_) => {} + ComponentNameKind::Interface(iface_name) => { + if let Some(suffix) = version_suffix { + if let Err(e) = iface_name.version(Some(suffix)) { + bail!(offset, "invalid interface version: {e}"); + } + } + } _ => bail!(offset, "name `{implements}` must be an interface"), } } diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 6c7b0bed7f..afd494cabf 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -610,12 +610,18 @@ impl<'a> EncodingState<'a> { .component .type_instance(Some(&format!("ty-{name}")), &ty); + let mut implements = info.implements.clone(); let (import_name, version_suffix) = if self.info.encoder.emit_canonical_names { - let canon_name = resolve - .canon_id_of(interface_id) - .unwrap_or_else(|| name.to_string()); let suffix = resolve.version_suffix_of(interface_id); - (canon_name, suffix) + if implements.is_some() { + implements = resolve.canon_id_of(interface_id); + (name.to_string(), suffix) + } else { + let canon_name = resolve + .canon_id_of(interface_id) + .unwrap_or_else(|| name.to_string()); + (canon_name, suffix) + } } else { (name.to_string(), None) }; @@ -623,7 +629,7 @@ impl<'a> EncodingState<'a> { let instance_idx = self.component.import( wasm_encoder::ComponentExternName { name: import_name.into(), - implements: info.implements.as_deref().map(|s| s.into()), + implements: implements.map(|s| s.into()), external_id: info.external_id.as_deref().map(|s| s.into()), version_suffix: version_suffix.map(|s| s.into()), }, @@ -1008,11 +1014,15 @@ impl<'a> EncodingState<'a> { component_index, imports, ); + let mut implements = resolve.implements_value(key, item); let export_version_suffix = if self.info.encoder.emit_canonical_names { - if let WorldKey::Interface(id) = key { - resolve.version_suffix_of(*id) - } else { - None + match (&implements, key, item) { + (Some(_), _, WorldItem::Interface { id, .. }) => { + implements = resolve.canon_id_of(*id); + resolve.version_suffix_of(*id) + } + (None, WorldKey::Interface(id), _) => resolve.version_suffix_of(*id), + _ => None, } } else { None @@ -1020,7 +1030,7 @@ impl<'a> EncodingState<'a> { let idx = self.component.export( wasm_encoder::ComponentExternName { name: export_name.into(), - implements: resolve.implements_value(key, item).map(|s| s.into()), + implements: implements.map(|s| s.into()), external_id: resolve.external_id_value(key, item).map(|s| s.into()), version_suffix: export_version_suffix.map(|s| s.into()), }, diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index 4add7c3d4b..4eb9d25787 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -154,13 +154,18 @@ fn component_extern_name( ) -> wasm_encoder::ComponentExternName<'static> { if canonical_names { let name = resolve.name_canon_world_key(key); - let version_suffix = match key { - WorldKey::Interface(id) => resolve.version_suffix_of(*id), - WorldKey::Name(_) => None, + let mut implements = resolve.implements_value(key, item); + let version_suffix = match (&implements, key, item) { + (Some(_), _, WorldItem::Interface { id, .. }) => { + implements = resolve.canon_id_of(*id); + resolve.version_suffix_of(*id) + } + (None, WorldKey::Interface(id), _) => resolve.version_suffix_of(*id), + _ => None, }; ComponentExternName { name: name.into(), - implements: resolve.implements_value(key, item).map(|s| s.into()), + implements: implements.map(|s| s.into()), external_id: resolve.external_id_value(key, item).map(|s| s.into()), version_suffix: version_suffix.map(|s| s.into()), } diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index 79868bb207..c5825265a3 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -2583,10 +2583,15 @@ impl Resolve { // Afterwards exports are additionally updated, but only their // dependencies on imports which were remapped. Exports themselves are // not deduplicated and/or removed. - for (key, item) in mem::take(&mut self.worlds[world_id].imports) { - if let WorldItem::Interface { id, .. } = item { - if replacements.contains_key(&id) { - continue; + for (key, mut item) in mem::take(&mut self.worlds[world_id].imports) { + if let WorldItem::Interface { id, .. } = &mut item { + if let Some(&replacement) = replacements.get(id) { + if let WorldKey::Interface(_) = key { + continue; + } + // Labeled imports with `implements` keep their label but + // update to the newer semver-compatible interface. + *id = replacement; } } diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index d34df0431c..98a04c6774 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -208,7 +208,12 @@ struct ComponentEncoderOpts { /// semver ranges. /// /// This is enabled by default. - #[arg(long, require_equals = true, value_name = "true|false", conflicts_with = "merge_imports_based_on_canonical_version")] + #[arg( + long, + require_equals = true, + value_name = "true|false", + conflicts_with = "merge_imports_based_on_canonical_version" + )] merge_imports_based_on_semver: Option>, /// Merges imports based on canonical version prefixes and emits canonical diff --git a/tests/cli/component-model/implements-versionsuffix.wast b/tests/cli/component-model/implements-versionsuffix.wast new file mode 100644 index 0000000000..2568fdb25a --- /dev/null +++ b/tests/cli/component-model/implements-versionsuffix.wast @@ -0,0 +1,18 @@ +;; RUN: wast --assert default --snapshot tests/snapshots % -f cm-canon-names,cm-implements + +;; versionsuffix combined with implements: the suffix refers to the +;; version in implements, not the main label. +(component + (component + (import "my-label" (implements "a:b/c@1") (versionsuffix ".2.3") (instance)) + (import "other" (implements "a:b/c@0.2") (versionsuffix ".3") (instance)) + (instance $a) + (export "x" (implements "a:b/c@1") (versionsuffix ".2.3") (instance $a)) + ) +) + +(component (import "my-label" (implements "a:b/c@1") (versionsuffix ".2.3") (instance))) + +(assert_invalid + (component (import "my-label" (implements "a:b/c@1") (versionsuffix "2.3") (instance))) + "invalid interface version") diff --git a/tests/cli/merge-canon-with-implements.wit b/tests/cli/merge-canon-with-implements.wit new file mode 100644 index 0000000000..d0ca85a8fd --- /dev/null +++ b/tests/cli/merge-canon-with-implements.wit @@ -0,0 +1,24 @@ +// RUN: component wit --merge-world-imports-based-on-canonical-version foo % + +// When merging semver-compatible imports, labeled imports with `implements` +// should be kept (updated to the newer version) rather than removed. + +package test:pkg; + +world foo { + import a:b/c@0.1.0; + import a:b/c@0.1.1; + import my-thing: a:b/c@0.1.0; +} + +package a:b@0.1.0 { + interface c { + f: func(); + } +} + +package a:b@0.1.1 { + interface c { + f: func(); + } +} diff --git a/tests/cli/merge-canon-with-implements.wit.stdout b/tests/cli/merge-canon-with-implements.wit.stdout new file mode 100644 index 0000000000..1e1dae00d1 --- /dev/null +++ b/tests/cli/merge-canon-with-implements.wit.stdout @@ -0,0 +1,21 @@ +/// RUN: component wit --merge-world-imports-based-on-canonical-version foo % +/// When merging semver-compatible imports, labeled imports with `implements` +/// should be kept (updated to the newer version) rather than removed. +package test:pkg; + +world foo { + import a:b/c@0.1.1; + import my-thing: a:b/c@0.1.1; +} +package a:b@0.1.0 { + interface c { + f: func(); + } +} + + +package a:b@0.1.1 { + interface c { + f: func(); + } +} diff --git a/tests/snapshots/cli/component-model/implements-versionsuffix.wast.json b/tests/snapshots/cli/component-model/implements-versionsuffix.wast.json new file mode 100644 index 0000000000..0456d5d081 --- /dev/null +++ b/tests/snapshots/cli/component-model/implements-versionsuffix.wast.json @@ -0,0 +1,24 @@ +{ + "source_filename": "tests/cli/component-model/implements-versionsuffix.wast", + "commands": [ + { + "type": "module", + "line": 5, + "filename": "implements-versionsuffix.0.wasm", + "module_type": "binary" + }, + { + "type": "module", + "line": 14, + "filename": "implements-versionsuffix.1.wasm", + "module_type": "binary" + }, + { + "type": "assert_invalid", + "line": 17, + "filename": "implements-versionsuffix.2.wasm", + "module_type": "binary", + "text": "invalid interface version" + } + ] +} \ No newline at end of file diff --git a/tests/snapshots/cli/component-model/implements-versionsuffix.wast/0.print b/tests/snapshots/cli/component-model/implements-versionsuffix.wast/0.print new file mode 100644 index 0000000000..2202b3c1c6 --- /dev/null +++ b/tests/snapshots/cli/component-model/implements-versionsuffix.wast/0.print @@ -0,0 +1,14 @@ +(component + (component (;0;) + (type (;0;) + (instance) + ) + (import "my-label" (implements "a:b/c@1") (versionsuffix ".2.3") (instance (;0;) (type 0))) + (type (;1;) + (instance) + ) + (import "other" (implements "a:b/c@0.2") (versionsuffix ".3") (instance (;1;) (type 1))) + (instance $a (;2;)) + (export (;3;) "x" (implements "a:b/c@1") (versionsuffix ".2.3") (instance $a)) + ) +) diff --git a/tests/snapshots/cli/component-model/implements-versionsuffix.wast/1.print b/tests/snapshots/cli/component-model/implements-versionsuffix.wast/1.print new file mode 100644 index 0000000000..3404b4bafb --- /dev/null +++ b/tests/snapshots/cli/component-model/implements-versionsuffix.wast/1.print @@ -0,0 +1,6 @@ +(component + (type (;0;) + (instance) + ) + (import "my-label" (implements "a:b/c@1") (versionsuffix ".2.3") (instance (;0;) (type 0))) +)