From 48baf9049e8b4590cde00c5e55966671dc07c5a5 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Fri, 3 Apr 2026 12:58:02 -0600 Subject: [PATCH 1/2] Add exportize -- the inverse of importize Signed-off-by: Brian Hardock --- crates/wit-parser/src/resolve/mod.rs | 352 ++++++++++++++++++++++++++- 1 file changed, 351 insertions(+), 1 deletion(-) diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index c7d302c4c7..bd15017643 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1102,6 +1102,94 @@ 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 type definitions which may be needed by + /// exported functions). + /// + /// If `interfaces` is `None`, all imports are moved to exports (the + /// original behavior). If `interfaces` is `Some`, only the imports whose + /// key is `WorldKey::Interface(id)` where `id` is in the provided list + /// are moved to exports; all other imports are left as-is. + /// + /// 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, + interfaces: Option<&[InterfaceId]>, + ) -> Result<()> { + // Rename the world to avoid having it get confused with the original + // name of the world. Add `-exportized` 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("-exportized"); + pkg.worlds.insert(world.name.clone(), world_id); + } + + // Trim all non-type definitions from exports. Types can be used by + // imported functions, for example, so they're preserved. + world.exports.retain(|_, item| match item { + WorldItem::Type { .. } => true, + _ => false, + }); + + // Determine which imports to move based on the `interfaces` filter. + let should_move = |key: &WorldKey| -> bool { + match interfaces { + None => true, + Some(ids) => match key { + WorldKey::Interface(id) => ids.contains(id), + _ => false, + }, + } + }; + + let old_imports = mem::take(&mut world.imports); + for (name, import) in old_imports { + if should_move(&name) { + match (name.clone(), world.exports.insert(name, import)) { + // no previous item? this insertion was ok + (_, None) => {} + + // cannot overwrite an export with an import + (WorldKey::Name(name), Some(_)) => { + bail!("world import `{name}` conflicts with export of same name"); + } + + // Imports already don't overlap each other and the only exports + // preserved above were types so this shouldn't be reachable. + (WorldKey::Interface(_), _) => unreachable!(), + } + } else { + // Keep as an import. + 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 +4528,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 +5632,265 @@ interface iface { Ok(()) } + + #[test] + fn exportize_basic() -> Result<()> { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str( + "test.wit", + r#" + package foo:bar; + + interface i { + f: func(); + } + + world w { + import i; + export e: func(); + } + "#, + )?; + let world_id = resolve.packages[pkg].worlds["w"]; + + resolve.exportize(world_id, None, None)?; + + let world = &resolve.worlds[world_id]; + assert_eq!(world.name, "w-exportized"); + // The original import `i` should now be an export. + assert!( + world.exports.keys().any(|k| match k { + WorldKey::Interface(_) => true, + _ => false, + }), + "expected interface import to become an export" + ); + // `e` was an exported function but should now be gone (only types are + // kept from the original exports). + assert!( + !world + .exports + .keys() + .any(|k| matches!(k, WorldKey::Name(n) if n == "e")), + "expected original non-type export `e` to be removed" + ); + + Ok(()) + } + + #[test] + fn exportize_custom_name() -> Result<()> { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str( + "test.wit", + r#" + package foo:bar; + + world w { + import f: func(); + } + "#, + )?; + let world_id = resolve.packages[pkg].worlds["w"]; + + resolve.exportize(world_id, Some("my-world".to_string()), None)?; + + let world = &resolve.worlds[world_id]; + assert_eq!(world.name, "my-world"); + // The import `f` should now be an export. + assert!(world.exports.contains_key(&WorldKey::Name("f".to_string()))); + assert!(world.imports.is_empty()); + + Ok(()) + } + + #[test] + fn exportize_with_interface() -> Result<()> { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str( + "test.wit", + r#" + package foo:bar; + + interface a { + x: func(); + } + + interface b { + y: func(); + } + + world w { + import a; + import b; + } + "#, + )?; + let world_id = resolve.packages[pkg].worlds["w"]; + + resolve.exportize(world_id, None, None)?; + + let world = &resolve.worlds[world_id]; + // Both interfaces should now be exports. + let export_count = world + .exports + .keys() + .filter(|k| matches!(k, WorldKey::Interface(_))) + .count(); + assert_eq!(export_count, 2, "expected both interfaces as exports"); + // No non-elaborated imports should remain. + assert!( + world + .imports + .iter() + .all(|(_, item)| matches!(item, WorldItem::Interface { .. })), + "only elaborated interface imports should remain" + ); + + 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(()) + } + + /// When `interfaces` is `Some`, only the listed interface imports are + /// moved to exports; other imports remain as imports. + #[test] + fn exportize_selective() -> Result<()> { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str( + "test.wit", + r#" + package foo:bar; + + interface a { + x: func(); + } + + interface b { + y: func(); + } + + world w { + import a; + import b; + } + "#, + )?; + let world_id = resolve.packages[pkg].worlds["w"]; + + // Grab the InterfaceId for `a` only. + let a_id = resolve.packages[pkg].interfaces["a"]; + + resolve.exportize(world_id, None, Some(&[a_id]))?; + + let world = &resolve.worlds[world_id]; + + // `a` should now be an export. + assert!( + world.exports.keys().any(|k| match k { + WorldKey::Interface(id) => *id == a_id, + _ => false, + }), + "expected interface `a` to become an export" + ); + + // `b` should still be an import (not moved). + let b_id = resolve.packages[pkg].interfaces["b"]; + assert!( + world.imports.keys().any(|k| match k { + WorldKey::Interface(id) => *id == b_id, + _ => false, + }), + "expected interface `b` to remain an import" + ); + + // `b` should NOT be in exports (apart from elaborated deps). + assert!( + !world.exports.keys().any(|k| match k { + WorldKey::Interface(id) => *id == b_id, + _ => false, + }), + "expected interface `b` not to appear as an export" + ); + + Ok(()) + } } From 04122673e73c007caa7b68d911873b8e2c113487 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Fri, 3 Apr 2026 15:06:27 -0600 Subject: [PATCH 2/2] Address feedback Signed-off-by: Brian Hardock --- crates/wit-parser/src/resolve/mod.rs | 288 +++--------------- src/bin/wasm-tools/component.rs | 131 +++++++- tests/cli/exportize.wit | 45 +++ tests/cli/exportize.wit.selective.stdout | 42 +++ tests/cli/exportize.wit.simple-rename.stdout | 41 +++ .../cli/exportize.wit.simple-toplevel.stdout | 41 +++ tests/cli/exportize.wit.simple.stdout | 39 +++ tests/cli/exportize.wit.with-deps.stdout | 41 +++ tests/cli/help-component-wit-short.wat.stdout | 14 +- tests/cli/help-component-wit.wat.stdout | 33 +- 10 files changed, 463 insertions(+), 252 deletions(-) create mode 100644 tests/cli/exportize.wit create mode 100644 tests/cli/exportize.wit.selective.stdout create mode 100644 tests/cli/exportize.wit.simple-rename.stdout create mode 100644 tests/cli/exportize.wit.simple-toplevel.stdout create mode 100644 tests/cli/exportize.wit.simple.stdout create mode 100644 tests/cli/exportize.wit.with-deps.stdout diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index bd15017643..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, @@ -1108,13 +1118,13 @@ package {name} is defined in two different locations:\n\ /// 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 type definitions which may be needed by - /// exported functions). + /// with no imports (except for transitive interface dependencies which may + /// be needed by exported interfaces). /// - /// If `interfaces` is `None`, all imports are moved to exports (the - /// original behavior). If `interfaces` is `Some`, only the imports whose - /// key is `WorldKey::Interface(id)` where `id` is in the provided list - /// are moved to exports; all other imports are left as-is. + /// 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 @@ -1124,59 +1134,22 @@ package {name} is defined in two different locations:\n\ &mut self, world_id: WorldId, out_world_name: Option, - interfaces: Option<&[InterfaceId]>, + filter: Option<&dyn Fn(&WorldKey, &WorldItem) -> bool>, ) -> Result<()> { - // Rename the world to avoid having it get confused with the original - // name of the world. Add `-exportized` 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("-exportized"); - pkg.worlds.insert(world.name.clone(), world_id); - } + self.rename_world(world_id, out_world_name, "-exportized"); - // Trim all non-type definitions from exports. Types can be used by - // imported functions, for example, so they're preserved. - world.exports.retain(|_, item| match item { - WorldItem::Type { .. } => true, - _ => false, - }); - - // Determine which imports to move based on the `interfaces` filter. - let should_move = |key: &WorldKey| -> bool { - match interfaces { - None => true, - Some(ids) => match key { - WorldKey::Interface(id) => ids.contains(id), - _ => false, - }, - } - }; + let world = &mut self.worlds[world_id]; + world.exports.clear(); let old_imports = mem::take(&mut world.imports); for (name, import) in old_imports { - if should_move(&name) { - match (name.clone(), world.exports.insert(name, import)) { - // no previous item? this insertion was ok - (_, None) => {} - - // cannot overwrite an export with an import - (WorldKey::Name(name), Some(_)) => { - bail!("world import `{name}` conflicts with export of same name"); - } - - // Imports already don't overlap each other and the only exports - // preserved above were types so this shouldn't be reachable. - (WorldKey::Interface(_), _) => unreachable!(), - } + let should_move = match &filter { + Some(f) => f(&name, &import), + None => true, + }; + if should_move { + world.exports.insert(name, import); } else { - // Keep as an import. world.imports.insert(name, import); } } @@ -5633,123 +5606,6 @@ interface iface { Ok(()) } - #[test] - fn exportize_basic() -> Result<()> { - let mut resolve = Resolve::default(); - let pkg = resolve.push_str( - "test.wit", - r#" - package foo:bar; - - interface i { - f: func(); - } - - world w { - import i; - export e: func(); - } - "#, - )?; - let world_id = resolve.packages[pkg].worlds["w"]; - - resolve.exportize(world_id, None, None)?; - - let world = &resolve.worlds[world_id]; - assert_eq!(world.name, "w-exportized"); - // The original import `i` should now be an export. - assert!( - world.exports.keys().any(|k| match k { - WorldKey::Interface(_) => true, - _ => false, - }), - "expected interface import to become an export" - ); - // `e` was an exported function but should now be gone (only types are - // kept from the original exports). - assert!( - !world - .exports - .keys() - .any(|k| matches!(k, WorldKey::Name(n) if n == "e")), - "expected original non-type export `e` to be removed" - ); - - Ok(()) - } - - #[test] - fn exportize_custom_name() -> Result<()> { - let mut resolve = Resolve::default(); - let pkg = resolve.push_str( - "test.wit", - r#" - package foo:bar; - - world w { - import f: func(); - } - "#, - )?; - let world_id = resolve.packages[pkg].worlds["w"]; - - resolve.exportize(world_id, Some("my-world".to_string()), None)?; - - let world = &resolve.worlds[world_id]; - assert_eq!(world.name, "my-world"); - // The import `f` should now be an export. - assert!(world.exports.contains_key(&WorldKey::Name("f".to_string()))); - assert!(world.imports.is_empty()); - - Ok(()) - } - - #[test] - fn exportize_with_interface() -> Result<()> { - let mut resolve = Resolve::default(); - let pkg = resolve.push_str( - "test.wit", - r#" - package foo:bar; - - interface a { - x: func(); - } - - interface b { - y: func(); - } - - world w { - import a; - import b; - } - "#, - )?; - let world_id = resolve.packages[pkg].worlds["w"]; - - resolve.exportize(world_id, None, None)?; - - let world = &resolve.worlds[world_id]; - // Both interfaces should now be exports. - let export_count = world - .exports - .keys() - .filter(|k| matches!(k, WorldKey::Interface(_))) - .count(); - assert_eq!(export_count, 2, "expected both interfaces as exports"); - // No non-elaborated imports should remain. - assert!( - world - .imports - .iter() - .all(|(_, item)| matches!(item, WorldItem::Interface { .. })), - "only elaborated interface imports should remain" - ); - - 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) @@ -5829,68 +5685,4 @@ interface iface { Ok(()) } - - /// When `interfaces` is `Some`, only the listed interface imports are - /// moved to exports; other imports remain as imports. - #[test] - fn exportize_selective() -> Result<()> { - let mut resolve = Resolve::default(); - let pkg = resolve.push_str( - "test.wit", - r#" - package foo:bar; - - interface a { - x: func(); - } - - interface b { - y: func(); - } - - world w { - import a; - import b; - } - "#, - )?; - let world_id = resolve.packages[pkg].worlds["w"]; - - // Grab the InterfaceId for `a` only. - let a_id = resolve.packages[pkg].interfaces["a"]; - - resolve.exportize(world_id, None, Some(&[a_id]))?; - - let world = &resolve.worlds[world_id]; - - // `a` should now be an export. - assert!( - world.exports.keys().any(|k| match k { - WorldKey::Interface(id) => *id == a_id, - _ => false, - }), - "expected interface `a` to become an export" - ); - - // `b` should still be an import (not moved). - let b_id = resolve.packages[pkg].interfaces["b"]; - assert!( - world.imports.keys().any(|k| match k { - WorldKey::Interface(id) => *id == b_id, - _ => false, - }), - "expected interface `b` to remain an import" - ); - - // `b` should NOT be in exports (apart from elaborated deps). - assert!( - !world.exports.keys().any(|k| match k { - WorldKey::Interface(id) => *id == b_id, - _ => false, - }), - "expected interface `b` not to appear as an export" - ); - - 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.