diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index c7d302c4c7..744fef7759 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1039,6 +1039,28 @@ package {name} is defined in two different locations:\n\ Some(self.canonicalized_id_of_name(interface.package.unwrap(), interface.name.as_ref()?)) } + /// Helper to rename a world and update the package's world map. + /// + /// Used by both [`Resolve::importize`] and [`Resolve::exportize`] to + /// rename the world to avoid confusion with the original world name. + fn rename_world( + &mut self, + world_id: WorldId, + out_world_name: Option, + default_suffix: &str, + ) { + let world = &mut self.worlds[world_id]; + let pkg = &mut self.packages[world.package.unwrap()]; + pkg.worlds.shift_remove(&world.name); + if let Some(name) = out_world_name { + world.name = name.clone(); + pkg.worlds.insert(name, world_id); + } else { + world.name.push_str(default_suffix); + pkg.worlds.insert(world.name.clone(), world_id); + } + } + /// Convert a world to an "importized" version where the world is updated /// in-place to reflect what it would look like to be imported. /// @@ -1055,23 +1077,11 @@ package {name} is defined in two different locations:\n\ /// is intended to be used as part of language tooling when depending on /// other components. pub fn importize(&mut self, world_id: WorldId, out_world_name: Option) -> Result<()> { - // Rename the world to avoid having it get confused with the original - // name of the world. Add `-importized` to it for now. Precisely how - // this new world is created may want to be updated over time if this - // becomes problematic. - let world = &mut self.worlds[world_id]; - let pkg = &mut self.packages[world.package.unwrap()]; - pkg.worlds.shift_remove(&world.name); - if let Some(name) = out_world_name { - world.name = name.clone(); - pkg.worlds.insert(name, world_id); - } else { - world.name.push_str("-importized"); - pkg.worlds.insert(world.name.clone(), world_id); - } + self.rename_world(world_id, out_world_name, "-importized"); // Trim all non-type definitions from imports. Types can be used by // exported functions, for example, so they're preserved. + let world = &mut self.worlds[world_id]; world.imports.retain(|_, item| match item { WorldItem::Type { .. } => true, _ => false, @@ -1102,6 +1112,57 @@ package {name} is defined in two different locations:\n\ Ok(()) } + /// Convert a world to an "exportized" version where the world is updated + /// in-place to reflect what it would look like to be exported. + /// + /// This is the inverse of [`Resolve::importize`]. The general idea is that + /// this function will update the `world_id` specified such that it exports + /// the functionality that it previously imported. The world will be left + /// with no imports (except for transitive interface dependencies which may + /// be needed by exported interfaces). + /// + /// An optional `filter` can be provided to control which imports are moved. + /// When `Some`, only imports for which the filter returns `true` are moved + /// to exports; remaining imports are left as-is. When `None`, all imports + /// are moved. + /// + /// This world is then suitable for merging into other worlds or generating + /// bindings in a context that is exporting the original world. This is + /// intended to be used as part of language tooling when implementing + /// components. + pub fn exportize( + &mut self, + world_id: WorldId, + out_world_name: Option, + filter: Option<&dyn Fn(&WorldKey, &WorldItem) -> bool>, + ) -> Result<()> { + self.rename_world(world_id, out_world_name, "-exportized"); + + let world = &mut self.worlds[world_id]; + world.exports.clear(); + + let old_imports = mem::take(&mut world.imports); + for (name, import) in old_imports { + let should_move = match &filter { + Some(f) => f(&name, &import), + None => true, + }; + if should_move { + world.exports.insert(name, import); + } else { + world.imports.insert(name, import); + } + } + + // Fill out any missing transitive interface imports by elaborating this + // world which does that for us. + self.elaborate_world(world_id)?; + + #[cfg(debug_assertions)] + self.assert_valid(); + Ok(()) + } + /// Returns the ID of the specified `name` within the `pkg`. pub fn id_of_name(&self, pkg: PackageId, name: &str) -> String { let package = &self.packages[pkg]; @@ -4440,7 +4501,8 @@ impl core::error::Error for InvalidTransitiveDependency {} #[cfg(test)] mod tests { use crate::alloc::format; - use crate::alloc::string::ToString; + use crate::alloc::string::{String, ToString}; + use crate::alloc::vec::Vec; use crate::{Resolve, WorldItem, WorldKey}; use anyhow::Result; @@ -5543,4 +5605,84 @@ interface iface { Ok(()) } + + /// Demonstrates the round-trip property: starting from a world with only + /// exports, `importize` turns them into imports, then `exportize` turns + /// them back. The resulting world has the same set of exports (by key) + /// as the original. + #[test] + fn exportize_importize_roundtrip() -> Result<()> { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str( + "test.wit", + r#" + package foo:bar; + + interface types { + type my-type = u32; + } + + interface api { + use types.{my-type}; + do-something: func(a: my-type) -> my-type; + } + + world w { + export api; + } + "#, + )?; + let world_id = resolve.packages[pkg].worlds["w"]; + + // Snapshot original export keys. + let original_export_keys: Vec = resolve.worlds[world_id] + .exports + .keys() + .map(|k| resolve.name_world_key(k)) + .collect(); + assert!(!original_export_keys.is_empty()); + assert!(resolve.worlds[world_id].imports.iter().all(|(_, item)| { + // Before importize the only imports should be elaborated + // interface deps (all interface items). + matches!(item, WorldItem::Interface { .. }) + })); + + // importize: exports -> imports, no exports remain. + resolve.importize(world_id, Some("w-temp".to_string()))?; + assert!( + resolve.worlds[world_id].exports.is_empty(), + "importize should leave no exports" + ); + // The original exports should now appear as imports. + for key in &original_export_keys { + assert!( + resolve.worlds[world_id] + .imports + .keys() + .any(|k| resolve.name_world_key(k) == *key), + "expected `{key}` to be an import after importize" + ); + } + + // exportize: imports -> exports, round-tripping back. + resolve.exportize(world_id, Some("w-final".to_string()), None)?; + assert!( + !resolve.worlds[world_id].exports.is_empty(), + "exportize should produce exports" + ); + // The original export keys should be present as exports again. + let final_export_keys: Vec = resolve.worlds[world_id] + .exports + .keys() + .map(|k| resolve.name_world_key(k)) + .collect(); + for key in &original_export_keys { + assert!( + final_export_keys.contains(key), + "expected `{key}` to be an export after round-trip, got exports: {final_export_keys:?}" + ); + } + + Ok(()) + } } diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index a55b62647c..a3eca6b109 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -17,7 +17,7 @@ use wit_component::{ ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter, embed_component_metadata, metadata, }; -use wit_parser::{LiftLowerAbi, Mangling, ManglingAndAbi}; +use wit_parser::{LiftLowerAbi, Mangling, ManglingAndAbi, WorldItem, WorldKey}; /// WebAssembly wit-based component tooling. #[derive(Parser)] @@ -761,12 +761,14 @@ pub struct WitOpts { #[clap( long, conflicts_with = "importize_world", + conflicts_with = "exportize", + conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", conflicts_with = "generate_nominal_type_ids" )] importize: bool, - /// The name of the world to generate when using `--importize` or `importize-world`. + /// The name of the world to generate when using `--importize` or `--importize-world`. #[clap(long = "importize-out-world-name")] importize_out_world_name: Option, @@ -783,12 +785,63 @@ pub struct WitOpts { #[clap( long, conflicts_with = "importize", + conflicts_with = "exportize", + conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", conflicts_with = "generate_nominal_type_ids", value_name = "WORLD" )] importize_world: Option, + /// Generates WIT to export the component specified to this command. + /// + /// This flag requires that the input is a binary component, not a + /// wasm-encoded WIT package. This will then generate a WIT world and output + /// that. The returned world will have exports corresponding to the imports + /// of the component which is input. + /// + /// This is similar to `--exportize-world`, but is used with components. + #[clap( + long, + conflicts_with = "importize", + conflicts_with = "importize_world", + conflicts_with = "exportize_world", + conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "generate_nominal_type_ids" + )] + exportize: bool, + + /// The name of the world to generate when using `--exportize` or `--exportize-world`. + #[clap(long = "exportize-out-world-name")] + exportize_out_world_name: Option, + + /// When used with `--exportize` or `--exportize-world`, only move the + /// specified imports to exports. Can be specified multiple times. If not + /// provided, all imports are moved. + #[clap(long = "exportize-import", value_name = "IMPORT")] + exportize_imports: Vec, + + /// Generates a WIT world to export a component which corresponds to the + /// selected world. + /// + /// This flag is used to indicate that the input is a WIT package and the + /// world passed here is the name of a WIT `world` within the package. The + /// output of the command will be the same WIT world but one that's + /// exporting the selected world. This effectively moves the world's imports + /// to exports. + /// + /// This is similar to `--exportize`, but is used with WIT packages. + #[clap( + long, + conflicts_with = "importize", + conflicts_with = "importize_world", + conflicts_with = "exportize", + conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "generate_nominal_type_ids", + value_name = "WORLD" + )] + exportize_world: Option, + /// Updates the world specified to deduplicate all of its imports based on /// semver versions. /// @@ -800,6 +853,8 @@ pub struct WitOpts { long, conflicts_with = "importize", conflicts_with = "importize_world", + conflicts_with = "exportize", + conflicts_with = "exportize_world", conflicts_with = "generate_nominal_type_ids", value_name = "WORLD" )] @@ -818,6 +873,8 @@ pub struct WitOpts { long, conflicts_with = "importize", conflicts_with = "importize_world", + conflicts_with = "exportize", + conflicts_with = "exportize_world", conflicts_with = "merge_world_imports_based_on_semver", value_name = "WORLD" )] @@ -855,6 +912,20 @@ impl WitOpts { self.importize_world.as_deref(), self.importize_out_world_name.as_ref(), )?; + } else if self.exportize { + self.exportize( + &mut decoded, + None, + self.exportize_out_world_name.as_ref(), + &self.exportize_imports, + )?; + } else if self.exportize_world.is_some() { + self.exportize( + &mut decoded, + self.exportize_world.as_deref(), + self.exportize_out_world_name.as_ref(), + &self.exportize_imports, + )?; } else if let Some(world) = &self.merge_world_imports_based_on_semver { let (resolve, world_id) = match &mut decoded { DecodedWasm::Component(..) => { @@ -986,6 +1057,62 @@ impl WitOpts { Ok(()) } + fn exportize( + &self, + decoded: &mut DecodedWasm, + world: Option<&str>, + out_world_name: Option<&String>, + import_names: &[String], + ) -> Result<()> { + let (resolve, world_id) = match (&mut *decoded, world) { + (DecodedWasm::Component(resolve, world), None) => (resolve, *world), + (DecodedWasm::Component(..), Some(_)) => { + bail!( + "the `--exportize-world` flag is not compatible with a \ + component input, use `--exportize` instead" + ); + } + (DecodedWasm::WitPackage(resolve, id), world) => { + let world = resolve.select_world(&[*id], world)?; + (resolve, world) + } + }; + // Build a set of matching names (both plain names and interface names) + // before constructing the filter, to avoid borrowing `resolve` in the + // closure. + let filter: Option bool>> = if import_names.is_empty() + { + None + } else { + let names: Vec = import_names.to_vec(); + let world = &resolve.worlds[world_id]; + let matching_keys: Vec = world + .imports + .keys() + .filter(|key| match key { + WorldKey::Name(n) => names.iter().any(|f| f == n), + WorldKey::Interface(id) => { + let iface = &resolve.interfaces[*id]; + match &iface.name { + Some(n) => names.iter().any(|f| f == n), + None => false, + } + } + }) + .cloned() + .collect(); + Some(Box::new(move |key: &WorldKey, _item: &WorldItem| { + matching_keys.contains(key) + })) + }; + resolve + .exportize(world_id, out_world_name.cloned(), filter.as_deref()) + .context("failed to move world imports to exports")?; + let resolve = mem::take(resolve); + *decoded = DecodedWasm::Component(resolve, world_id); + Ok(()) + } + fn generate_nominal_type_ids(&self, decoded: &mut DecodedWasm, world: &str) -> Result<()> { let (resolve, pkg) = match decoded { DecodedWasm::Component(resolve, world_id) => { diff --git a/tests/cli/exportize.wit b/tests/cli/exportize.wit new file mode 100644 index 0000000000..810daeae8e --- /dev/null +++ b/tests/cli/exportize.wit @@ -0,0 +1,45 @@ +// RUN[simple]: component wit --exportize-world simple % +// RUN[simple-rename]: component wit --exportize-world simple-rename --exportize-out-world-name test-rename % +// RUN[with-deps]: component wit --exportize-world with-deps % +// RUN[simple-toplevel]: component wit --exportize-world simple-toplevel % +// RUN[selective]: component wit --exportize-world selective --exportize-import a % + +package exportize:exportize; + +interface i { + f: func(); +} + +interface a { + x: func(); +} + +interface b { + y: func(); +} + +world simple { + import i; + export e: func(); +} + +world simple-rename { + import f: func(); +} + +world with-deps { + import a; + import b; +} + +world simple-toplevel { + import foo: func(); + import bar: interface { + baz: func(); + } +} + +world selective { + import a; + import b; +} diff --git a/tests/cli/exportize.wit.selective.stdout b/tests/cli/exportize.wit.selective.stdout new file mode 100644 index 0000000000..7011a2febd --- /dev/null +++ b/tests/cli/exportize.wit.selective.stdout @@ -0,0 +1,42 @@ +/// RUN[simple]: component wit --exportize-world simple % +/// RUN[simple-rename]: component wit --exportize-world simple-rename --exportize-out-world-name test-rename % +/// RUN[with-deps]: component wit --exportize-world with-deps % +/// RUN[simple-toplevel]: component wit --exportize-world simple-toplevel % +/// RUN[selective]: component wit --exportize-world selective --exportize-import a % +package exportize:exportize; + +interface i { + f: func(); +} + +interface a { + x: func(); +} + +interface b { + y: func(); +} + +world simple { + import i; + + export e: func(); +} +world simple-rename { + import f: func(); +} +world with-deps { + import a; + import b; +} +world simple-toplevel { + import bar: interface { + baz: func(); + } + import foo: func(); +} +world selective-exportized { + import b; + + export a; +} diff --git a/tests/cli/exportize.wit.simple-rename.stdout b/tests/cli/exportize.wit.simple-rename.stdout new file mode 100644 index 0000000000..fca483ffdd --- /dev/null +++ b/tests/cli/exportize.wit.simple-rename.stdout @@ -0,0 +1,41 @@ +/// RUN[simple]: component wit --exportize-world simple % +/// RUN[simple-rename]: component wit --exportize-world simple-rename --exportize-out-world-name test-rename % +/// RUN[with-deps]: component wit --exportize-world with-deps % +/// RUN[simple-toplevel]: component wit --exportize-world simple-toplevel % +/// RUN[selective]: component wit --exportize-world selective --exportize-import a % +package exportize:exportize; + +interface i { + f: func(); +} + +interface a { + x: func(); +} + +interface b { + y: func(); +} + +world simple { + import i; + + export e: func(); +} +world with-deps { + import a; + import b; +} +world simple-toplevel { + import bar: interface { + baz: func(); + } + import foo: func(); +} +world selective { + import a; + import b; +} +world test-rename { + export f: func(); +} diff --git a/tests/cli/exportize.wit.simple-toplevel.stdout b/tests/cli/exportize.wit.simple-toplevel.stdout new file mode 100644 index 0000000000..61da9cc141 --- /dev/null +++ b/tests/cli/exportize.wit.simple-toplevel.stdout @@ -0,0 +1,41 @@ +/// RUN[simple]: component wit --exportize-world simple % +/// RUN[simple-rename]: component wit --exportize-world simple-rename --exportize-out-world-name test-rename % +/// RUN[with-deps]: component wit --exportize-world with-deps % +/// RUN[simple-toplevel]: component wit --exportize-world simple-toplevel % +/// RUN[selective]: component wit --exportize-world selective --exportize-import a % +package exportize:exportize; + +interface i { + f: func(); +} + +interface a { + x: func(); +} + +interface b { + y: func(); +} + +world simple { + import i; + + export e: func(); +} +world simple-rename { + import f: func(); +} +world with-deps { + import a; + import b; +} +world selective { + import a; + import b; +} +world simple-toplevel-exportized { + export foo: func(); + export bar: interface { + baz: func(); + } +} diff --git a/tests/cli/exportize.wit.simple.stdout b/tests/cli/exportize.wit.simple.stdout new file mode 100644 index 0000000000..c9b81ec836 --- /dev/null +++ b/tests/cli/exportize.wit.simple.stdout @@ -0,0 +1,39 @@ +/// RUN[simple]: component wit --exportize-world simple % +/// RUN[simple-rename]: component wit --exportize-world simple-rename --exportize-out-world-name test-rename % +/// RUN[with-deps]: component wit --exportize-world with-deps % +/// RUN[simple-toplevel]: component wit --exportize-world simple-toplevel % +/// RUN[selective]: component wit --exportize-world selective --exportize-import a % +package exportize:exportize; + +interface i { + f: func(); +} + +interface a { + x: func(); +} + +interface b { + y: func(); +} + +world simple-rename { + import f: func(); +} +world with-deps { + import a; + import b; +} +world simple-toplevel { + import bar: interface { + baz: func(); + } + import foo: func(); +} +world selective { + import a; + import b; +} +world simple-exportized { + export i; +} diff --git a/tests/cli/exportize.wit.with-deps.stdout b/tests/cli/exportize.wit.with-deps.stdout new file mode 100644 index 0000000000..f9e0cf944d --- /dev/null +++ b/tests/cli/exportize.wit.with-deps.stdout @@ -0,0 +1,41 @@ +/// RUN[simple]: component wit --exportize-world simple % +/// RUN[simple-rename]: component wit --exportize-world simple-rename --exportize-out-world-name test-rename % +/// RUN[with-deps]: component wit --exportize-world with-deps % +/// RUN[simple-toplevel]: component wit --exportize-world simple-toplevel % +/// RUN[selective]: component wit --exportize-world selective --exportize-import a % +package exportize:exportize; + +interface i { + f: func(); +} + +interface a { + x: func(); +} + +interface b { + y: func(); +} + +world simple { + import i; + + export e: func(); +} +world simple-rename { + import f: func(); +} +world simple-toplevel { + import bar: interface { + baz: func(); + } + import foo: func(); +} +world selective { + import a; + import b; +} +world with-deps-exportized { + export a; + export b; +} diff --git a/tests/cli/help-component-wit-short.wat.stdout b/tests/cli/help-component-wit-short.wat.stdout index bc175bdafb..2ec5f926a4 100644 --- a/tests/cli/help-component-wit-short.wat.stdout +++ b/tests/cli/help-component-wit-short.wat.stdout @@ -33,10 +33,22 @@ Options: Generates WIT to import the component specified to this command --importize-out-world-name The name of the world to generate when using `--importize` or - `importize-world` + `--importize-world` --importize-world Generates a WIT world to import a component which corresponds to the selected world + --exportize + Generates WIT to export the component specified to this command + --exportize-out-world-name + The name of the world to generate when using `--exportize` or + `--exportize-world` + --exportize-import + When used with `--exportize` or `--exportize-world`, only move the + specified imports to exports. Can be specified multiple times. If not + provided, all imports are moved + --exportize-world + Generates a WIT world to export a component which corresponds to the + selected world --merge-world-imports-based-on-semver Updates the world specified to deduplicate all of its imports based on semver versions diff --git a/tests/cli/help-component-wit.wat.stdout b/tests/cli/help-component-wit.wat.stdout index 214c6da183..10c3e99037 100644 --- a/tests/cli/help-component-wit.wat.stdout +++ b/tests/cli/help-component-wit.wat.stdout @@ -78,7 +78,7 @@ Options: --importize-out-world-name The name of the world to generate when using `--importize` or - `importize-world` + `--importize-world` --importize-world Generates a WIT world to import a component which corresponds to the @@ -92,6 +92,37 @@ Options: This is similar to `--importize`, but is used with WIT packages. + --exportize + Generates WIT to export the component specified to this command. + + This flag requires that the input is a binary component, not a + wasm-encoded WIT package. This will then generate a WIT world and + output that. The returned world will have exports corresponding to the + imports of the component which is input. + + This is similar to `--exportize-world`, but is used with components. + + --exportize-out-world-name + The name of the world to generate when using `--exportize` or + `--exportize-world` + + --exportize-import + When used with `--exportize` or `--exportize-world`, only move the + specified imports to exports. Can be specified multiple times. If not + provided, all imports are moved + + --exportize-world + Generates a WIT world to export a component which corresponds to the + selected world. + + This flag is used to indicate that the input is a WIT package and the + world passed here is the name of a WIT `world` within the package. The + output of the command will be the same WIT world but one that's + exporting the selected world. This effectively moves the world's + imports to exports. + + This is similar to `--exportize`, but is used with WIT packages. + --merge-world-imports-based-on-semver Updates the world specified to deduplicate all of its imports based on semver versions.