Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 157 additions & 15 deletions crates/wit-parser/src/resolve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
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.
///
Expand All @@ -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<String>) -> 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,
Expand Down Expand Up @@ -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<String>,
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];
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<String> = 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<String> = 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(())
}
}
131 changes: 129 additions & 2 deletions src/bin/wasm-tools/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<String>,

Expand All @@ -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<String>,

/// 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<String>,

/// 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<String>,

/// 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<String>,

/// Updates the world specified to deduplicate all of its imports based on
/// semver versions.
///
Expand All @@ -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"
)]
Expand All @@ -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"
)]
Expand Down Expand Up @@ -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(..) => {
Expand Down Expand Up @@ -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<Box<dyn Fn(&WorldKey, &WorldItem) -> bool>> = if import_names.is_empty()
{
None
} else {
let names: Vec<String> = import_names.to_vec();
let world = &resolve.worlds[world_id];
let matching_keys: Vec<WorldKey> = 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) => {
Expand Down
Loading
Loading