diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs index 3a984b6859..af78b96835 100644 --- a/crates/wasmparser/src/validator/component.rs +++ b/crates/wasmparser/src/validator/component.rs @@ -4663,6 +4663,18 @@ impl ComponentNameContext { } } + 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, @@ -4682,22 +4694,21 @@ 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(suffix) = version_suffix { + if let ComponentNameKind::Interface(iface) = implements.kind() { + if let Err(e) = iface.version(Some(suffix)) { + bail!(offset, "invalid interface version: {e}"); + } + } + } + } _ => bail!(offset, "name `{implements}` must be an interface"), } - } - - 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`"), - } - } + Some(implements) + } else { + None + }; if let Some(_) = external_id { require_feature::cm_implements( diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index b5c08a048b..47c2f6174b 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -72,6 +72,7 @@ //! component model. use crate::StringEncoding; +use crate::encoding::wit::component_extern_name; use crate::metadata::{self, Bindgen, ModuleMetadata}; use crate::validation::{ Export, ExportMap, Import, ImportInstance, ImportMap, PayloadInfo, PayloadType, @@ -609,15 +610,39 @@ impl<'a> EncodingState<'a> { let instance_type_idx = self .component .type_instance(Some(&format!("ty-{name}")), &ty); - let instance_idx = self.component.import( + + 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 { + interface_id + }; + wasm_encoder::ComponentExternName { + 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()), + } + } else { wasm_encoder::ComponentExternName { name: name.into(), - implements: info.implements.as_deref().map(|s| s.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: None, - }, - ComponentTypeRef::Instance(instance_type_idx), - ); + } + }; + 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(()) @@ -762,7 +787,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_canonicalized_world_key(export_name) + } else { + resolve.name_world_key(export_name) + }; match &world.exports[export_name] { WorldItem::Function(func) => { let ty = self @@ -993,13 +1022,10 @@ impl<'a> EncodingState<'a> { component_index, imports, ); + let extern_name = + component_extern_name(resolve, key, item, self.info.encoder.emit_canonical_names); 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, - }, + extern_name, ComponentExportKind::Instance, instance_index, None, @@ -3290,6 +3316,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 +3384,18 @@ 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 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. /// @@ -3671,7 +3710,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/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index a70f90cc38..0790748c47 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -24,8 +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> { - let mut component = encode_component(resolve, package)?; +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()) } @@ -48,11 +48,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( + resolve: &Resolve, + package: PackageId, + canonical_names: bool, +) -> Result { let mut encoder = Encoder { component: ComponentBuilder::default(), resolve, package, + canonical_names, }; encoder.run()?; @@ -66,7 +71,11 @@ pub fn encode_component(resolve: &Resolve, package: PackageId) -> Result Result { +pub fn encode_world( + 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 +102,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) } -fn component_extern_name( +pub(crate) 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, + let implements = resolve.implements_interface(key, item); + if canonical_names { + ComponentExternName { + 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: resolve.version_suffix_value(key, item).map(|s| s.into()), + } + } else { + ComponentExternName { + name: resolve.name_world_key(key).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, + } } } @@ -138,6 +160,7 @@ struct Encoder<'a> { component: ComponentBuilder, resolve: &'a Resolve, package: PackageId, + canonical_names: bool, } impl Encoder<'_> { @@ -153,7 +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(self.resolve, world)?; + let component_ty = encode_world(self.resolve, world, self.canonical_names)?; let world = &self.resolve.worlds[world]; let mut wrapper = ComponentType::new(); @@ -197,11 +220,17 @@ 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 extern_name = if self.canonical_names { + ComponentExternName::from(self.resolve.canonicalized_id_of(interface).unwrap()) + } else { + 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() { @@ -212,7 +241,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/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index c95cde01fb..d37b89c84a 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` @@ -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-component/src/lib.rs b/crates/wit-component/src/lib.rs index 11f57201e5..464c43ac86 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -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(), @@ -151,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/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-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)); } diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs index b4b12adcf0..5b75942bb2 100644 --- a/crates/wit-component/tests/components.rs +++ b/crates/wit-component/tests/components.rs @@ -110,15 +110,23 @@ 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) .realloc_via_memory_grow(config.realloc_via_memory_grow) + .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() @@ -147,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)?; } @@ -247,6 +267,7 @@ struct Config { use_built_in_libdl: bool, return_call_ref: bool, realloc_via_memory_grow: bool, + emit_canonical_names: bool, } /// Reads the configuration for the test located at `path`. @@ -310,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)?; @@ -333,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 new file mode 100644 index 0000000000..47d80a7fef --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/component.wat @@ -0,0 +1,106 @@ +(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)) + (type (;2;) (func (param i32 i32 i32 i32) (result i32))) + (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 4)) + (export "cabi_realloc" (func 5)) + (export "memory" (memory 0)) + (func (;4;) (type 0) (param i32 i32) + unreachable + ) + (func (;5;) (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") + ) + ) + (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.0-x)) + (export "$imports" (table 0)) + (func $indirect-a:b/c@0.1.0-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.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.0 (;1;) + (export "x" (func $indirect-a:b/c@0.1.0-x)) + (export "y" (func $y)) + ) + (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)) + ) + ) + (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 (;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 (;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 (;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))) + (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 $"#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)) + (@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..d05e5c7da5 --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/component.wit.print @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000000..5db7cf2923 --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/module.wat @@ -0,0 +1,12 @@ +;;! 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)) + + (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 new file mode 100644 index 0000000000..b282197e47 --- /dev/null +++ b/crates/wit-component/tests/components/canonical-names/module.wit @@ -0,0 +1,21 @@ +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 { + 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-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..2ec8c539d9 100644 --- a/crates/wit-component/tests/interfaces/wasi-http.wat +++ b/crates/wit-component/tests/interfaces/wasi-http.wat @@ -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-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/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index d1aece5a81..9d93210b1a 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -274,39 +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() + (version.to_string(), suffix) } } @@ -1572,4 +1579,57 @@ 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::version_compat_track_string(&v), + ("1".to_string(), ".2.3".to_string()) + ); + + let v = Version::parse("101.201.301").unwrap(); + assert_eq!( + 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::version_compat_track_string(&v), + ("0.2.6-rc.1".to_string(), "".to_string()) + ); + + let v = Version::parse("0.10.0+build.1").unwrap(); + assert_eq!( + PackageName::version_compat_track_string(&v), + ("0.10".to_string(), ".0+build.1".to_string()) + ); + + let v = Version::parse("0.0.1-alpha+build.1").unwrap(); + assert_eq!( + 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::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::version_compat_track_string(&v), + ("1.0.0-beta.1".to_string(), "".to_string()) + ); + + let v = Version::parse("0.0.100+build.1").unwrap(); + assert_eq!( + 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 388e2767b7..9adf97344e 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,21 +1550,43 @@ impl Resolve { } } - /// Returns the component model `implements` value for the world import of + /// Returns the component model `implements` interface 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 { + 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(self.id_of(*id).unwrap().into()); + return Some(*id); } } } None } + /// Returns the component model `version-suffix` value for the interface `id`. + /// + /// 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 `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 version_suffix_value(&self, key: &WorldKey, item: &WorldItem) -> Option { + let interface_id = match key { + WorldKey::Interface(id) => *id, + WorldKey::Name(_) => self.implements_interface(key, item)?, + }; + self.version_suffix_of(interface_id) + } + /// Returns the component model `external-id` value for the world import of /// `key` and `item`. /// @@ -2435,7 +2457,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)); } @@ -2515,7 +2537,10 @@ impl Resolve { for (key, item) in mem::take(&mut self.worlds[world_id].imports) { if let WorldItem::Interface { id, .. } = item { if replacements.contains_key(&id) { - continue; + if let WorldKey::Interface(_) = key { + continue; + } + // Keep labeled imports with `implements` version unchanged } } @@ -2583,7 +2608,7 @@ impl Resolve { 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)); + name.version = Some(PackageName::version_compat_track(version).0); Some(((name, iface.name.clone()?), version)) } 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/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index 045937deec..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,9 +27,10 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { }; resolve2.assert_valid(); - let wasm2 = wit_component::encode(&resolve2, pkg2).expect("failed to encode WIT document"); + 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"); @@ -62,8 +65,14 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { dummy = dst.finish(); } } - wit_component::embed_component_metadata(&mut dummy, &resolve, id, StringEncoding::UTF8) - .unwrap(); + 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"); @@ -153,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(); @@ -173,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).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"); diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 5452f9b4a0..f9bf4eab78 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -211,6 +211,14 @@ struct ComponentEncoderOpts { #[arg(long, require_equals = true, value_name = "true|false")] merge_imports_based_on_semver: 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, + /// Reject usage of the "legacy" naming scheme of `wit-component` and /// require the new naming scheme to be used. /// @@ -277,7 +285,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.emit_canonical_names); for (name, wasm) in self.adapters.iter() { encoder.adapter(name, wasm)?; } @@ -393,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. /// @@ -466,6 +483,7 @@ impl EmbedOpts { world, self.encoding.unwrap_or(StringEncoding::UTF8), None, + self.emit_canonical_names, )?; self.io.output_wasm(&encoded, false)?; @@ -508,6 +526,7 @@ impl EmbedOpts { &resolve, world, self.encoding.unwrap_or(StringEncoding::UTF8), + self.emit_canonical_names, )?; self.io.output_wasm(&wasm, self.wat)?; @@ -945,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 { @@ -1005,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)?; } @@ -1184,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)?; } @@ -1307,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 { @@ -1320,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)?; + 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/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/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-new-short.wat.stdout b/tests/cli/help-component-new-short.wat.stdout index 3e29a05d33..9978841fa9 100644 --- a/tests/cli/help-component-new-short.wat.stdout +++ b/tests/cli/help-component-new-short.wat.stdout @@ -31,6 +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] + --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 6dfa02391d..0d4de10376 100644 --- a/tests/cli/help-component-new.wat.stdout +++ b/tests/cli/help-component-new.wat.stdout @@ -102,6 +102,13 @@ Options: [possible values: true, false] + --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. + --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..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') diff --git a/tests/cli/merge-canon-with-implements.wit b/tests/cli/merge-canon-with-implements.wit new file mode 100644 index 0000000000..8e4dcb1f08 --- /dev/null +++ b/tests/cli/merge-canon-with-implements.wit @@ -0,0 +1,33 @@ +// RUN: component wit --merge-world-imports-based-on-semver foo % + +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; + 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 { + f: func(); + } +} + +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 new file mode 100644 index 0000000000..fe6fbde914 --- /dev/null +++ b/tests/cli/merge-canon-with-implements.wit.stdout @@ -0,0 +1,36 @@ +/// 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.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 { + f: func(); + } +} + + +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/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))) +)