From ffbcaab033bbb2404c239e1bbe9e031cae21da2a Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Fri, 21 Aug 2026 13:16:54 -0700 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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))) +) From 2eec02eca2deb0b2acf161916158cb8d0ac67933 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Tue, 25 Aug 2026 21:40:05 -0700 Subject: [PATCH 05/16] fix interface encoder; change CLI flag to --emit-canonical-names --- crates/wit-component/src/encoding/wit.rs | 21 +++++++--- crates/wit-component/tests/components.rs | 4 +- .../components/canonical-names/module.wat | 2 +- crates/wit-parser/src/lib.rs | 16 ++++++- src/bin/wasm-tools/component.rs | 42 ++++++++----------- tests/cli/help-component-new-short.wat.stdout | 5 +-- tests/cli/help-component-new.wat.stdout | 5 +-- tests/cli/help-component-wit-short.wat.stdout | 4 +- tests/cli/help-component-wit.wat.stdout | 11 +++-- tests/cli/merge-canon-with-implements.wit | 2 +- .../merge-canon-with-implements.wit.stdout | 2 +- 11 files changed, 63 insertions(+), 51 deletions(-) diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index 4eb9d25787..b3cd455f77 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -244,15 +244,24 @@ impl Encoder<'_> { for interface in interfaces { encoder.interface = Some(interface); let iface = &self.resolve.interfaces[interface]; - let name = if self.canonical_names { - self.resolve.canon_id_of(interface).unwrap() + let extern_name = if self.canonical_names { + let name = self.resolve.canon_id_of(interface).unwrap(); + let version_suffix = self.resolve.version_suffix_of(interface); + ComponentExternName { + name: name.into(), + implements: None, + external_id: None, + version_suffix: version_suffix.map(|s| s.into()), + } } else { - self.resolve.id_of(interface).unwrap() + ComponentExternName::from(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(extern_name, ComponentTypeRef::Instance(idx)); } else { encoder.push_instance(); for (_, id) in iface.types.iter() { @@ -263,7 +272,9 @@ 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(extern_name, ComponentTypeRef::Instance(idx)); } } diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs index 35cdb095bf..d2377de621 100644 --- a/crates/wit-component/tests/components.rs +++ b/crates/wit-component/tests/components.rs @@ -116,7 +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) + .emit_canonical_names(config.emit_canonical_names) .module(&module)?; for adapter in adapters { let (name, wasm) = read_name_and_module("adapt-", &adapter?, &resolve, pkg_id)?; @@ -248,7 +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, + emit_canonical_names: bool, } /// Reads the configuration for the test located at `path`. diff --git a/crates/wit-component/tests/components/canonical-names/module.wat b/crates/wit-component/tests/components/canonical-names/module.wat index ca39f2e6cd..201944ec96 100644 --- a/crates/wit-component/tests/components/canonical-names/module.wat +++ b/crates/wit-component/tests/components/canonical-names/module.wat @@ -1,4 +1,4 @@ -;;! merge-imports-based-on-canonical-version = true +;;! emit-canonical-names = true (module (import "a:b/c@0.1.1" "x" (func (param i32 i32))) diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index f95db12682..8befd4a25c 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -1605,16 +1605,22 @@ mod test { ("1".to_string(), ".2.3".to_string()) ); + let v = Version::parse("101.201.301").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("101".to_string(), ".201.301".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(); + let v = Version::parse("0.10.0").unwrap(); assert_eq!( PackageName::canon_version_split(&v), - ("0.1".to_string(), ".0".to_string()) + ("0.10".to_string(), ".0".to_string()) ); let v = Version::parse("0.0.1-alpha").unwrap(); @@ -1634,5 +1640,11 @@ mod test { PackageName::canon_version_split(&v), ("1".to_string(), ".0.0-beta.1".to_string()) ); + + let v = Version::parse("0.0.100-beta+build.1").unwrap(); + assert_eq!( + PackageName::canon_version_split(&v), + ("0.0.100".to_string(), "-beta+build.1".to_string()) + ); } } diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 98a04c6774..b95ea0e9b6 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -208,23 +208,17 @@ 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")] merge_imports_based_on_semver: Option>, - /// Merges imports based on canonical version prefixes and emits canonical - /// interface names with version suffixes. + /// 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, + #[clap(long)] + emit_canonical_names: bool, /// Reject usage of the "legacy" naming scheme of `wit-component` and /// require the new naming scheme to be used. @@ -293,7 +287,12 @@ impl ComponentEncoderOpts { true, )) .realloc_via_memory_grow(self.realloc_via_memory_grow) - .emit_canonical_names(self.merge_imports_based_on_canonical_version); + .emit_canonical_names(self.emit_canonical_names); + if self.emit_canonical_names + && matches!(self.merge_imports_based_on_semver, Some(Some(false))) + { + bail!("cannot use `--emit-canonical-names` with `--merge-imports-based-on-semver=false`"); + } for (name, wasm) in self.adapters.iter() { encoder.adapter(name, wasm)?; } @@ -832,7 +831,6 @@ 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, @@ -857,7 +855,6 @@ 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" )] @@ -877,7 +874,6 @@ 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, @@ -908,7 +904,6 @@ 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" )] @@ -928,18 +923,17 @@ 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. + /// canonical version prefixes and emits canonical interface names. /// /// 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. + /// to deduplicate WIT imports based on their canonical version prefix. This + /// is the same merge that happens in `component new` when + /// `--emit-canonical-names` is passed. #[clap( long, conflicts_with = "importize", @@ -947,10 +941,9 @@ pub struct WitOpts { 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, + emit_canonical_names: Option, /// Generates unique type IDs for nominal types in the world provided. /// @@ -968,7 +961,6 @@ 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, @@ -1037,11 +1029,11 @@ 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 { + } else if let Some(world) = &self.emit_canonical_names { let (resolve, world_id) = match &mut decoded { DecodedWasm::Component(..) => { bail!( - "the `--merge-world-imports-based-on-canonical-version` flag is \ + "the `--emit-canonical-names` flag is \ not compatible with a component input" ); } diff --git a/tests/cli/help-component-new-short.wat.stdout b/tests/cli/help-component-new-short.wat.stdout index b76dc99737..9978841fa9 100644 --- a/tests/cli/help-component-new-short.wat.stdout +++ b/tests/cli/help-component-new-short.wat.stdout @@ -31,9 +31,8 @@ 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 + --emit-canonical-names + 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 99a4eae3f4..0b4bf457e9 100644 --- a/tests/cli/help-component-new.wat.stdout +++ b/tests/cli/help-component-new.wat.stdout @@ -102,9 +102,8 @@ 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. + --emit-canonical-names + 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 diff --git a/tests/cli/help-component-wit-short.wat.stdout b/tests/cli/help-component-wit-short.wat.stdout index 34e12519ae..7a6ed803a2 100644 --- a/tests/cli/help-component-wit-short.wat.stdout +++ b/tests/cli/help-component-wit-short.wat.stdout @@ -52,9 +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 + --emit-canonical-names Updates the world specified to deduplicate all of its imports based on - canonical version prefixes + canonical version prefixes and emits canonical interface names --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 3862d1df64..68adca5e68 100644 --- a/tests/cli/help-component-wit.wat.stdout +++ b/tests/cli/help-component-wit.wat.stdout @@ -133,15 +133,14 @@ Options: can be used to explore outside of that command what's happening to the WIT. - --merge-world-imports-based-on-canonical-version + --emit-canonical-names Updates the world specified to deduplicate all of its imports based on - canonical version prefixes. + canonical version prefixes and emits canonical interface names. 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. + it to deduplicate WIT imports based on their canonical version prefix. + This is the same merge that happens in `component new` when + `--emit-canonical-names` is passed. --generate-nominal-type-ids Generates unique type IDs for nominal types in the world provided. diff --git a/tests/cli/merge-canon-with-implements.wit b/tests/cli/merge-canon-with-implements.wit index d0ca85a8fd..7280bb6cc0 100644 --- a/tests/cli/merge-canon-with-implements.wit +++ b/tests/cli/merge-canon-with-implements.wit @@ -1,4 +1,4 @@ -// RUN: component wit --merge-world-imports-based-on-canonical-version foo % +// RUN: component wit --emit-canonical-names foo % // When merging semver-compatible imports, labeled imports with `implements` // should be kept (updated to the newer version) rather than removed. diff --git a/tests/cli/merge-canon-with-implements.wit.stdout b/tests/cli/merge-canon-with-implements.wit.stdout index 1e1dae00d1..2d545ef9a4 100644 --- a/tests/cli/merge-canon-with-implements.wit.stdout +++ b/tests/cli/merge-canon-with-implements.wit.stdout @@ -1,4 +1,4 @@ -/// RUN: component wit --merge-world-imports-based-on-canonical-version foo % +/// RUN: component wit --emit-canonical-names foo % /// When merging semver-compatible imports, labeled imports with `implements` /// should be kept (updated to the newer version) rather than removed. package test:pkg; From b5eddd30e9950939f314ccc3a984f4d7cb2c3c62 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Tue, 25 Aug 2026 21:42:55 -0700 Subject: [PATCH 06/16] fmt --- src/bin/wasm-tools/component.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index b95ea0e9b6..3491481b9f 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -291,7 +291,9 @@ impl ComponentEncoderOpts { if self.emit_canonical_names && matches!(self.merge_imports_based_on_semver, Some(Some(false))) { - bail!("cannot use `--emit-canonical-names` with `--merge-imports-based-on-semver=false`"); + bail!( + "cannot use `--emit-canonical-names` with `--merge-imports-based-on-semver=false`" + ); } for (name, wasm) in self.adapters.iter() { encoder.adapter(name, wasm)?; From 7524b352331a8405c4c4f4f58e4ae3e3dfa65563 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Tue, 1 Sep 2026 20:13:33 -0700 Subject: [PATCH 07/16] switch back to use semver functions --- crates/wasmparser/src/validator/component.rs | 8 +- crates/wit-component/src/encoding.rs | 86 ++++++----- crates/wit-component/src/encoding/wit.rs | 21 +-- crates/wit-component/src/encoding/world.rs | 4 +- crates/wit-parser/src/lib.rs | 83 +++++------ crates/wit-parser/src/resolve/mod.rs | 141 ++++++------------ src/bin/wasm-tools/component.rs | 36 ----- tests/cli/help-component-wit-short.wat.stdout | 3 - tests/cli/help-component-wit.wat.stdout | 9 -- tests/cli/merge-canon-with-implements.wit | 17 ++- .../merge-canon-with-implements.wit.stdout | 23 ++- 11 files changed, 162 insertions(+), 269 deletions(-) diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs index b73dcd6c73..3a984b6859 100644 --- a/crates/wasmparser/src/validator/component.rs +++ b/crates/wasmparser/src/validator/component.rs @@ -4682,13 +4682,7 @@ 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(iface_name) => { - if let Some(suffix) = version_suffix { - if let Err(e) = iface_name.version(Some(suffix)) { - bail!(offset, "invalid interface version: {e}"); - } - } - } + ComponentNameKind::Interface(_) => {} _ => 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 afd494cabf..25457c63fe 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -610,31 +610,41 @@ 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 suffix = resolve.version_suffix_of(interface_id); - if implements.is_some() { - implements = resolve.canon_id_of(interface_id); - (name.to_string(), suffix) + let extern_name = if self.info.encoder.emit_canonical_names { + let name = resolve + .canonicalized_id_of(interface_id) + .unwrap_or_else(|| name.to_string()); + let implements = info + .implements + .map(|id| resolve.canonicalized_id_of(id).unwrap()); + let suffix_id = if let Some(id) = info.implements { + id } else { - let canon_name = resolve - .canon_id_of(interface_id) - .unwrap_or_else(|| name.to_string()); - (canon_name, suffix) + interface_id + }; + wasm_encoder::ComponentExternName { + name: resolve + .canonicalized_id_of(interface_id) + .unwrap_or_else(|| name.to_string()) + .into(), + implements: implements.map(|s| s.into()), + external_id: info.external_id.as_deref().map(|s| s.into()), + version_suffix: resolve.version_suffix_of(suffix_id).map(|s| s.into()), } } else { - (name.to_string(), None) - }; - - let instance_idx = self.component.import( wasm_encoder::ComponentExternName { - name: import_name.into(), - implements: implements.map(|s| s.into()), + name: name.into(), + implements: info + .implements + .as_ref() + .map(|s| resolve.id_of(*s).unwrap().into()), external_id: info.external_id.as_deref().map(|s| s.into()), - version_suffix: version_suffix.map(|s| s.into()), - }, - ComponentTypeRef::Instance(instance_type_idx), - ); + version_suffix: None, + } + }; + let instance_idx = self + .component + .import(extern_name, ComponentTypeRef::Instance(instance_type_idx)); let prev = self.instances.insert(interface_id, instance_idx); assert!(prev.is_none()); Ok(()) @@ -780,7 +790,7 @@ impl<'a> EncodingState<'a> { for export_name in exports { let export_string = if self.info.encoder.emit_canonical_names { - resolve.name_canon_world_key(export_name) + resolve.name_canonicalized_world_key(export_name) } else { resolve.name_world_key(export_name) }; @@ -1014,26 +1024,24 @@ 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 { - 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, + let implements = resolve.implements_interface(key, item); + let extern_name = if self.info.encoder.emit_canonical_names { + wasm_encoder::ComponentExternName { + name: export_name.into(), + implements: implements.map(|id| resolve.canonicalized_id_of(id).unwrap().into()), + external_id: resolve.external_id_value(key, item).map(|s| s.into()), + version_suffix: resolve.version_suffix_value(key, item).map(|s| s.into()), } } else { - None - }; - let idx = self.component.export( wasm_encoder::ComponentExternName { name: export_name.into(), - implements: implements.map(|s| s.into()), + implements: implements.map(|id| resolve.id_of(id).unwrap().into()), external_id: resolve.external_id_value(key, item).map(|s| s.into()), - version_suffix: export_version_suffix.map(|s| s.into()), - }, + version_suffix: None, + } + }; + let idx = self.component.export( + extern_name, ComponentExportKind::Instance, instance_index, None, @@ -3558,11 +3566,7 @@ impl ComponentEncoder { bail!("a module is required when encoding a component"); } - 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) { + 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 b3cd455f77..fdc4226076 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -152,27 +152,18 @@ fn component_extern_name( item: &WorldItem, canonical_names: bool, ) -> wasm_encoder::ComponentExternName<'static> { + let implements = resolve.implements_interface(key, item); if canonical_names { - let name = resolve.name_canon_world_key(key); - 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: implements.map(|s| s.into()), + name: resolve.name_canonicalized_world_key(key).into(), + implements: implements.map(|id| resolve.canonicalized_id_of(id).unwrap().into()), external_id: resolve.external_id_value(key, item).map(|s| s.into()), - version_suffix: version_suffix.map(|s| s.into()), + version_suffix: resolve.version_suffix_value(key, item).map(|s| s.into()), } } else { ComponentExternName { name: resolve.name_world_key(key).into(), - implements: resolve.implements_value(key, item).map(|s| s.into()), + implements: implements.map(|id| resolve.id_of(id).unwrap().into()), external_id: resolve.external_id_value(key, item).map(|s| s.into()), version_suffix: None, } @@ -245,7 +236,7 @@ impl Encoder<'_> { encoder.interface = Some(interface); let iface = &self.resolve.interfaces[interface]; let extern_name = if self.canonical_names { - let name = self.resolve.canon_id_of(interface).unwrap(); + let name = self.resolve.canonicalized_id_of(interface).unwrap(); let version_suffix = self.resolve.version_suffix_of(interface); ComponentExternName { name: name.into(), diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index c95cde01fb..cf07bb4c22 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -49,7 +49,7 @@ pub struct ComponentWorld<'a> { pub struct ImportedInterface { pub lowerings: IndexMap<(String, AbiVariant), Lowering>, pub interface: Option, - pub implements: Option, + pub implements: Option, pub external_id: Option, } @@ -293,7 +293,7 @@ impl<'a> ComponentWorld<'a> { WorldItem::Function(_) | WorldItem::Type { .. } => None, WorldItem::Interface { id, .. } => Some(*id), }; - let implements = resolve.implements_value(key, item); + let implements = resolve.implements_interface(key, item); // Note that `external_id` is only tracked for interface imports // here. World-level functions and types all share the `None` entry // in `import_map` but each item can have its own `external-id` diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index 8befd4a25c..9d93210b1a 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -274,61 +274,46 @@ impl PackageName { /// determine whether two imports can be merged together. This is /// additionally used when creating components to match up imports in /// core wasm to imports in worlds. - pub fn version_compat_track(version: &Version) -> Version { + pub fn version_compat_track(version: &Version) -> (Version, String) { let mut version = version.clone(); + let build = if version.build.is_empty() { + String::new() + } else { + format!("+{}", version.build) + }; version.build = semver::BuildMetadata::EMPTY; if !version.pre.is_empty() { - return version; + return (version, build); } if version.major != 0 { + let suffix = format!(".{}.{}{}", version.minor, version.patch, build); version.minor = 0; version.patch = 0; - return version; + return (version, suffix); } if version.minor != 0 { + let suffix = format!(".{}{}", version.patch, build); version.patch = 0; - return version; + return (version, suffix); } - version + (version, build) } /// Returns the string corresponding to /// [`PackageName::version_compat_track`]. This is done to match the /// component model's expected naming scheme of imports and exports. - pub fn version_compat_track_string(version: &Version) -> String { - let version = Self::version_compat_track(version); + pub fn version_compat_track_string(version: &Version) -> (String, String) { + let (version, suffix) = Self::version_compat_track(version); if !version.pre.is_empty() { - return version.to_string(); + return (version.to_string(), suffix); } if version.major != 0 { - return format!("{}", version.major); + return (format!("{}", version.major), suffix); } if version.minor != 0 { - return format!("{}.{}", version.major, version.minor); + return (format!("{}.{}", version.major, version.minor), suffix); } - 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) + (version.to_string(), suffix) } } @@ -1601,50 +1586,50 @@ mod test { let v = Version::parse("1.2.3").unwrap(); assert_eq!( - PackageName::canon_version_split(&v), + PackageName::version_compat_track_string(&v), ("1".to_string(), ".2.3".to_string()) ); let v = Version::parse("101.201.301").unwrap(); assert_eq!( - PackageName::canon_version_split(&v), + PackageName::version_compat_track_string(&v), ("101".to_string(), ".201.301".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()) + PackageName::version_compat_track_string(&v), + ("0.2.6-rc.1".to_string(), "".to_string()) ); - let v = Version::parse("0.10.0").unwrap(); + let v = Version::parse("0.10.0+build.1").unwrap(); assert_eq!( - PackageName::canon_version_split(&v), - ("0.10".to_string(), ".0".to_string()) + PackageName::version_compat_track_string(&v), + ("0.10".to_string(), ".0+build.1".to_string()) ); - let v = Version::parse("0.0.1-alpha").unwrap(); + let v = Version::parse("0.0.1-alpha+build.1").unwrap(); assert_eq!( - PackageName::canon_version_split(&v), - ("0.0.1".to_string(), "-alpha".to_string()) + PackageName::version_compat_track_string(&v), + ("0.0.1-alpha".to_string(), "+build.1".to_string()) ); let v = Version::parse("0.0.0").unwrap(); assert_eq!( - PackageName::canon_version_split(&v), + PackageName::version_compat_track_string(&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()) + PackageName::version_compat_track_string(&v), + ("1.0.0-beta.1".to_string(), "".to_string()) ); - let v = Version::parse("0.0.100-beta+build.1").unwrap(); + let v = Version::parse("0.0.100+build.1").unwrap(); assert_eq!( - PackageName::canon_version_split(&v), - ("0.0.100".to_string(), "-beta+build.1".to_string()) + PackageName::version_compat_track_string(&v), + ("0.0.100".to_string(), "+build.1".to_string()) ); } } diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index c5825265a3..265f6a1fba 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1293,7 +1293,7 @@ impl Resolve { base.push_str(name); if let Some(version) = &package.name.version { base.push_str("@"); - let string = PackageName::version_compat_track_string(version); + let (string, _) = PackageName::version_compat_track_string(version); base.push_str(&string); } base @@ -1550,73 +1550,47 @@ impl Resolve { } } - /// Returns the canonical interface ID using [`PackageName::canon_version_split`]. + /// Returns the component model `implements` interface for the world import of + /// `key` and `item`. /// - /// 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"), + /// See the component model explainer and 🏷️ for more information on this feature. + pub fn implements_interface(&self, key: &WorldKey, item: &WorldItem) -> Option { + if let WorldKey::Name(_) = key { + if let WorldItem::Interface { id, .. } = item { + if self.interfaces[*id].name.is_some() { + return Some(*id); + } + } } + None } - /// Returns the version suffix for the given interface's package version, - /// using [`PackageName::canon_version_split`]. + /// Returns the component model `version-suffix` value for the interface `id`. /// - /// 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) - } + /// See the component model explainer and 🔗 for more information on this feature. + pub fn version_suffix_of(&self, id: InterfaceId) -> Option { + let pkg = self.interfaces[id].package?; + let version = self.packages[pkg].name.version.as_ref()?; + let (_, suffix) = PackageName::version_compat_track(version); + Some(suffix) } - /// Returns the component model `implements` value for the world import of + /// Returns the component model `version-suffix` value for the world import of /// `key` and `item`. /// - /// See the component model explainer and 🏷️ for more information on this feature. - pub fn implements_value(&self, key: &WorldKey, item: &WorldItem) -> Option { - if let WorldKey::Name(_) = key { - if let WorldItem::Interface { id, .. } = item { - if self.interfaces[*id].name.is_some() { - return Some(self.id_of(*id).unwrap().into()); + /// See the component model explainer and 🔗 for more information on this feature. + pub fn version_suffix_value(&self, key: &WorldKey, item: &WorldItem) -> Option { + let interface_id = match key { + WorldKey::Interface(id) => *id, + WorldKey::Name(_) => { + if let WorldItem::Interface { id, .. } = item { + *id + } else { + return None; } } - } - None + }; + self.version_suffix_of(interface_id) } /// Returns the component model `external-id` value for the world import of @@ -2462,23 +2436,6 @@ 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 @@ -2489,14 +2446,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<(String, String), (&Version, InterfaceId)> = HashMap::new(); + let mut semver_tracks = 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, use_canonical_version) { + let (track, version) = match self.semver_track(iface_id) { Some(track) => track, None => continue, }; @@ -2527,7 +2484,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, use_canonical_version).unwrap(); + let (track, _) = self.semver_track(id).unwrap(); let (_, latest) = semver_tracks[&track]; let prev = replacements.insert(id, latest); assert!(prev.is_none()); @@ -2583,15 +2540,13 @@ 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, 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) { + for (key, item) in mem::take(&mut self.worlds[world_id].imports) { + if let WorldItem::Interface { id, .. } = item { + if replacements.contains_key(&id) { if let WorldKey::Interface(_) = key { continue; } - // Labeled imports with `implements` keep their label but - // update to the newer semver-compatible interface. - *id = replacement; + // Keep labeled imports with `implements` version unchanged } } @@ -2651,25 +2606,13 @@ 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. - fn semver_track( - &self, - id: InterfaceId, - use_canonical_version: bool, - ) -> Option<((String, String), &Version)> { + fn semver_track(&self, id: InterfaceId) -> Option<((PackageName, String), &Version)> { let iface = &self.interfaces[id]; let pkg = &self.packages[iface.package?]; let version = pkg.name.version.as_ref()?; - 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)) + let mut name = pkg.name.clone(); + name.version = Some(PackageName::version_compat_track(version).0); + Some(((name, 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 3491481b9f..2f8402a0db 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -929,24 +929,6 @@ pub struct WitOpts { )] merge_world_imports_based_on_semver: Option, - /// Updates the world specified to deduplicate all of its imports based on - /// canonical version prefixes and emits canonical interface names. - /// - /// This option can be used to read a WIT world from a package and update it - /// to deduplicate WIT imports based on their canonical version prefix. This - /// is the same merge that happens in `component new` when - /// `--emit-canonical-names` is passed. - #[clap( - long, - conflicts_with = "importize", - conflicts_with = "importize_world", - conflicts_with = "exportize", - conflicts_with = "exportize_world", - conflicts_with = "generate_nominal_type_ids", - value_name = "WORLD" - )] - emit_canonical_names: 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, @@ -1031,24 +1013,6 @@ 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.emit_canonical_names { - let (resolve, world_id) = match &mut decoded { - DecodedWasm::Component(..) => { - bail!( - "the `--emit-canonical-names` 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-wit-short.wat.stdout b/tests/cli/help-component-wit-short.wat.stdout index 7a6ed803a2..ad838100ee 100644 --- a/tests/cli/help-component-wit-short.wat.stdout +++ b/tests/cli/help-component-wit-short.wat.stdout @@ -52,9 +52,6 @@ Options: --merge-world-imports-based-on-semver Updates the world specified to deduplicate all of its imports based on semver versions - --emit-canonical-names - Updates the world specified to deduplicate all of its imports based on - canonical version prefixes and emits canonical interface names --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 68adca5e68..aa80ec05e3 100644 --- a/tests/cli/help-component-wit.wat.stdout +++ b/tests/cli/help-component-wit.wat.stdout @@ -133,15 +133,6 @@ Options: can be used to explore outside of that command what's happening to the WIT. - --emit-canonical-names - Updates the world specified to deduplicate all of its imports based on - canonical version prefixes and emits canonical interface names. - - This option can be used to read a WIT world from a package and update - it to deduplicate WIT imports based on their canonical version prefix. - This is the same merge that happens in `component new` when - `--emit-canonical-names` is passed. - --generate-nominal-type-ids Generates unique type IDs for nominal types in the world provided. diff --git a/tests/cli/merge-canon-with-implements.wit b/tests/cli/merge-canon-with-implements.wit index 7280bb6cc0..8e4dcb1f08 100644 --- a/tests/cli/merge-canon-with-implements.wit +++ b/tests/cli/merge-canon-with-implements.wit @@ -1,7 +1,4 @@ -// RUN: component wit --emit-canonical-names foo % - -// When merging semver-compatible imports, labeled imports with `implements` -// should be kept (updated to the newer version) rather than removed. +// RUN: component wit --merge-world-imports-based-on-semver foo % package test:pkg; @@ -9,6 +6,9 @@ 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; + import my-thing-2: a:b/c@0.1.2; + export my-export: a:b/c@0.1.1; + export my-export-2: a:b/c@0.1.0; } package a:b@0.1.0 { @@ -20,5 +20,14 @@ package a:b@0.1.0 { package a:b@0.1.1 { interface c { f: func(); + g: func(); + } +} + +package a:b@0.1.2 { + interface c { + f: func(); + g: func(); + h: func(); } } diff --git a/tests/cli/merge-canon-with-implements.wit.stdout b/tests/cli/merge-canon-with-implements.wit.stdout index 2d545ef9a4..fe6fbde914 100644 --- a/tests/cli/merge-canon-with-implements.wit.stdout +++ b/tests/cli/merge-canon-with-implements.wit.stdout @@ -1,11 +1,13 @@ -/// RUN: component wit --emit-canonical-names foo % -/// When merging semver-compatible imports, labeled imports with `implements` -/// should be kept (updated to the newer version) rather than removed. +/// RUN: component wit --merge-world-imports-based-on-semver foo % package test:pkg; world foo { import a:b/c@0.1.1; - import my-thing: a:b/c@0.1.1; + import my-thing: a:b/c@0.1.0; + import my-thing-2: a:b/c@0.1.2; + + export my-export: a:b/c@0.1.1; + export my-export-2: a:b/c@0.1.0; } package a:b@0.1.0 { interface c { @@ -17,5 +19,18 @@ package a:b@0.1.0 { package a:b@0.1.1 { interface c { f: func(); + + g: func(); + } +} + + +package a:b@0.1.2 { + interface c { + f: func(); + + g: func(); + + h: func(); } } From 54492be59b7ac3a73cffad5358674f8f5696c02e Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Tue, 1 Sep 2026 20:40:44 -0700 Subject: [PATCH 08/16] fix --- crates/wasmparser/src/validator/component.rs | 29 +++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs index 3a984b6859..d36746a73a 100644 --- a/crates/wasmparser/src/validator/component.rs +++ b/crates/wasmparser/src/validator/component.rs @@ -4663,7 +4663,7 @@ impl ComponentNameContext { } } - if let Some(implements) = implements { + let implements_name = if let Some(implements) = implements { require_feature::cm_implements( *features, "the `cm-implements` feature is not active", @@ -4685,7 +4685,10 @@ impl ComponentNameContext { ComponentNameKind::Interface(_) => {} _ => bail!(offset, "name `{implements}` must be an interface"), } - } + Some(implements) + } else { + None + }; if let Some(_) = version_suffix { require_feature::cm_canon_names( @@ -4709,8 +4712,15 @@ impl ComponentNameContext { // Validate that the kebab name, if it has structure such as // `[method]a.b`, is indeed valid with respect to known resources. - self.validate(&kebab, version_suffix, ty, types, offset) - .with_context(|| format!("{} name `{kebab}` is not valid", kind.desc()))?; + self.validate( + &kebab, + version_suffix, + implements_name.as_ref(), + ty, + types, + offset, + ) + .with_context(|| format!("{} name `{kebab}` is not valid", kind.desc()))?; // Top-level kebab-names must all be unique, even between both imports // and exports ot a component. For those names consult the `kebab_names` @@ -4753,6 +4763,7 @@ impl ComponentNameContext { &self, name: &ComponentName, version_suffix: Option<&str>, + implements: Option<&ComponentName>, ty: &ComponentEntityType, types: &TypeAlloc, offset: u64, @@ -4765,6 +4776,16 @@ impl ComponentNameContext { Ok(&types[id]) }; + // When an `implements` is present, validate the `version_suffix` + // against the implements interface name rather than the main name. + if let Some(implements) = implements { + if let ComponentNameKind::Interface(iface) = implements.kind() { + if let Err(e) = iface.version(version_suffix) { + bail!(offset, "invalid interface version: {e}"); + } + } + } + match name.kind() { // No validation necessary for these styles of names ComponentNameKind::Label(_) From 523adc4b4bdd3fd17ef9dc057a6a8f9654f0223c Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Tue, 1 Sep 2026 21:58:29 -0700 Subject: [PATCH 09/16] fix --- crates/wit-component/src/validation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs index 0b611b8748..cb9ef19819 100644 --- a/crates/wit-component/src/validation.rs +++ b/crates/wit-component/src/validation.rs @@ -2528,8 +2528,8 @@ impl NameMangling for Legacy { }; // Test if the two semver versions are compatible - let module_compat = PackageName::version_compat_track(&module_version); - let pkg_compat = PackageName::version_compat_track(pkg_version); + let (module_compat, _) = PackageName::version_compat_track(&module_version); + let (pkg_compat, _) = PackageName::version_compat_track(pkg_version); if module_compat == pkg_compat { return Ok((key.clone(), id)); } From c056255464dedba7e91ec986fb7aa4522480fc55 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Tue, 1 Sep 2026 22:12:49 -0700 Subject: [PATCH 10/16] fix --- crates/wit-component/src/encoding.rs | 9 ++------- crates/wit-component/src/encoding/world.rs | 2 +- crates/wit-parser/src/resolve/mod.rs | 3 +++ src/bin/wasm-tools/component.rs | 10 +--------- tests/cli/help-component-new.wat.stdout | 3 +-- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 25457c63fe..e7abb4660b 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -623,10 +623,7 @@ impl<'a> EncodingState<'a> { interface_id }; wasm_encoder::ComponentExternName { - name: resolve - .canonicalized_id_of(interface_id) - .unwrap_or_else(|| name.to_string()) - .into(), + name: name.into(), implements: implements.map(|s| s.into()), external_id: info.external_id.as_deref().map(|s| s.into()), version_suffix: resolve.version_suffix_of(suffix_id).map(|s| s.into()), @@ -3404,9 +3401,7 @@ impl ComponentEncoder { /// /// 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. + /// `version_suffix` field is populated. /// /// This is disabled by default. pub fn emit_canonical_names(&mut self, emit: bool) -> &mut Self { diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index cf07bb4c22..d37b89c84a 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -307,7 +307,7 @@ impl<'a> ComponentWorld<'a> { .or_insert_with(|| ImportedInterface { interface: interface_id, lowerings: Default::default(), - implements: implements.clone(), + implements, external_id: external_id.clone(), }); assert_eq!(interface.interface, interface_id); diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index 265f6a1fba..e960cf5416 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -2606,6 +2606,9 @@ 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)> { let iface = &self.interfaces[id]; let pkg = &self.packages[iface.package?]; diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 2f8402a0db..964fe2b2b1 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -215,8 +215,7 @@ struct ComponentEncoderOpts { /// /// 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. + /// `version_suffix` field is populated in the binary. #[clap(long)] emit_canonical_names: bool, @@ -288,13 +287,6 @@ impl ComponentEncoderOpts { )) .realloc_via_memory_grow(self.realloc_via_memory_grow) .emit_canonical_names(self.emit_canonical_names); - if self.emit_canonical_names - && matches!(self.merge_imports_based_on_semver, Some(Some(false))) - { - bail!( - "cannot use `--emit-canonical-names` with `--merge-imports-based-on-semver=false`" - ); - } for (name, wasm) in self.adapters.iter() { encoder.adapter(name, wasm)?; } diff --git a/tests/cli/help-component-new.wat.stdout b/tests/cli/help-component-new.wat.stdout index 0b4bf457e9..0d4de10376 100644 --- a/tests/cli/help-component-new.wat.stdout +++ b/tests/cli/help-component-new.wat.stdout @@ -107,8 +107,7 @@ Options: 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. + `version_suffix` field is populated in the binary. --reject-legacy-names Reject usage of the "legacy" naming scheme of `wit-component` and From 83a647fac62103db97a151fd654d8e3cc94b90d4 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Thu, 3 Sep 2026 12:22:36 -0700 Subject: [PATCH 11/16] add canonical_names argument to encode --- crates/wasmparser/src/validator/component.rs | 58 ++++++++----------- crates/wit-component/src/encoding.rs | 2 +- crates/wit-component/src/encoding/wit.rs | 25 ++------ crates/wit-component/src/lib.rs | 5 +- crates/wit-component/src/metadata.rs | 3 +- crates/wit-component/src/semver_check.rs | 14 +++-- crates/wit-component/src/targets.rs | 9 ++- crates/wit-smith/src/config.rs | 4 ++ crates/wit-smith/src/lib.rs | 3 +- src/bin/wasm-tools/component.rs | 30 ++++++++-- src/bin/wasm-tools/wit_dylib.rs | 5 ++ .../cli/help-component-embed-short.wat.stdout | 2 + tests/cli/help-component-embed.wat.stdout | 7 +++ tests/cli/help-component-wit-short.wat.stdout | 2 + tests/cli/help-component-wit.wat.stdout | 7 +++ 15 files changed, 107 insertions(+), 69 deletions(-) diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs index d36746a73a..753990f0b9 100644 --- a/crates/wasmparser/src/validator/component.rs +++ b/crates/wasmparser/src/validator/component.rs @@ -4663,7 +4663,19 @@ impl ComponentNameContext { } } - let implements_name = if let Some(implements) = implements { + if let Some(_) = version_suffix { + require_feature::cm_canon_names( + *features, + "the `cm-canon-names` feature is not active", + offset, + )?; + match ty { + ComponentEntityType::Instance(_) => {} + _ => bail!(offset, "only instances can have an `versionsuffix`"), + } + } + + if let Some(implements) = implements { require_feature::cm_implements( *features, "the `cm-implements` feature is not active", @@ -4682,7 +4694,15 @@ 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(_) => { + if let Some(version) = version_suffix { + if let ComponentNameKind::Interface(iface) = implements.kind() { + if let Err(e) = iface.version(Some(version)) { + bail!(offset, "invalid interface version: {e}"); + } + } + } + } _ => bail!(offset, "name `{implements}` must be an interface"), } Some(implements) @@ -4690,18 +4710,6 @@ impl ComponentNameContext { None }; - if let Some(_) = version_suffix { - require_feature::cm_canon_names( - *features, - "the `cm-canon-names` feature is not active", - offset, - )?; - match ty { - ComponentEntityType::Instance(_) => {} - _ => bail!(offset, "only instances can have an `versionsuffix`"), - } - } - if let Some(_) = external_id { require_feature::cm_implements( *features, @@ -4712,15 +4720,8 @@ impl ComponentNameContext { // Validate that the kebab name, if it has structure such as // `[method]a.b`, is indeed valid with respect to known resources. - self.validate( - &kebab, - version_suffix, - implements_name.as_ref(), - ty, - types, - offset, - ) - .with_context(|| format!("{} name `{kebab}` is not valid", kind.desc()))?; + self.validate(&kebab, version_suffix, ty, types, offset) + .with_context(|| format!("{} name `{kebab}` is not valid", kind.desc()))?; // Top-level kebab-names must all be unique, even between both imports // and exports ot a component. For those names consult the `kebab_names` @@ -4763,7 +4764,6 @@ impl ComponentNameContext { &self, name: &ComponentName, version_suffix: Option<&str>, - implements: Option<&ComponentName>, ty: &ComponentEntityType, types: &TypeAlloc, offset: u64, @@ -4776,16 +4776,6 @@ impl ComponentNameContext { Ok(&types[id]) }; - // When an `implements` is present, validate the `version_suffix` - // against the implements interface name rather than the main name. - if let Some(implements) = implements { - if let ComponentNameKind::Interface(iface) = implements.kind() { - if let Err(e) = iface.version(version_suffix) { - bail!(offset, "invalid interface version: {e}"); - } - } - } - match name.kind() { // No validation necessary for these styles of names ComponentNameKind::Label(_) diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index e7abb4660b..4793c46f42 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_with_options, encode_world}; +pub use wit::{encode, encode_world}; mod types; use types::{InstanceTypeEncoder, RootTypeEncoder, TypeEncodingMaps, ValtypeEncoder}; diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index fdc4226076..84bc4174d1 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -24,17 +24,8 @@ 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> { - 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)?; +pub fn encode(resolve: &Resolve, package: PackageId, canonical_names: bool) -> Result> { + let mut component = encode_component(resolve, package, canonical_names)?; component.raw_custom_section(&crate::base_producers().raw_custom_section()); Ok(component.finish()) } @@ -57,7 +48,7 @@ pub fn encode_with_options( /// /// The binary returned can be [`decode`d](crate::decode) to recover the WIT /// package provided. -pub fn encode_component_with_options( +pub fn encode_component( resolve: &Resolve, package: PackageId, canonical_names: bool, @@ -80,12 +71,7 @@ pub fn encode_component_with_options( } /// Encodes a `world` as a component type. -pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> 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( +pub fn encode_world( resolve: &Resolve, world_id: WorldId, canonical_names: bool, @@ -190,8 +176,7 @@ 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_with_options(self.resolve, world, self.canonical_names)?; + let component_ty = encode_world(self.resolve, world, self.canonical_names)?; let world = &self.resolve.worlds[world]; let mut wrapper = ComponentType::new(); diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 9a38220e4d..f5affd15b9 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, encode_with_options}; +pub use encoding::{ComponentEncoder, LibraryInfo, encode}; pub use linking::Linker; pub use printing::*; pub use targets::*; @@ -95,8 +95,9 @@ pub fn embed_component_metadata( wit_resolver: &Resolve, world: WorldId, encoding: StringEncoding, + canonical_names: bool, ) -> Result<()> { - let encoded = metadata::encode(&wit_resolver, world, encoding, None)?; + let encoded = metadata::encode(&wit_resolver, world, encoding, None, canonical_names)?; let section = wasm_encoder::CustomSection { name: "component-type".into(), diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs index 361facbc78..a613f282eb 100644 --- a/crates/wit-component/src/metadata.rs +++ b/crates/wit-component/src/metadata.rs @@ -273,8 +273,9 @@ pub fn encode( world: WorldId, string_encoding: StringEncoding, extra_producers: Option<&Producers>, + canonical_names: bool, ) -> Result> { - let ty = crate::encoding::encode_world(resolve, world)?; + let ty = crate::encoding::encode_world(resolve, world, canonical_names)?; let world = &resolve.worlds[world]; let mut outer_ty = ComponentType::new(); diff --git a/crates/wit-component/src/semver_check.rs b/crates/wit-component/src/semver_check.rs index 8fb4cb1c1e..c4ed41695a 100644 --- a/crates/wit-component/src/semver_check.rs +++ b/crates/wit-component/src/semver_check.rs @@ -66,8 +66,14 @@ pub fn semver_check(mut resolve: Resolve, prev: WorldId, new: WorldId) -> Result // (1) above - create a dummy component which has the shape of `prev`. let mut prev_as_module = dummy_module(&resolve, prev, ManglingAndAbi::Standard32); - embed_component_metadata(&mut prev_as_module, &resolve, prev, StringEncoding::UTF8) - .context("failed to embed component metadata")?; + embed_component_metadata( + &mut prev_as_module, + &resolve, + prev, + StringEncoding::UTF8, + true, + ) + .context("failed to embed component metadata")?; let prev_as_component = ComponentEncoder::default() .module(&prev_as_module) .context("failed to register previous world encoded as a module")? @@ -78,8 +84,8 @@ pub fn semver_check(mut resolve: Resolve, prev: WorldId, new: WorldId) -> Result // (2) above - create a component which imports a component of the shape of // `new`. let test_component_idx = { - let component_ty = - encode_world(&resolve, new).context("failed to encode the new world as a type")?; + let component_ty = encode_world(&resolve, new, true) + .context("failed to encode the new world as a type")?; let mut component = ComponentBuilder::default(); let component_ty_idx = component.type_component(None, &component_ty); component.import( diff --git a/crates/wit-component/src/targets.rs b/crates/wit-component/src/targets.rs index 4212b01822..68446f3445 100644 --- a/crates/wit-component/src/targets.rs +++ b/crates/wit-component/src/targets.rs @@ -7,7 +7,12 @@ use wit_parser::{Resolve, WorldId}; /// This function checks whether `component_to_test` correctly conforms to the world specified. /// It does so by instantiating a generated component that imports a component instance with /// the component type as described by the "target" world. -pub fn targets(resolve: &Resolve, world: WorldId, component_to_test: &[u8]) -> Result<()> { +pub fn targets( + resolve: &Resolve, + world: WorldId, + component_to_test: &[u8], + canonical_names: bool, +) -> Result<()> { let mut root_component = ComponentBuilder::default(); // (1) Embed the component to test. @@ -16,7 +21,7 @@ pub fn targets(resolve: &Resolve, world: WorldId, component_to_test: &[u8]) -> R // (2) Encode the world to a component type and embed a new component which // imports the encoded component type. let test_component_idx = { - let component_ty = encode_world(resolve, world)?; + let component_ty = encode_world(resolve, world, canonical_names)?; let mut component = ComponentBuilder::default(); let component_ty_idx = component.type_component(None, &component_ty); component.import( diff --git a/crates/wit-smith/src/config.rs b/crates/wit-smith/src/config.rs index ba0ec2074f..a7cf686eb0 100644 --- a/crates/wit-smith/src/config.rs +++ b/crates/wit-smith/src/config.rs @@ -33,6 +33,8 @@ pub struct Config { pub world_include: bool, #[cfg_attr(feature = "clap", clap(long, default_value_t = Config::default().implements))] pub implements: bool, + #[cfg_attr(feature = "clap", clap(long, default_value_t = Config::default().canonical_names))] + pub canonical_names: bool, } impl Default for Config { @@ -53,6 +55,7 @@ impl Default for Config { fixed_length_lists: false, world_include: false, implements: false, + canonical_names: false, } } } @@ -75,6 +78,7 @@ impl Arbitrary<'_> for Config { fixed_length_lists: u.arbitrary()?, world_include: false, implements: u.arbitrary()?, + canonical_names: u.arbitrary()?, }) } } diff --git a/crates/wit-smith/src/lib.rs b/crates/wit-smith/src/lib.rs index 7a29f79e48..f54e8a2ca5 100644 --- a/crates/wit-smith/src/lib.rs +++ b/crates/wit-smith/src/lib.rs @@ -44,7 +44,8 @@ pub fn smith(config: &Config, u: &mut Unstructured<'_>) -> Result> { } let pkg = last.unwrap(); - let wasm = wit_component::encode(&resolve, pkg).expect("failed to encode WIT document"); + let wasm = wit_component::encode(&resolve, pkg, config.canonical_names) + .expect("failed to encode WIT document"); // Handle disallowing `stream` here vs not generating it to start // with as it's a bit easier to handle. diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 964fe2b2b1..ace84934ec 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -402,6 +402,14 @@ pub struct EmbedOpts { #[clap(short, long)] world: Option, + /// 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. + #[clap(long)] + emit_canonical_names: bool, + /// Don't read a core wasm module as input, instead generating a "dummy" /// module as a placeholder. /// @@ -475,6 +483,7 @@ impl EmbedOpts { world, self.encoding.unwrap_or(StringEncoding::UTF8), None, + self.emit_canonical_names, )?; self.io.output_wasm(&encoded, false)?; @@ -517,6 +526,7 @@ impl EmbedOpts { &resolve, world, self.encoding.unwrap_or(StringEncoding::UTF8), + self.emit_canonical_names, )?; self.io.output_wasm(&wasm, self.wat)?; @@ -954,6 +964,14 @@ pub struct WitOpts { /// items are otherwise hidden by default. #[clap(long)] all_features: bool, + + /// 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. + #[clap(long)] + emit_canonical_names: bool, } impl WitOpts { @@ -1014,7 +1032,7 @@ impl WitOpts { if self.json { self.emit_json(&decoded)?; } else if self.wasm || self.wat { - self.emit_wasm(&decoded)?; + self.emit_wasm(&decoded, self.emit_canonical_names)?; } else { self.emit_wit(&decoded)?; } @@ -1193,12 +1211,12 @@ impl WitOpts { Ok(()) } - fn emit_wasm(&self, decoded: &DecodedWasm) -> Result<()> { + fn emit_wasm(&self, decoded: &DecodedWasm, canonical_names: bool) -> Result<()> { assert!(self.wasm || self.wat); assert!(self.out_dir.is_none()); let decoded_package = decoded.package(); - let bytes = wit_component::encode(decoded.resolve(), decoded_package)?; + let bytes = wit_component::encode(decoded.resolve(), decoded_package, canonical_names)?; if !self.skip_validation { wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&bytes)?; } @@ -1316,6 +1334,10 @@ pub struct TargetsOpts { #[clap(flatten)] input: wasm_tools::InputArg, + + /// Emits canonical interface names with version suffixes. + #[clap(long)] + emit_canonical_names: bool, } impl TargetsOpts { @@ -1329,7 +1351,7 @@ impl TargetsOpts { let world = resolve.select_world(&[pkg_id], self.world.as_deref())?; let component_to_test = self.input.get_binary_wasm(None)?; - wit_component::targets(&resolve, world, &component_to_test)?; + wit_component::targets(&resolve, world, &component_to_test, self.emit_canonical_names)?; Ok(()) } diff --git a/src/bin/wasm-tools/wit_dylib.rs b/src/bin/wasm-tools/wit_dylib.rs index db429cdd2f..b42a535bf4 100644 --- a/src/bin/wasm-tools/wit_dylib.rs +++ b/src/bin/wasm-tools/wit_dylib.rs @@ -45,6 +45,10 @@ pub struct Opts { #[clap(flatten)] dylib_opts: wit_dylib::DylibOpts, + + /// Emits canonical interface names with version suffixes. + #[clap(long)] + emit_canonical_names: bool, } impl Opts { @@ -64,6 +68,7 @@ impl Opts { &resolve, world, self.encoding.unwrap_or(StringEncoding::UTF8), + self.emit_canonical_names, )?; if self.validate { diff --git a/tests/cli/help-component-embed-short.wat.stdout b/tests/cli/help-component-embed-short.wat.stdout index 826192c6dc..221c799409 100644 --- a/tests/cli/help-component-embed-short.wat.stdout +++ b/tests/cli/help-component-embed-short.wat.stdout @@ -27,6 +27,8 @@ Options: The expected string encoding format for the component -w, --world The world that the component uses + --emit-canonical-names + Emits canonical interface names with version suffixes --dummy Don't read a core wasm module as input, instead generating a "dummy" module as a placeholder diff --git a/tests/cli/help-component-embed.wat.stdout b/tests/cli/help-component-embed.wat.stdout index 47166874d3..e110e77a39 100644 --- a/tests/cli/help-component-embed.wat.stdout +++ b/tests/cli/help-component-embed.wat.stdout @@ -94,6 +94,13 @@ Options: such as `wasi:http/proxy` which can select a world from a WIT dependency as well. + --emit-canonical-names + 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. + --dummy Don't read a core wasm module as input, instead generating a "dummy" module as a placeholder. diff --git a/tests/cli/help-component-wit-short.wat.stdout b/tests/cli/help-component-wit-short.wat.stdout index ad838100ee..72a8585c0e 100644 --- a/tests/cli/help-component-wit-short.wat.stdout +++ b/tests/cli/help-component-wit-short.wat.stdout @@ -58,6 +58,8 @@ Options: Features to enable when parsing the `wit` option --all-features Enable all features when parsing the `wit` option + --emit-canonical-names + Emits canonical interface names with version suffixes -h, --help Print help (see more with '--help') diff --git a/tests/cli/help-component-wit.wat.stdout b/tests/cli/help-component-wit.wat.stdout index aa80ec05e3..837d06afa7 100644 --- a/tests/cli/help-component-wit.wat.stdout +++ b/tests/cli/help-component-wit.wat.stdout @@ -156,6 +156,13 @@ Options: This flag enables all `@unstable` features in WIT documents where the items are otherwise hidden by default. + --emit-canonical-names + 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. + -h, --help Print help (see a summary with '-h') From a46e3407e358de51656a53659ccdc176431b03ae Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Thu, 3 Sep 2026 17:26:44 -0700 Subject: [PATCH 12/16] fix test --- crates/wit-component/src/lib.rs | 2 +- crates/wit-component/tests/components.rs | 52 ++++++++++++++--- .../components/canonical-names/component.wat | 51 +++++++++------- .../components/canonical-names/module.wat | 2 + crates/wit-component/tests/interfaces.rs | 5 +- .../tests/interfaces/wasi-http.wat | 58 +++++++++---------- crates/wit-component/tests/linking.rs | 27 ++++++--- crates/wit-component/tests/targets.rs | 2 +- crates/wit-parser/src/resolve/mod.rs | 8 +-- 9 files changed, 129 insertions(+), 78 deletions(-) diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index f5affd15b9..464c43ac86 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -152,7 +152,7 @@ world test-world {} let world = resolver.select_world(&[pkg], Some("test-world"))?; // Embed component metadata - embed_component_metadata(&mut bytes, &resolver, world, StringEncoding::UTF8)?; + embed_component_metadata(&mut bytes, &resolver, world, StringEncoding::UTF8, true)?; // Re-retrieve custom section count, and search for the component-type custom section along the way let mut found_component_section = false; diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs index d2377de621..5b75942bb2 100644 --- a/crates/wit-component/tests/components.rs +++ b/crates/wit-component/tests/components.rs @@ -110,8 +110,9 @@ fn run_test(path: &Path) -> Result<()> { } let mut encoder = ComponentEncoder::default(); (|| -> Result<_> { - let module = read_core_module(&module_path, &resolve, pkg_id) - .with_context(|| format!("failed to read core module at {module_path:?}"))?; + let module = + read_core_module(&module_path, &resolve, pkg_id, config.emit_canonical_names) + .with_context(|| format!("failed to read core module at {module_path:?}"))?; encoder .debug_names(true) .shim_return_call_ref(config.return_call_ref) @@ -119,7 +120,13 @@ fn run_test(path: &Path) -> Result<()> { .emit_canonical_names(config.emit_canonical_names) .module(&module)?; for adapter in adapters { - let (name, wasm) = read_name_and_module("adapt-", &adapter?, &resolve, pkg_id)?; + let (name, wasm) = read_name_and_module( + "adapt-", + &adapter?, + &resolve, + pkg_id, + config.emit_canonical_names, + )?; encoder.adapter(&name, &wasm)?; } encoder.encode() @@ -148,11 +155,23 @@ fn run_test(path: &Path) -> Result<()> { (|| -> Result<_> { for (prefix, path, dl_openable) in libs { - let (name, wasm) = read_name_and_module(prefix, &path, &resolve, pkg_id)?; + let (name, wasm) = read_name_and_module( + prefix, + &path, + &resolve, + pkg_id, + config.emit_canonical_names, + )?; linker.library(&name, &wasm, dl_openable)?; } for path in adapters { - let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg_id)?; + let (name, wasm) = read_name_and_module( + "adapt-", + &path?, + &resolve, + pkg_id, + config.emit_canonical_names, + )?; linker.encoder().adapter(&name, &wasm)?; } @@ -312,8 +331,9 @@ fn read_name_and_module( path: &Path, resolve: &Resolve, pkg: PackageId, + canonical_names: bool, ) -> Result<(String, Vec)> { - let wasm = read_core_module(path, resolve, pkg) + let wasm = read_core_module(path, resolve, pkg, canonical_names) .with_context(|| format!("failed to read core module at {path:?}"))?; let stem = path.file_stem().unwrap().to_str().unwrap(); let contents = fs::read_to_string(path)?; @@ -335,20 +355,34 @@ fn read_name_and_module( /// The `resolve` and `pkg` are the parsed WIT package from this test's /// directory and the `path`'s filename is used to find a WIT document of the /// corresponding name which should have a world that `path` ascribes to. -fn read_core_module(path: &Path, resolve: &Resolve, pkg: PackageId) -> Result> { +fn read_core_module( + path: &Path, + resolve: &Resolve, + pkg: PackageId, + canonical_names: bool, +) -> Result> { let mut wasm = wat::parse_file(path)?; let name = path.file_stem().and_then(|s| s.to_str()).unwrap(); + let mut resolve = resolve.clone(); let world = resolve .select_world(&[pkg], Some(name)) .context("failed to select a world")?; + if canonical_names { + resolve.merge_world_imports_based_on_semver(world)?; + } // Add this producer data to the wit-component metadata so we can make sure it gets through the // translation: let mut producers = wasm_metadata::Producers::empty(); producers.add("processed-by", "my-fake-bindgen", "123.45"); - let encoded = - wit_component::metadata::encode(resolve, world, StringEncoding::UTF8, Some(&producers))?; + let encoded = wit_component::metadata::encode( + &resolve, + world, + StringEncoding::UTF8, + Some(&producers), + canonical_names, + )?; let section = wasm_encoder::CustomSection { name: "component-type".into(), diff --git a/crates/wit-component/tests/components/canonical-names/component.wat b/crates/wit-component/tests/components/canonical-names/component.wat index d12b6ee9ec..47d80a7fef 100644 --- a/crates/wit-component/tests/components/canonical-names/component.wat +++ b/crates/wit-component/tests/components/canonical-names/component.wat @@ -12,16 +12,18 @@ (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))) + (import "a:b/c@0.1.0" "x" (func (;0;) (type 0))) + (import "a:b/c@0.1.0" "y" (func (;1;) (type 1))) + (import "a:b/c@0.1.1" "x" (func (;2;) (type 0))) + (import "a:b/c@0.1.1" "y" (func (;3;) (type 1))) (memory (;0;) 1) - (export "a:b/c@0.1.0#x" (func 2)) - (export "cabi_realloc" (func 3)) + (export "a:b/c@0.1.0#x" (func 4)) + (export "cabi_realloc" (func 5)) (export "memory" (memory 0)) - (func (;2;) (type 0) (param i32 i32) + (func (;4;) (type 0) (param i32 i32) unreachable ) - (func (;3;) (type 2) (param i32 i32 i32 i32) (result i32) + (func (;5;) (type 2) (param i32 i32 i32 i32) (result i32) unreachable ) (@producers @@ -32,9 +34,9 @@ (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 "0" (func $indirect-a:b/c@0.1.0-x)) (export "$imports" (table 0)) - (func $indirect-a:b/c@0.1.1-x (;0;) (type 0) (param i32 i32) + (func $indirect-a:b/c@0.1.0-x (;0;) (type 0) (param i32 i32) local.get 0 local.get 1 i32.const 0 @@ -45,14 +47,21 @@ ) ) (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 core export $wit-component-shim-instance "0" (core func $indirect-a:b/c@0.1.0-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)) + (core instance $a:b/c@0.1.0 (;1;) + (export "x" (func $indirect-a:b/c@0.1.0-x)) (export "y" (func $y)) ) - (core instance $main (;2;) (instantiate $main + (alias export $a:b/c@0.1 "y" (func $"#func1 y" (@name "y") (;1;))) + (core func $"#core-func2 y" (@name "y") (;2;) (canon lower (func $"#func1 y"))) + (core instance $a:b/c@0.1.1 (;2;) + (export "x" (func $indirect-a:b/c@0.1.0-x)) + (export "y" (func $"#core-func2 y")) + ) + (core instance $main (;3;) (instantiate $main + (with "a:b/c@0.1.0" (instance $a:b/c@0.1.0)) (with "a:b/c@0.1.1" (instance $a:b/c@0.1.1)) ) ) @@ -66,20 +75,20 @@ (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")) + (alias export $a:b/c@0.1 "x" (func $x (;2;))) + (core func $"#core-func3 indirect-a:b/c@0.1.0-x" (@name "indirect-a:b/c@0.1.0-x") (;3;) (canon lower (func $x) (memory $memory) string-encoding=utf8)) + (core instance $actual (;4;) + (export "0" (func $"#core-func3 indirect-a:b/c@0.1.0-x")) ) - (core instance $fixup (;4;) (instantiate $wit-component-fixup + (core instance $fixup (;5;) (instantiate $wit-component-fixup (with "actual" (instance $actual)) (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)) + (alias core export $main "a:b/c@0.1.0#x" (core func $a:b/c@0.1.0#x (;4;))) + (alias core export $main "cabi_realloc" (core func $cabi_realloc (;5;))) + (func $"#func3 x" (@name "x") (;3;) (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))) @@ -87,7 +96,7 @@ (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")) + (with "import-func-x" (func $"#func3 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)) diff --git a/crates/wit-component/tests/components/canonical-names/module.wat b/crates/wit-component/tests/components/canonical-names/module.wat index 201944ec96..5db7cf2923 100644 --- a/crates/wit-component/tests/components/canonical-names/module.wat +++ b/crates/wit-component/tests/components/canonical-names/module.wat @@ -1,6 +1,8 @@ ;;! emit-canonical-names = true (module + (import "a:b/c@0.1.0" "x" (func (param i32 i32))) + (import "a:b/c@0.1.0" "y" (func)) (import "a:b/c@0.1.1" "x" (func (param i32 i32))) (import "a:b/c@0.1.1" "y" (func)) diff --git a/crates/wit-component/tests/interfaces.rs b/crates/wit-component/tests/interfaces.rs index d89676df1b..f3cd8baef1 100644 --- a/crates/wit-component/tests/interfaces.rs +++ b/crates/wit-component/tests/interfaces.rs @@ -59,7 +59,7 @@ fn run_test(path: &Path, is_dir: bool) -> Result<()> { // First convert the WIT package to a binary WebAssembly output, then // convert that binary wasm to textual wasm, then assert it matches the // expectation. - let wasm = wit_component::encode(&resolve, package)?; + let wasm = wit_component::encode(&resolve, package, true)?; let wat = wasmprinter::print_bytes(&wasm)?; assert_output(&path.with_extension("wat"), &wat)?; wasmparser::Validator::new_with_features(WasmFeatures::all()) @@ -74,10 +74,9 @@ fn run_test(path: &Path, is_dir: bool) -> Result<()> { let resolve = decoded.resolve(); assert_print(resolve, decoded.package(), path, is_dir)?; - // Finally convert the decoded package to wasm again and make sure it // matches the prior wasm. - let wasm2 = wit_component::encode(resolve, decoded_package)?; + let wasm2 = wit_component::encode(resolve, decoded_package, true)?; if wasm != wasm2 { let wat2 = wasmprinter::print_bytes(&wasm)?; assert_eq!(wat, wat2, "document did not roundtrip correctly"); diff --git a/crates/wit-component/tests/interfaces/wasi-http.wat b/crates/wit-component/tests/interfaces/wasi-http.wat index a52278ac81..cdffdc7831 100644 --- a/crates/wit-component/tests/interfaces/wasi-http.wat +++ b/crates/wit-component/tests/interfaces/wasi-http.wat @@ -6,7 +6,7 @@ (export (;0;) "pollable" (type (sub resource))) ) ) - (import "wasi:io/poll@0.2.0-rc-2023-11-10" (instance (;0;) (type 0))) + (import "wasi:io/poll@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;0;) (type 0))) (alias export 0 "pollable" (type (;1;))) (type (;2;) (instance @@ -18,13 +18,13 @@ (export (;5;) "duration" (type (eq 4))) ) ) - (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (instance (;1;) (type 2))) + (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;1;) (type 2))) (type (;3;) (instance (export (;0;) "error" (type (sub resource))) ) ) - (import "wasi:io/error@0.2.0-rc-2023-11-10" (instance (;2;) (type 3))) + (import "wasi:io/error@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;2;) (type 3))) (alias export 2 "error" (type (;4;))) (type (;5;) (instance @@ -39,7 +39,7 @@ (export (;8;) "output-stream" (type (sub resource))) ) ) - (import "wasi:io/streams@0.2.0-rc-2023-11-10" (instance (;3;) (type 5))) + (import "wasi:io/streams@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;3;) (type 5))) (alias export 1 "duration" (type (;6;))) (alias export 3 "input-stream" (type (;7;))) (alias export 3 "output-stream" (type (;8;))) @@ -239,7 +239,7 @@ (export (;50;) "http-error-code" (func (type 140))) ) ) - (export (;4;) "wasi:http/types@0.2.0-rc-2023-12-05" (instance (type 9))) + (export (;4;) "wasi:http/types@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (type 9))) ) ) (export (;1;) "types" (type 0)) @@ -250,7 +250,7 @@ (export (;0;) "pollable" (type (sub resource))) ) ) - (import "wasi:io/poll@0.2.0-rc-2023-11-10" (instance (;0;) (type 0))) + (import "wasi:io/poll@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;0;) (type 0))) (alias export 0 "pollable" (type (;1;))) (type (;2;) (instance @@ -262,13 +262,13 @@ (export (;5;) "duration" (type (eq 4))) ) ) - (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (instance (;1;) (type 2))) + (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;1;) (type 2))) (type (;3;) (instance (export (;0;) "error" (type (sub resource))) ) ) - (import "wasi:io/error@0.2.0-rc-2023-11-10" (instance (;2;) (type 3))) + (import "wasi:io/error@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;2;) (type 3))) (alias export 2 "error" (type (;4;))) (type (;5;) (instance @@ -283,7 +283,7 @@ (export (;8;) "output-stream" (type (sub resource))) ) ) - (import "wasi:io/streams@0.2.0-rc-2023-11-10" (instance (;3;) (type 5))) + (import "wasi:io/streams@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;3;) (type 5))) (alias export 1 "duration" (type (;6;))) (alias export 3 "input-stream" (type (;7;))) (alias export 3 "output-stream" (type (;8;))) @@ -340,7 +340,7 @@ (export (;48;) "future-incoming-response" (type (sub resource))) ) ) - (import "wasi:http/types@0.2.0-rc-2023-12-05" (instance (;4;) (type 9))) + (import "wasi:http/types@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;4;) (type 9))) (alias export 4 "incoming-request" (type (;10;))) (alias export 4 "response-outparam" (type (;11;))) (type (;12;) @@ -355,7 +355,7 @@ (export (;0;) "handle" (func (type 6))) ) ) - (export (;5;) "wasi:http/incoming-handler@0.2.0-rc-2023-12-05" (instance (type 12))) + (export (;5;) "wasi:http/incoming-handler@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (type 12))) ) ) (export (;3;) "incoming-handler" (type 2)) @@ -366,7 +366,7 @@ (export (;0;) "pollable" (type (sub resource))) ) ) - (import "wasi:io/poll@0.2.0-rc-2023-11-10" (instance (;0;) (type 0))) + (import "wasi:io/poll@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;0;) (type 0))) (alias export 0 "pollable" (type (;1;))) (type (;2;) (instance @@ -378,13 +378,13 @@ (export (;5;) "duration" (type (eq 4))) ) ) - (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (instance (;1;) (type 2))) + (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;1;) (type 2))) (type (;3;) (instance (export (;0;) "error" (type (sub resource))) ) ) - (import "wasi:io/error@0.2.0-rc-2023-11-10" (instance (;2;) (type 3))) + (import "wasi:io/error@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;2;) (type 3))) (alias export 2 "error" (type (;4;))) (type (;5;) (instance @@ -399,7 +399,7 @@ (export (;8;) "output-stream" (type (sub resource))) ) ) - (import "wasi:io/streams@0.2.0-rc-2023-11-10" (instance (;3;) (type 5))) + (import "wasi:io/streams@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;3;) (type 5))) (alias export 1 "duration" (type (;6;))) (alias export 3 "input-stream" (type (;7;))) (alias export 3 "output-stream" (type (;8;))) @@ -456,7 +456,7 @@ (export (;48;) "future-incoming-response" (type (sub resource))) ) ) - (import "wasi:http/types@0.2.0-rc-2023-12-05" (instance (;4;) (type 9))) + (import "wasi:http/types@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;4;) (type 9))) (alias export 4 "outgoing-request" (type (;10;))) (alias export 4 "request-options" (type (;11;))) (alias export 4 "future-incoming-response" (type (;12;))) @@ -480,7 +480,7 @@ (export (;0;) "handle" (func (type 13))) ) ) - (export (;5;) "wasi:http/outgoing-handler@0.2.0-rc-2023-12-05" (instance (type 14))) + (export (;5;) "wasi:http/outgoing-handler@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (type 14))) ) ) (export (;5;) "outgoing-handler" (type 4)) @@ -497,7 +497,7 @@ (export (;1;) "get-random-u64" (func (type 2))) ) ) - (import "wasi:random/random@0.2.0-rc-2023-11-10" (instance (;0;) (type 0))) + (import "wasi:random/random@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;0;) (type 0))) (type (;1;) (instance (export (;0;) "error" (type (sub resource))) @@ -506,7 +506,7 @@ (export (;0;) "[method]error.to-debug-string" (func (type 2))) ) ) - (import "wasi:io/error@0.2.0-rc-2023-11-10" (instance (;1;) (type 1))) + (import "wasi:io/error@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;1;) (type 1))) (type (;2;) (instance (export (;0;) "pollable" (type (sub resource))) @@ -521,7 +521,7 @@ (export (;2;) "poll" (func (type 6))) ) ) - (import "wasi:io/poll@0.2.0-rc-2023-11-10" (instance (;2;) (type 2))) + (import "wasi:io/poll@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;2;) (type 2))) (alias export 1 "error" (type (;3;))) (alias export 2 "pollable" (type (;4;))) (type (;5;) @@ -568,7 +568,7 @@ (export (;14;) "[method]output-stream.blocking-splice" (func (type 24))) ) ) - (import "wasi:io/streams@0.2.0-rc-2023-11-10" (instance (;3;) (type 5))) + (import "wasi:io/streams@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;3;) (type 5))) (alias export 3 "output-stream" (type (;6;))) (type (;7;) (instance @@ -579,7 +579,7 @@ (export (;0;) "get-stdout" (func (type 3))) ) ) - (import "wasi:cli/stdout@0.2.0-rc-2023-12-05" (instance (;4;) (type 7))) + (import "wasi:cli/stdout@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;4;) (type 7))) (type (;8;) (instance (alias outer 1 6 (type (;0;))) @@ -589,7 +589,7 @@ (export (;0;) "get-stderr" (func (type 3))) ) ) - (import "wasi:cli/stderr@0.2.0-rc-2023-12-05" (instance (;5;) (type 8))) + (import "wasi:cli/stderr@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;5;) (type 8))) (alias export 3 "input-stream" (type (;9;))) (type (;10;) (instance @@ -600,7 +600,7 @@ (export (;0;) "get-stdin" (func (type 3))) ) ) - (import "wasi:cli/stdin@0.2.0-rc-2023-12-05" (instance (;6;) (type 10))) + (import "wasi:cli/stdin@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;6;) (type 10))) (type (;11;) (instance (alias outer 1 4 (type (;0;))) @@ -620,7 +620,7 @@ (export (;3;) "subscribe-duration" (func (type 10))) ) ) - (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (instance (;7;) (type 11))) + (import "wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;7;) (type 11))) (alias export 7 "duration" (type (;12;))) (type (;13;) (instance @@ -818,7 +818,7 @@ (export (;50;) "http-error-code" (func (type 140))) ) ) - (import "wasi:http/types@0.2.0-rc-2023-12-05" (instance (;8;) (type 13))) + (import "wasi:http/types@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;8;) (type 13))) (alias export 8 "outgoing-request" (type (;14;))) (alias export 8 "request-options" (type (;15;))) (alias export 8 "future-incoming-response" (type (;16;))) @@ -842,7 +842,7 @@ (export (;0;) "handle" (func (type 13))) ) ) - (import "wasi:http/outgoing-handler@0.2.0-rc-2023-12-05" (instance (;9;) (type 18))) + (import "wasi:http/outgoing-handler@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (;9;) (type 18))) (type (;19;) (instance (type (;0;) (record (field "seconds" u64) (field "nanoseconds" u32))) @@ -852,7 +852,7 @@ (export (;1;) "resolution" (func (type 2))) ) ) - (import "wasi:clocks/wall-clock@0.2.0-rc-2023-11-10" (instance (;10;) (type 19))) + (import "wasi:clocks/wall-clock@0.2.0-rc-2023-11-10" (versionsuffix "") (instance (;10;) (type 19))) (alias export 8 "incoming-request" (type (;20;))) (alias export 8 "response-outparam" (type (;21;))) (type (;22;) @@ -867,7 +867,7 @@ (export (;0;) "handle" (func (type 6))) ) ) - (export (;11;) "wasi:http/incoming-handler@0.2.0-rc-2023-12-05" (instance (type 22))) + (export (;11;) "wasi:http/incoming-handler@0.2.0-rc-2023-12-05" (versionsuffix "") (instance (type 22))) ) ) (export (;0;) "wasi:http/proxy@0.2.0-rc-2023-12-05" (component (type 0))) diff --git a/crates/wit-component/tests/linking.rs b/crates/wit-component/tests/linking.rs index adc7a41bb1..207a4f5d67 100644 --- a/crates/wit-component/tests/linking.rs +++ b/crates/wit-component/tests/linking.rs @@ -147,7 +147,7 @@ world bar { } "#; -fn encode(wat: &str, wit: Option<&str>) -> Result> { +fn encode(wat: &str, wit: Option<&str>, canonical_names: bool) -> Result> { let mut module = wat::parse_str(wat)?; if let Some(wit) = wit { @@ -160,6 +160,7 @@ fn encode(wat: &str, wit: Option<&str>) -> Result> { &resolve, world, StringEncoding::UTF8, + canonical_names, )?; } @@ -168,8 +169,7 @@ fn encode(wat: &str, wit: Option<&str>) -> Result> { Ok(module) } -#[test] -fn linking() -> Result<()> { +fn run_linking(canonical_names: bool) -> Result<()> { let mut linker = wit_component::Linker::default(); linker.encoder().validate(true); for (name, wat, wit) in [ @@ -179,7 +179,7 @@ fn linking() -> Result<()> { ] { linker.library( name, - &encode(wat, wit).with_context(|| name.to_owned())?, + &encode(wat, wit, canonical_names).with_context(|| name.to_owned())?, false, )?; } @@ -226,6 +226,13 @@ fn linking() -> Result<()> { Ok(()) } +#[test] +fn linking() -> Result<()> { + run_linking(false)?; + run_linking(true)?; + Ok(()) +} + const GOT_IMPORT: &str = r#" (module (@dylink.0 @@ -257,8 +264,7 @@ world bar { } "#; -#[test] -fn linking_got_weak() -> Result<()> { +fn run_linking_got_weak(canonical_names: bool) -> Result<()> { let mut linker = wit_component::Linker::default(); linker.encoder().validate(true); for (name, wat, wit) in [ @@ -267,7 +273,7 @@ fn linking_got_weak() -> Result<()> { ] { linker.library( name, - &encode(wat, wit).with_context(|| name.to_owned())?, + &encode(wat, wit, canonical_names).with_context(|| name.to_owned())?, false, )?; } @@ -303,3 +309,10 @@ fn linking_got_weak() -> Result<()> { } Ok(()) } + +#[test] +fn linking_got_weak() -> Result<()> { + run_linking_got_weak(false)?; + run_linking_got_weak(true)?; + Ok(()) +} diff --git a/crates/wit-component/tests/targets.rs b/crates/wit-component/tests/targets.rs index 60ba93fd51..3da64a7799 100644 --- a/crates/wit-component/tests/targets.rs +++ b/crates/wit-component/tests/targets.rs @@ -41,7 +41,7 @@ fn targets() -> Result<()> { let component = wat::parse_file(path.join("test.wat")) .with_context(|| "failed to parse component WAT".to_string())?; - match wit_component::targets(&resolve, world, &component) { + match wit_component::targets(&resolve, world, &component, true) { Ok(_) => { assert!( !test_case.starts_with("error-"), diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index e960cf5416..9adf97344e 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1582,13 +1582,7 @@ impl Resolve { pub fn version_suffix_value(&self, key: &WorldKey, item: &WorldItem) -> Option { let interface_id = match key { WorldKey::Interface(id) => *id, - WorldKey::Name(_) => { - if let WorldItem::Interface { id, .. } = item { - *id - } else { - return None; - } - } + WorldKey::Name(_) => self.implements_interface(key, item)?, }; self.version_suffix_of(interface_id) } From 3ba5de1a9eb87852bdbfa09dddbcecc573707f49 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Thu, 3 Sep 2026 21:39:31 -0700 Subject: [PATCH 13/16] fix --- crates/wit-component/src/encoding.rs | 2 +- fuzz/src/roundtrip_wit.rs | 17 +++++++++++++---- src/bin/wasm-tools/component.rs | 7 ++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 4793c46f42..8d64a12935 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -3723,7 +3723,7 @@ world test { let mut module = dummy_module(&resolve, world, ManglingAndAbi::Standard32); - embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8).unwrap(); + embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8, true).unwrap(); let encoded = ComponentEncoder::default() .import_name_map(HashMap::from([ diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index 045937deec..111a0caf1c 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -25,7 +25,9 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { }; resolve2.assert_valid(); - let wasm2 = wit_component::encode(&resolve2, pkg2).expect("failed to encode WIT document"); + let canonical_names = u.arbitrary()?; + let wasm2 = wit_component::encode(&resolve2, pkg2, canonical_names) + .expect("failed to encode WIT document"); write_file("doc2.wasm", &wasm2); roundtrip_through_printing("doc2", &resolve2, pkg2, &wasm2); @@ -62,8 +64,15 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { dummy = dst.finish(); } } - wit_component::embed_component_metadata(&mut dummy, &resolve, id, StringEncoding::UTF8) - .unwrap(); + let canonical_names = u.arbitrary()?; + wit_component::embed_component_metadata( + &mut dummy, + &resolve, + id, + StringEncoding::UTF8, + canonical_names, + ) + .unwrap(); write_file("dummy.wasm", &dummy); log::debug!("... componentizing the world into a binary component"); @@ -173,7 +182,7 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, pkg: PackageId, was // Finally encode the `new_resolve` which should be the exact same as // before. - let wasm2 = wit_component::encode(&new_resolve, new_pkg).unwrap(); + let wasm2 = wit_component::encode(&new_resolve, new_pkg, true).unwrap(); write_file(&format!("{file}-reencoded.wasm"), &wasm2); if wasm != wasm2 { panic!("failed to roundtrip through text printing"); diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index ace84934ec..adbdd7156d 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -1351,7 +1351,12 @@ impl TargetsOpts { let world = resolve.select_world(&[pkg_id], self.world.as_deref())?; let component_to_test = self.input.get_binary_wasm(None)?; - wit_component::targets(&resolve, world, &component_to_test, self.emit_canonical_names)?; + wit_component::targets( + &resolve, + world, + &component_to_test, + self.emit_canonical_names + )?; Ok(()) } From 0b6fa64c69aca23944cf299f0c9b1468eca1e8f4 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Thu, 3 Sep 2026 21:43:39 -0700 Subject: [PATCH 14/16] fix --- crates/wit-dylib/test-programs/artifacts/src/lib.rs | 1 + src/bin/wasm-tools/component.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/wit-dylib/test-programs/artifacts/src/lib.rs b/crates/wit-dylib/test-programs/artifacts/src/lib.rs index 81008bffe7..1e70208f2b 100644 --- a/crates/wit-dylib/test-programs/artifacts/src/lib.rs +++ b/crates/wit-dylib/test-programs/artifacts/src/lib.rs @@ -50,6 +50,7 @@ fn create_component( resolve, wasm.1, wit_component::StringEncoding::UTF8, + true, )?; let adapter_file = tempdir.path().join(format!("{name}_adapter.wasm")); diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index adbdd7156d..f9bf4eab78 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -1355,7 +1355,7 @@ impl TargetsOpts { &resolve, world, &component_to_test, - self.emit_canonical_names + self.emit_canonical_names, )?; Ok(()) From 637d04a7d6793a7ac10eb81f74d007b79d9b2961 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Thu, 3 Sep 2026 21:50:38 -0700 Subject: [PATCH 15/16] fix --- fuzz/src/roundtrip_wit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index 111a0caf1c..5f05da25a2 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -182,7 +182,7 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, pkg: PackageId, was // Finally encode the `new_resolve` which should be the exact same as // before. - let wasm2 = wit_component::encode(&new_resolve, new_pkg, true).unwrap(); + let wasm2 = wit_component::encode(&new_resolve, new_pkg, false).unwrap(); write_file(&format!("{file}-reencoded.wasm"), &wasm2); if wasm != wasm2 { panic!("failed to roundtrip through text printing"); From b5b707a929149a2deb2ae3685dcbc30730bb1a03 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Thu, 3 Sep 2026 22:18:01 -0700 Subject: [PATCH 16/16] fix --- fuzz/src/roundtrip_wit.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index 5f05da25a2..855cc54bed 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -6,7 +6,9 @@ use wit_component::*; use wit_parser::{LiftLowerAbi, ManglingAndAbi, PackageId, Resolve}; pub fn run(u: &mut Unstructured<'_>) -> Result<()> { - let wasm = u.arbitrary().and_then(|config| { + let canonical_names = u.arbitrary()?; + let wasm = u.arbitrary().and_then(|mut config: wit_smith::Config| { + config.canonical_names = canonical_names; log::debug!("config: {config:#?}"); wit_smith::smith(&config, u) })?; @@ -17,7 +19,7 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { }; resolve.assert_valid(); - roundtrip_through_printing("doc1", &resolve, pkg, &wasm); + roundtrip_through_printing("doc1", &resolve, pkg, &wasm, canonical_names); let (resolve2, pkg2) = match wit_component::decode(&wasm).unwrap() { DecodedWasm::WitPackage(resolve, pkgs) => (resolve, pkgs), @@ -25,11 +27,10 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { }; resolve2.assert_valid(); - let canonical_names = u.arbitrary()?; let wasm2 = wit_component::encode(&resolve2, pkg2, canonical_names) .expect("failed to encode WIT document"); write_file("doc2.wasm", &wasm2); - roundtrip_through_printing("doc2", &resolve2, pkg2, &wasm2); + roundtrip_through_printing("doc2", &resolve2, pkg2, &wasm2, canonical_names); if wasm != wasm2 { panic!("roundtrip wasm didn't match"); @@ -64,7 +65,6 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { dummy = dst.finish(); } } - let canonical_names = u.arbitrary()?; wit_component::embed_component_metadata( &mut dummy, &resolve, @@ -162,7 +162,13 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { Ok(()) } -fn roundtrip_through_printing(file: &str, resolve: &Resolve, pkg: PackageId, wasm: &[u8]) { +fn roundtrip_through_printing( + file: &str, + resolve: &Resolve, + pkg: PackageId, + wasm: &[u8], + canonical_names: bool, +) { // Print to a single string, using nested `package ... { .. }` statements, // and then parse that in a new `Resolve`. let mut new_resolve = Resolve::default(); @@ -182,7 +188,7 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, pkg: PackageId, was // Finally encode the `new_resolve` which should be the exact same as // before. - let wasm2 = wit_component::encode(&new_resolve, new_pkg, false).unwrap(); + let wasm2 = wit_component::encode(&new_resolve, new_pkg, canonical_names).unwrap(); write_file(&format!("{file}-reencoded.wasm"), &wasm2); if wasm != wasm2 { panic!("failed to roundtrip through text printing");