Repository: bytecodealliance/wasm-tools
Pull request #1815 (2024-09-23 15:15:37)

Implement merging semver-compatible interfaces in imports
This commit is the first half and the meat of the implementation of #1774 where, by default, WIT processing tools will now merge world imports when possible based on semver versions. The precise shape of how this is done has gone through a few iterations and this is the end result I've ended up settling on. This should handle all the various cases we're interested in and continue to produce valid worlds within a `Resolve` (with the help of `elaborate_world` added in #1800). CLI tooling has been updated with flags to configure this behavior but the behavior is now enabled by default.

Closes https://github.com/bytecodealliance/wasm-tools/issues/1774

alexcrichton: This is a draft PR at this time because changes are needed to `wit-component` to enable this to actually work. While interfaces are correctly merged at the WIT level they're not yet hooked up to core wasm that imports the "old" version. Once https://github.com/bytecodealliance/wasm-tools/pull/1810 lands that connection will be much easier to implement and I plan on adding a follow-up with test cases here as well.
alexcrichton: Ok and now with https://github.com/bytecodealliance/wasm-tools/pull/1810 landed I was hoping this was ready to go. I realized though I should also test with the original .NET component that @dicej had to ensure this works. That's uncovering some more issues so this isn't actually quite ready yet.

Patch:
diff --git a/crates/wasm-wave/src/parser.rs b/crates/wasm-wave/src/parser.rs
index f35e5e34..b7576983 100644
--- a/crates/wasm-wave/src/parser.rs
+++ b/crates/wasm-wave/src/parser.rs
@@ -51,7 +51,13 @@ impl<'source> Parser<'source> {
         let name = self.parse_label()?;
         self.advance()?;
         self.expect_token(Token::ParenOpen)?;
-        let params = self.parse_tuple()?;
+
+        let params = if self.next_is(Token::ParenClose) {
+            self.advance()?;
+            None
+        } else {
+            Some(self.parse_tuple()?)
+        };
         Ok(UntypedFuncCall::new(self.lex.source(), name, params))
     }
 
diff --git a/crates/wasm-wave/src/untyped.rs b/crates/wasm-wave/src/untyped.rs
index 6baf3c1d..2706a7ac 100644
--- a/crates/wasm-wave/src/untyped.rs
+++ b/crates/wasm-wave/src/untyped.rs
@@ -60,11 +60,15 @@ impl std::fmt::Display for UntypedValue<'_> {
 pub struct UntypedFuncCall<'source> {
     source: Cow<'source, str>,
     name: Node,
-    params: Node,
+    params: Option<Node>,
 }
 
 impl<'source> UntypedFuncCall<'source> {
-    pub(crate) fn new(source: impl Into<Cow<'source, str>>, name: Node, params: Node) -> Self {
+    pub(crate) fn new(
+        source: impl Into<Cow<'source, str>>,
+        name: Node,
+        params: Option<Node>,
+    ) -> Self {
         Self {
             source: source.into(),
             name,
@@ -100,8 +104,10 @@ impl<'source> UntypedFuncCall<'source> {
     }
 
     /// Returns the function parameters node.
-    pub fn params_node(&self) -> &Node {
-        &self.params
+    ///
+    /// Returns `None` if the function call has no parameters.
+    pub fn params_node(&self) -> Option<&Node> {
+        self.params.as_ref()
     }
 
     /// Returns the function name.
@@ -117,14 +123,20 @@ impl<'source> UntypedFuncCall<'source> {
         &self,
         types: impl IntoIterator<Item = &'types V::Type>,
     ) -> Result<Vec<V>, ParserError> {
-        self.params.to_wasm_params(types, self.source())
+        match &self.params {
+            Some(params) => params.to_wasm_params(types, self.source()),
+            None => Ok(vec![]),
+        }
     }
 }
 
 impl std::fmt::Display for UntypedFuncCall<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.write_str(self.name.slice(&self.source))?;
-        fmt_node(f, &self.params, &self.source)
+        match &self.params {
+            Some(params) => fmt_node(f, params, &self.source),
+            None => f.write_str("()"),
+        }
     }
 }
 
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 26871bb9..19dd7605 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -76,8 +76,10 @@ use crate::validation::{Export, ExportMap, Import, ImportInstance, ImportMap, RE
 use crate::StringEncoding;
 use anyhow::{anyhow, bail, Context, Result};
 use indexmap::{IndexMap, IndexSet};
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::hash::Hash;
+use std::mem;
 use wasm_encoder::*;
 use wasmparser::Validator;
 use wit_parser::{
@@ -980,35 +982,25 @@ impl<'a> EncodingState<'a> {
     }
 
     fn encode_shim_instantiation(&mut self) -> Result<Shims<'a>> {
-        let mut signatures = Vec::new();
         let mut ret = Shims::default();
 
-        ret.append_indirect(self.info, CustomModule::Main, &mut signatures)
+        ret.append_indirect(self.info, CustomModule::Main)
             .context("failed to register indirect shims for main module")?;
 
         // For all required adapter modules a shim is created for each required
         // function and additionally a set of shims are created for the
         // interface imported into the shim module itself.
         for (adapter_name, _adapter) in self.info.adapters.iter() {
-            ret.append_indirect(
-                self.info,
-                CustomModule::Adapter(adapter_name),
-                &mut signatures,
-            )
-            .with_context(|| {
-                format!("failed to register indirect shims for adapter {adapter_name}")
-            })?;
+            ret.append_indirect(self.info, CustomModule::Adapter(adapter_name))
+                .with_context(|| {
+                    format!("failed to register indirect shims for adapter {adapter_name}")
+                })?;
         }
 
-        if ret.list.is_empty() {
+        if ret.shims.is_empty() {
             return Ok(ret);
         }
 
-        for shim in ret.list.iter() {
-            let prev = ret.shim_names.insert(shim.kind.clone(), shim.name.clone());
-            assert!(prev.is_none());
-        }
-
         assert!(self.shim_instance_index.is_none());
         assert!(self.fixups_module_index.is_none());
 
@@ -1031,19 +1023,19 @@ impl<'a> EncodingState<'a> {
         let mut func_indexes = Vec::new();
         let mut func_names = NameMap::new();
 
-        for (i, (sig, shim)) in signatures.iter().zip(&ret.list).enumerate() {
+        for (i, shim) in ret.shims.values().enumerate() {
             let i = i as u32;
-            let type_index = *sigs.entry(sig).or_insert_with(|| {
+            let type_index = *sigs.entry(&shim.sig).or_insert_with(|| {
                 let index = types.len();
                 types.ty().function(
-                    sig.params.iter().map(to_val_type),
-                    sig.results.iter().map(to_val_type),
+                    shim.sig.params.iter().map(to_val_type),
+                    shim.sig.results.iter().map(to_val_type),
                 );
                 index
             });
 
             functions.function(type_index);
-            Self::encode_shim_function(type_index, i, &mut code, sig.params.len() as u32);
+            Self::encode_shim_function(type_index, i, &mut code, shim.sig.params.len() as u32);
             exports.export(&shim.name, ExportKind::Func, i);
 
             imports_section.import("", &shim.name, EntityType::Function(type_index));
@@ -1056,8 +1048,8 @@ impl<'a> EncodingState<'a> {
 
         let table_type = TableType {
             element_type: RefType::FUNCREF,
-            minimum: signatures.len() as u64,
-            maximum: Some(signatures.len() as u64),
+            minimum: ret.shims.len() as u64,
+            maximum: Some(ret.shims.len() as u64),
             table64: false,
             shared: false,
         };
@@ -1123,7 +1115,7 @@ impl<'a> EncodingState<'a> {
     }
 
     fn encode_indirect_lowerings(&mut self, shims: &Shims<'_>) -> Result<()> {
-        if shims.list.is_empty() {
+        if shims.shims.is_empty() {
             return Ok(());
         }
 
@@ -1137,7 +1129,7 @@ impl<'a> EncodingState<'a> {
         let mut exports = Vec::new();
         exports.push((INDIRECT_TABLE_NAME, ExportKind::Table, table_index));
 
-        for shim in shims.list.iter() {
+        for shim in shims.shims.values() {
             let core_func_index = match &shim.kind {
                 // Indirect lowerings are a `canon lower`'d function with
                 // options specified from a previously instantiated instance.
@@ -1246,10 +1238,11 @@ impl<'a> EncodingState<'a> {
                 // validation, if one is present.
                 let exports = self.info.exports_for(for_module);
                 let dtor = exports.resource_dtor(*ty).map(|name| {
-                    let name = &shims.shim_names[&ShimKind::ResourceDtor {
+                    let name = &shims.shims[&ShimKind::ResourceDtor {
                         module: for_module,
                         export: name,
-                    }];
+                    }]
+                        .name;
                     let shim = self.shim_instance_index.unwrap();
                     self.core_alias_export(shim, name, ExportKind::Func)
                 });
@@ -1363,10 +1356,11 @@ impl<'a> EncodingState<'a> {
                     .expect("shim should be instantiated");
                 let index = self.core_alias_export(
                     shim_instance,
-                    &shims.shim_names[&ShimKind::Adapter {
+                    &shims.shims[&ShimKind::Adapter {
                         adapter: module,
                         func: field,
-                    }],
+                    }]
+                        .name,
                     ExportKind::Func,
                 );
                 return Ok((ExportKind::Func, index));
@@ -1457,12 +1451,13 @@ impl<'a> EncodingState<'a> {
                 self.core_alias_export(
                     self.shim_instance_index
                         .expect("shim should be instantiated"),
-                    &shims.shim_names[&ShimKind::IndirectLowering {
+                    &shims.shims[&ShimKind::IndirectLowering {
                         interface: interface.clone(),
                         index,
                         realloc: for_module,
                         encoding,
-                    }],
+                    }]
+                        .name,
                     ExportKind::Func,
                 )
             }
@@ -1576,10 +1571,7 @@ impl<'a> EncodingState<'a> {
 #[derive(Default)]
 struct Shims<'a> {
     /// The list of all shims that a module will require.
-    list: Vec<Shim<'a>>,
-
-    /// A map from a shim to the name of the shim in the shim instance.
-    shim_names: IndexMap<ShimKind<'a>, String>,
+    shims: IndexMap<ShimKind<'a>, Shim<'a>>,
 }
 
 struct Shim<'a> {
@@ -1598,6 +1590,9 @@ struct Shim<'a> {
 
     /// Precise information about what this shim is a lowering of.
     kind: ShimKind<'a>,
+
+    /// Wasm type of this shim.
+    sig: WasmSignature,
 }
 
 #[derive(Debug, Clone, Hash, Eq, PartialEq)]
@@ -1661,7 +1656,6 @@ impl<'a> Shims<'a> {
         &mut self,
         world: &'a ComponentWorld<'a>,
         for_module: CustomModule<'a>,
-        sigs: &mut Vec<WasmSignature>,
     ) -> Result<()> {
         let module_imports = world.imports_for(for_module);
         let module_exports = world.exports_for(for_module);
@@ -1683,15 +1677,9 @@ impl<'a> Shims<'a> {
                 // Adapter imports into the main module must got through an
                 // indirection, so that's registered here.
                 Import::AdapterExport(ty) => {
-                    let name = self.list.len().to_string();
+                    let name = self.shims.len().to_string();
                     log::debug!("shim {name} is adapter `{module}::{field}`");
-                    sigs.push(WasmSignature {
-                        params: ty.params().iter().map(to_wasm_type).collect(),
-                        results: ty.results().iter().map(to_wasm_type).collect(),
-                        indirect_params: false,
-                        retptr: false,
-                    });
-                    self.list.push(Shim {
+                    self.push(Shim {
                         name,
                         debug_name: format!("adapt-{module}-{field}"),
                         // Pessimistically assume that all adapters require
@@ -1702,6 +1690,12 @@ impl<'a> Shims<'a> {
                             adapter: module,
                             func: field,
                         },
+                        sig: WasmSignature {
+                            params: ty.params().iter().map(to_wasm_type).collect(),
+                            results: ty.results().iter().map(to_wasm_type).collect(),
+                            indirect_params: false,
+                            retptr: false,
+                        },
                     });
                     continue;
 
@@ -1725,7 +1719,7 @@ impl<'a> Shims<'a> {
             let key = key.map(|key| resolve.name_world_key(key));
             let interface = &world.import_map[&key];
             let (index, _, lowering) = interface.lowerings.get_full(name).unwrap();
-            let shim_name = self.list.len().to_string();
+            let shim_name = self.shims.len().to_string();
             match lowering {
                 Lowering::Direct | Lowering::ResourceDrop(_) => {}
 
@@ -1733,7 +1727,6 @@ impl<'a> Shims<'a> {
                     log::debug!(
                         "shim {shim_name} is import `{module}::{field}` lowering {index} `{name}`",
                     );
-                    sigs.push(sig.clone());
                     let encoding = *metadata
                         .import_encodings
                         .get(&(module.to_string(), field.to_string()))
@@ -1743,7 +1736,7 @@ impl<'a> Shims<'a> {
                                 `{module}::{field}`"
                             )
                         })?;
-                    self.list.push(Shim {
+                    self.push(Shim {
                         name: shim_name,
                         debug_name: format!("indirect-{module}-{field}"),
                         options: *options,
@@ -1753,6 +1746,7 @@ impl<'a> Shims<'a> {
                             realloc: for_module,
                             encoding,
                         },
+                        sig: sig.clone(),
                     });
                 }
             }
@@ -1768,15 +1762,9 @@ impl<'a> Shims<'a> {
                 Export::ResourceDtor(id) => id,
                 _ => continue,
             };
-            sigs.push(WasmSignature {
-                params: vec![WasmType::I32],
-                results: Vec::new(),
-                indirect_params: false,
-                retptr: false,
-            });
             let resource = resolve.types[*id].name.as_ref().unwrap();
-            let name = self.list.len().to_string();
-            self.list.push(Shim {
+            let name = self.shims.len().to_string();
+            self.push(Shim {
                 name,
                 debug_name: format!("dtor-{resource}"),
                 options: RequiredOptions::empty(),
@@ -1784,11 +1772,21 @@ impl<'a> Shims<'a> {
                     module: for_module,
                     export: export_name,
                 },
+                sig: WasmSignature {
+                    params: vec![WasmType::I32],
+                    results: Vec::new(),
+                    indirect_params: false,
+                    retptr: false,
+                },
             });
         }
 
         Ok(())
     }
+
+    fn push(&mut self, shim: Shim<'a>) {
+        self.shims.insert(shim.kind.clone(), shim);
+    }
 }
 
 /// Alias argument to an instantiation
@@ -1866,6 +1864,7 @@ pub struct ComponentEncoder {
     adapters: IndexMap<String, Adapter>,
     import_name_map: HashMap<String, String>,
     realloc_via_memory_grow: bool,
+    merge_imports_based_on_semver: Option<bool>,
 }
 
 impl ComponentEncoder {
@@ -1875,14 +1874,11 @@ impl ComponentEncoder {
     /// It will also add any producers information inside the component type information to the
     /// core module.
     pub fn module(mut self, module: &[u8]) -> Result<Self> {
-        let (wasm, metadata) = metadata::decode(module)?;
-        let wasm = wasm.as_deref().unwrap_or(module);
-        let world = self
-            .metadata
-            .merge(metadata)
+        let (wasm, metadata) = self.decode(module)?;
+        let exports = self
+            .merge_metadata(metadata)
             .context("failed merge WIT metadata for module with previous metadata")?;
-        self.main_module_exports
-            .extend(self.metadata.resolve.worlds[world].exports.keys().cloned());
+        self.main_module_exports.extend(exports);
         self.module = if let Some(producers) = &self.metadata.producers {
             producers.add_to_wasm(&wasm)?
         } else {
@@ -1891,12 +1887,36 @@ impl ComponentEncoder {
         Ok(self)
     }
 
+    fn decode<'a>(&self, wasm: &'a [u8]) -> Result<(Cow<'a, [u8]>, Bindgen)> {
+        let (bytes, metadata) = metadata::decode(wasm)?;
+        match bytes {
+            Some(wasm) => Ok((Cow::Owned(wasm), metadata)),
+            None => Ok((Cow::Borrowed(wasm), metadata)),
+        }
+    }
+
+    fn merge_metadata(&mut self, metadata: Bindgen) -> Result<IndexSet<WorldKey>> {
+        self.metadata.merge(metadata)
+    }
+
     /// Sets whether or not the encoder will validate its output.
     pub fn validate(mut self, validate: bool) -> Self {
         self.validate = validate;
         self
     }
 
+    /// Sets whether to merge imports based on semver to the specified value.
+    ///
+    /// This affects how when to WIT worlds are merged together, for example
+    /// from two different libraries, whether their imports are unified when the
+    /// semver version ranges for interface allow it.
+    ///
+    /// This is enabled by default.
+    pub fn merge_imports_based_on_semver(mut self, merge: bool) -> Self {
+        self.merge_imports_based_on_semver = Some(merge);
+        self
+    }
+
     /// Specifies a new adapter which is used to translate from a historical
     /// wasm ABI to the canonical ABI and the `interface` provided.
     ///
@@ -1940,36 +1960,18 @@ impl ComponentEncoder {
         bytes: &[u8],
         library_info: Option<LibraryInfo>,
     ) -> Result<Self> {
-        let (wasm, metadata) = metadata::decode(bytes)?;
-        let wasm = wasm.as_deref().unwrap_or(bytes);
+        let (wasm, mut metadata) = self.decode(bytes)?;
         // Merge the adapter's document into our own document to have one large
         // document, and then afterwards merge worlds as well.
         //
-        // The first `merge` operation will interleave equivalent packages from
-        // each adapter into packages that are stored within our own resolve.
-        // The second `merge_worlds` operation will then ensure that both the
-        // adapter and the main module have compatible worlds, meaning that they
-        // either import the same items or they import disjoint items, for
-        // example.
-        let world = self
-            .metadata
-            .resolve
-            .merge(metadata.resolve)
-            .with_context(|| {
-                format!("failed to merge WIT packages of adapter `{name}` into main packages")
-            })?
-            .map_world(metadata.world, None)?;
-        self.metadata
-            .resolve
-            .merge_worlds(world, self.metadata.world)
-            .with_context(|| {
-                format!("failed to merge WIT world of adapter `{name}` into main package")
-            })?;
-        let exports = self.metadata.resolve.worlds[world]
-            .exports
-            .keys()
-            .cloned()
-            .collect();
+        // Note that the `metadata` tracking import/export encodings is removed
+        // since this adapter can get different lowerings and is allowed to
+        // differ from the main module. This is then tracked within the
+        // `Adapter` structure produced below.
+        let adapter_metadata = mem::take(&mut metadata.metadata);
+        let exports = self.merge_metadata(metadata).with_context(|| {
+            format!("failed to merge WIT packages of adapter `{name}` into main packages")
+        })?;
         if let Some(library_info) = &library_info {
             // Validate that all referenced modules can be resolved.
             for (_, instance) in &library_info.arguments {
@@ -1998,7 +2000,7 @@ impl ComponentEncoder {
             name.to_string(),
             Adapter {
                 wasm: wasm.to_vec(),
-                metadata: metadata.metadata,
+                metadata: adapter_metadata,
                 required_exports: exports,
                 library_info,
             },
@@ -2031,11 +2033,17 @@ impl ComponentEncoder {
     }
 
     /// Encode the component and return the bytes.
-    pub fn encode(&self) -> Result<Vec<u8>> {
+    pub fn encode(&mut self) -> Result<Vec<u8>> {
         if self.module.is_empty() {
             bail!("a module is required when encoding a component");
         }
 
+        if self.merge_imports_based_on_semver.unwrap_or(true) {
+            self.metadata
+                .resolve
+                .merge_world_imports_based_on_semver(self.metadata.world)?;
+        }
+
         let world = ComponentWorld::new(self).context("failed to decode world from module")?;
         let mut state = EncodingState {
             component: ComponentBuilder::default(),
diff --git a/crates/wit-component/src/linking.rs b/crates/wit-component/src/linking.rs
index 69428228..cde2b524 100644
--- a/crates/wit-component/src/linking.rs
+++ b/crates/wit-component/src/linking.rs
@@ -1222,6 +1222,11 @@ pub struct Linker {
     ///
     /// If `None`, use `DEFAULT_STACK_SIZE_BYTES`.
     stack_size: Option<u32>,
+
+    /// This affects how when to WIT worlds are merged together, for example
+    /// from two different libraries, whether their imports are unified when the
+    /// semver version ranges for interface allow it.
+    merge_imports_based_on_semver: Option<bool>,
 }
 
 impl Linker {
@@ -1269,6 +1274,16 @@ impl Linker {
         self
     }
 
+    /// This affects how when to WIT worlds are merged together, for example
+    /// from two different libraries, whether their imports are unified when the
+    /// semver version ranges for interface allow it.
+    ///
+    /// This is enabled by default.
+    pub fn merge_imports_based_on_semver(mut self, merge: bool) -> Self {
+        self.merge_imports_based_on_semver = Some(merge);
+        self
+    }
+
     /// Encode the component and return the bytes
     pub fn encode(mut self) -> Result<Vec<u8>> {
         if self.use_built_in_libdl {
@@ -1415,9 +1430,11 @@ impl Linker {
             self.stack_size.unwrap_or(DEFAULT_STACK_SIZE_BYTES),
         );
 
-        let mut encoder = ComponentEncoder::default()
-            .validate(self.validate)
-            .module(&env_module)?;
+        let mut encoder = ComponentEncoder::default().validate(self.validate);
+        if let Some(merge) = self.merge_imports_based_on_semver {
+            encoder = encoder.merge_imports_based_on_semver(merge);
+        };
+        encoder = encoder.module(&env_module)?;
 
         for (name, module) in &self.adapters {
             encoder = encoder.adapter(name, module)?;
diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs
index 63307312..18b2608d 100644
--- a/crates/wit-component/src/metadata.rs
+++ b/crates/wit-component/src/metadata.rs
@@ -44,14 +44,14 @@
 use crate::validation::BARE_FUNC_MODULE_NAME;
 use crate::{DecodedWasm, StringEncoding};
 use anyhow::{bail, Context, Result};
-use indexmap::IndexMap;
+use indexmap::{IndexMap, IndexSet};
 use std::borrow::Cow;
 use wasm_encoder::{
     ComponentBuilder, ComponentExportKind, ComponentType, ComponentTypeRef, CustomSection,
 };
 use wasm_metadata::Producers;
 use wasmparser::{BinaryReader, Encoding, Parser, Payload};
-use wit_parser::{Package, PackageName, Resolve, World, WorldId, WorldItem};
+use wit_parser::{Package, PackageName, Resolve, World, WorldId, WorldItem, WorldKey};
 
 const CURRENT_VERSION: u8 = 0x04;
 const CUSTOM_SECTION_NAME: &str = "wit-component-encoding";
@@ -298,7 +298,10 @@ impl Bindgen {
     ///
     /// Note that at this time there's no support for changing string encodings
     /// between metadata.
-    pub fn merge(&mut self, other: Bindgen) -> Result<WorldId> {
+    ///
+    /// This function returns the set of exports that the main world of
+    /// `other` added to the world in `self`.
+    pub fn merge(&mut self, other: Bindgen) -> Result<IndexSet<WorldKey>> {
         let Bindgen {
             resolve,
             world,
@@ -315,6 +318,7 @@ impl Bindgen {
             .merge(resolve)
             .context("failed to merge WIT package sets together")?
             .map_world(world, None)?;
+        let exports = self.resolve.worlds[world].exports.keys().cloned().collect();
         self.resolve
             .merge_worlds(world, self.world)
             .context("failed to merge worlds from two documents")?;
@@ -349,7 +353,7 @@ impl Bindgen {
             }
         }
 
-        Ok(world)
+        Ok(exports)
     }
 }
 
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index 14c53293..3bb7dfdd 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -414,14 +414,80 @@ impl ImportMap {
         resolve: &Resolve,
         items: &IndexMap<WorldKey, WorldItem>,
     ) -> Result<(WorldKey, InterfaceId)> {
-        let key = world_key(resolve, module);
-        match items.get(&key) {
-            Some(WorldItem::Interface { id, .. }) => Ok((key, *id)),
-            Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
-                bail!("import `{module}` is not an interface")
+        // First see if this is a bare name
+        let bare_name = WorldKey::Name(module.to_string());
+        if let Some(WorldItem::Interface { id, .. }) = items.get(&bare_name) {
+            return Ok((bare_name, *id));
+        }
+
+        // ... and if this isn't a bare name then it's time to do some parsing
+        // related to interfaces, versions, and such. First up the `module` name
+        // is parsed as a normal component name from `wasmparser` to see if it's
+        // of the "interface kind". If it's not then that means the above match
+        // should have been a hit but it wasn't, so an error is returned.
+        let kebab_name = ComponentName::new(module, 0);
+        let name = match kebab_name.as_ref().map(|k| k.kind()) {
+            Ok(ComponentNameKind::Interface(name)) => name,
+            _ => bail!("module requires an import interface named `{module}`"),
+        };
+
+        // Prioritize an exact match based on versions, so try that first.
+        let pkgname = PackageName {
+            namespace: name.namespace().to_string(),
+            name: name.package().to_string(),
+            version: name.version(),
+        };
+        if let Some(pkg) = resolve.package_names.get(&pkgname) {
+            if let Some(id) = resolve.packages[*pkg]
+                .interfaces
+                .get(name.interface().as_str())
+            {
+                let key = WorldKey::Interface(*id);
+                if items.contains_key(&key) {
+                    return Ok((key, *id));
+                }
+            }
+        }
+
+        // If an exact match wasn't found then instead search for the first
+        // match based on versions. This means that a core wasm import for
+        // "1.2.3" might end up matching an interface at "1.2.4", for example.
+        // (or "1.2.2", depending on what's available).
+        for (key, _) in items {
+            let id = match key {
+                WorldKey::Interface(id) => *id,
+                WorldKey::Name(_) => continue,
+            };
+            // Make sure the interface names match
+            let interface = &resolve.interfaces[id];
+            if interface.name.as_ref().unwrap() != name.interface().as_str() {
+                continue;
+            }
+
+            // Make sure the package name (without version) matches
+            let pkg = &resolve.packages[interface.package.unwrap()];
+            if pkg.name.namespace != pkgname.namespace || pkg.name.name != pkgname.name {
+                continue;
+            }
+
+            let module_version = match name.version() {
+                Some(version) => version,
+                None => continue,
+            };
+            let pkg_version = match &pkg.name.version {
+                Some(version) => version,
+                None => continue,
+            };
+
+            // 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);
+            if module_compat == pkg_compat {
+                return Ok((key.clone(), id));
             }
-            None => bail!("module requires an import interface named `{module}`"),
         }
+
+        bail!("module requires an import interface named `{module}`")
     }
 
     fn classify_import_with_library(
@@ -907,29 +973,6 @@ pub fn validate_adapter_module(
     Ok(ret)
 }
 
-fn world_key(resolve: &Resolve, name: &str) -> WorldKey {
-    let kebab_name = ComponentName::new(name, 0);
-    let (pkgname, interface) = match kebab_name.as_ref().map(|k| k.kind()) {
-        Ok(ComponentNameKind::Interface(name)) => {
-            let pkgname = PackageName {
-                namespace: name.namespace().to_string(),
-                name: name.package().to_string(),
-                version: name.version(),
-            };
-            (pkgname, name.interface().as_str())
-        }
-        _ => return WorldKey::Name(name.to_string()),
-    };
-    match resolve
-        .package_names
-        .get(&pkgname)
-        .and_then(|p| resolve.packages[*p].interfaces.get(interface))
-    {
-        Some(id) => WorldKey::Interface(*id),
-        None => WorldKey::Name(name.to_string()),
-    }
-}
-
 fn valid_resource_drop(
     func_name: &str,
     ty: &FuncType,
diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs
index f26b87f0..26f355bb 100644
--- a/crates/wit-parser/src/lib.rs
+++ b/crates/wit-parser/src/lib.rs
@@ -189,6 +189,36 @@ impl PackageName {
         }
         s
     }
+
+    /// Determines the "semver compatible track" for the given version.
+    ///
+    /// This method implements the logic from the component model where semver
+    /// versions can be compatible with one another. For example versions 1.2.0
+    /// and 1.2.1 would be considered both compatible with one another because
+    /// they're on the same semver compatible track.
+    ///
+    /// This predicate is used during
+    /// [`Resolve::merge_world_imports_based_on_semver`] for example to
+    /// 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 {
+        let mut version = version.clone();
+        version.build = semver::BuildMetadata::EMPTY;
+        if !version.pre.is_empty() {
+            return version;
+        }
+        if version.major != 0 {
+            version.minor = 0;
+            version.patch = 0;
+            return version;
+        }
+        if version.minor != 0 {
+            version.patch = 0;
+            return version;
+        }
+        version
+    }
 }
 
 impl fmt::Display for PackageName {
diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs
index 40379ae9..bc02c7ce 100644
--- a/crates/wit-parser/src/resolve.rs
+++ b/crates/wit-parser/src/resolve.rs
@@ -1,3 +1,5 @@
+use std::cmp::Ordering;
+use std::collections::hash_map;
 use std::collections::{BTreeMap, HashMap, HashSet};
 use std::mem;
 use std::path::{Path, PathBuf};
@@ -5,6 +7,7 @@ use std::path::{Path, PathBuf};
 use anyhow::{anyhow, bail, ensure, Context, Result};
 use id_arena::{Arena, Id};
 use indexmap::{IndexMap, IndexSet};
+use semver::Version;
 #[cfg(feature = "serde")]
 use serde_derive::Serialize;
 
@@ -691,13 +694,15 @@ package {name} is defined in two different locations:\n\
         // ids within `self`.
         for id in moved_worlds {
             let id = remap.map_world(id, None)?;
-            let pkg = self.worlds[id].package.as_mut().unwrap();
-            *pkg = remap.packages[pkg.index()];
+            if let Some(pkg) = self.worlds[id].package.as_mut() {
+                *pkg = remap.packages[pkg.index()];
+            }
         }
         for id in moved_interfaces {
             let id = remap.map_interface(id, None)?;
-            let pkg = self.interfaces[id].package.as_mut().unwrap();
-            *pkg = remap.packages[pkg.index()];
+            if let Some(pkg) = self.interfaces[id].package.as_mut() {
+                *pkg = remap.packages[pkg.index()];
+            }
         }
         for id in moved_types {
             let id = remap.map_type(id, None)?;
@@ -1421,8 +1426,10 @@ package {name} is defined in two different locations:\n\
         let mut world_types = Vec::new();
         for (id, world) in self.worlds.iter() {
             log::debug!("validating world {}", &world.name);
-            assert!(self.packages.get(world.package.unwrap()).is_some());
-            assert!(package_worlds[world.package.unwrap().index()].contains(&id));
+            if let Some(package) = world.package {
+                assert!(self.packages.get(package).is_some());
+                assert!(package_worlds[package.index()].contains(&id));
+            }
             assert!(world.includes.is_empty());
 
             let mut types = HashSet::new();
@@ -1952,6 +1959,220 @@ package {name} is defined in two different locations:\n\
             true
         }
     }
+
+    /// Remove duplicate imports from a world if they import from the same
+    /// interface with semver-compatible versions.
+    ///
+    /// This will merge duplicate interfaces present at multiple versions in
+    /// both a world by selecting the larger version of the two interfaces. This
+    /// requires that the interfaces are indeed semver-compatible and it means
+    /// that some imports might be removed and replaced. Note that this is only
+    /// done within a single semver track, for example the world imports 0.2.0
+    /// and 0.2.1 then the result afterwards will be that it imports
+    /// 0.2.1. If, however, 0.3.0 where imported then the final result would
+    /// import both 0.2.0 and 0.3.0.
+    pub fn merge_world_imports_based_on_semver(&mut self, world_id: WorldId) -> Result<()> {
+        let world = &self.worlds[world_id];
+
+        // The first pass here is to build a map of "semver tracks" where they
+        // key is per-interface and the value is the maximal version found in
+        // that semver-compatible-track plus the interface which is the maximal
+        // version.
+        //
+        // At the same time a `to_remove` set is maintained to remember what
+        // interfaces are being removed from `from` and `into`. All of
+        // `to_remove` are placed with a known other version.
+        let mut semver_tracks = HashMap::new();
+        let mut to_remove = HashSet::new();
+        for (key, _) in world.imports.iter() {
+            let iface_id = match key {
+                WorldKey::Interface(id) => *id,
+                WorldKey::Name(_) => continue,
+            };
+            let (track, version) = match self.semver_track(iface_id) {
+                Some(track) => track,
+                None => continue,
+            };
+            log::debug!(
+                "{} is on track {}/{}",
+                self.id_of(iface_id).unwrap(),
+                track.0,
+                track.1,
+            );
+            match semver_tracks.entry(track.clone()) {
+                hash_map::Entry::Vacant(e) => {
+                    e.insert((version, iface_id));
+                }
+                hash_map::Entry::Occupied(mut e) => match version.cmp(&e.get().0) {
+                    Ordering::Greater => {
+                        to_remove.insert(e.get().1);
+                        e.insert((version, iface_id));
+                    }
+                    Ordering::Equal => {}
+                    Ordering::Less => {
+                        to_remove.insert(iface_id);
+                    }
+                },
+            }
+        }
+
+        // Build a map of "this interface is replaced with this interface" using
+        // the results of the loop above.
+        let mut replacements = HashMap::new();
+        for id in to_remove {
+            let (track, _) = self.semver_track(id).unwrap();
+            let (_, latest) = semver_tracks[&track];
+            let prev = replacements.insert(id, latest);
+            assert!(prev.is_none());
+        }
+
+        // Validate that `merge_world_item` succeeds for merging all removed
+        // interfaces with their replacement. This is a double-check that the
+        // semver version is actually correct and all items present in the old
+        // interface are in the new.
+        for (to_replace, replace_with) in replacements.iter() {
+            self.merge_world_item(
+                &WorldItem::Interface {
+                    id: *to_replace,
+                    stability: Default::default(),
+                },
+                &WorldItem::Interface {
+                    id: *replace_with,
+                    stability: Default::default(),
+                },
+            )
+            .with_context(|| {
+                let old_name = self.id_of(*to_replace).unwrap();
+                let new_name = self.id_of(*replace_with).unwrap();
+                format!(
+                    "failed to upgrade `{old_name}` to `{new_name}`, was \
+                     this semver-compatible update not semver compatible?"
+                )
+            })?;
+        }
+
+        for (to_replace, replace_with) in replacements.iter() {
+            log::debug!(
+                "REPLACE {} => {}",
+                self.id_of(*to_replace).unwrap(),
+                self.id_of(*replace_with).unwrap(),
+            );
+        }
+
+        // Finally perform the actual transformation of the imports/exports.
+        // Here all imports are removed if they're replaced and otherwise all
+        // imports have their dependencies updated, possibly transitively, to
+        // point to the new interfaces in `replacements`.
+        //
+        // Afterwards exports are additionally updated, but only their
+        // dependencies on imports which were remapped. Exports themselves are
+        // not deduplicated and/or removed.
+        for (key, item) in mem::take(&mut self.worlds[world_id].imports) {
+            if let WorldItem::Interface { id, .. } = item {
+                if replacements.contains_key(&id) {
+                    continue;
+                }
+            }
+
+            self.update_interface_deps_of_world_item(&item, &replacements);
+
+            let prev = self.worlds[world_id].imports.insert(key, item);
+            assert!(prev.is_none());
+        }
+        for (key, item) in mem::take(&mut self.worlds[world_id].exports) {
+            self.update_interface_deps_of_world_item(&item, &replacements);
+            let prev = self.worlds[world_id].exports.insert(key, item);
+            assert!(prev.is_none());
+        }
+
+        // Run through `elaborate_world` to reorder imports as appropriate and
+        // fill anything back in if it's actually required by exports. For now
+        // this doesn't tamper with exports at all. Also note that this is
+        // applied to all worlds in this `Resolve` because interfaces were
+        // modified directly.
+        let ids = self.worlds.iter().map(|(id, _)| id).collect::<Vec<_>>();
+        for world_id in ids {
+            self.elaborate_world(world_id).with_context(|| {
+                let name = &self.worlds[world_id].name;
+                format!(
+                    "failed to elaborate world `{name}` after deduplicating imports \
+                     based on semver"
+                )
+            })?;
+        }
+
+        #[cfg(debug_assertions)]
+        self.assert_valid();
+
+        Ok(())
+    }
+
+    fn update_interface_deps_of_world_item(
+        &mut self,
+        item: &WorldItem,
+        replacements: &HashMap<InterfaceId, InterfaceId>,
+    ) {
+        match *item {
+            WorldItem::Type(t) => self.update_interface_dep_of_type(t, &replacements),
+            WorldItem::Interface { id, .. } => {
+                let types = self.interfaces[id]
+                    .types
+                    .values()
+                    .copied()
+                    .collect::<Vec<_>>();
+                for ty in types {
+                    self.update_interface_dep_of_type(ty, &replacements);
+                }
+            }
+            WorldItem::Function(_) => {}
+        }
+    }
+
+    /// Returns the "semver track" of an interface plus the interface's version.
+    ///
+    /// This function returns `None` if the interface `id` has a package without
+    /// a version. If the version is present, however, the first element of the
+    /// tuple returned is a "semver track" for the specific interface. The
+    /// version listed in `PackageName` will be modified so all
+    /// semver-compatible versions are listed the same way.
+    ///
+    /// The second element in the returned tuple is this interface's package's
+    /// version.
+    fn semver_track(&self, id: InterfaceId) -> Option<((PackageName, String), &Version)> {
+        let iface = &self.interfaces[id];
+        let pkg = &self.packages[iface.package?];
+        let version = pkg.name.version.as_ref()?;
+        let mut name = pkg.name.clone();
+        name.version = Some(PackageName::version_compat_track(version));
+        Some(((name, iface.name.clone()?), version))
+    }
+
+    /// If `ty` is a definition where it's a `use` from another interface, then
+    /// change what interface it's using from according to the pairs in the
+    /// `replacements` map.
+    fn update_interface_dep_of_type(
+        &mut self,
+        ty: TypeId,
+        replacements: &HashMap<InterfaceId, InterfaceId>,
+    ) {
+        let to_replace = match self.type_interface_dep(ty) {
+            Some(id) => id,
+            None => return,
+        };
+        let replace_with = match replacements.get(&to_replace) {
+            Some(id) => id,
+            None => return,
+        };
+        let dep = match self.types[ty].kind {
+            TypeDefKind::Type(Type::Id(id)) => id,
+            _ => return,
+        };
+        let name = self.types[dep].name.as_ref().unwrap();
+        // Note the infallible name indexing happening here. This should be
+        // previously validated with `merge_world_item` to succeed.
+        let replacement_id = self.interfaces[*replace_with].types[name];
+        self.types[ty].kind = TypeDefKind::Type(Type::Id(replacement_id));
+    }
 }
 
 /// Structure returned by [`Resolve::merge`] which contains mappings from
diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs
index f954bfb5..ed696160 100644
--- a/fuzz/src/roundtrip_wit.rs
+++ b/fuzz/src/roundtrip_wit.rs
@@ -71,20 +71,27 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> {
 
     let i = u.choose_index(decoded_bindgens.len())?;
     let (mut b1, wasm1, world1) = decoded_bindgens.swap_remove(i);
-    let i = u.choose_index(decoded_bindgens.len())?;
-    let (b2, wasm2, world2) = decoded_bindgens.swap_remove(i);
-
-    log::debug!("merging bindgens world {world1} <- world {world2}");
 
-    write_file("bindgen1.wasm", &wasm1);
-    write_file("bindgen2.wasm", &wasm2);
-
-    // Merging worlds may fail but if successful then a `Resolve` is asserted
-    // to be valid which is what we're interested in here. Note that failure
-    // here can be due to the structure of worlds which aren't reasonable to
-    // control in this generator, so it's just done to see what happens and try
-    // to trigger panics in `Resolve::assert_valid`.
-    let _ = b1.merge(b2);
+    if u.arbitrary()? {
+        let i = u.choose_index(decoded_bindgens.len())?;
+        let (b2, wasm2, world2) = decoded_bindgens.swap_remove(i);
+
+        log::debug!("merging bindgens world {world1} <- world {world2}");
+
+        write_file("bindgen1.wasm", &wasm1);
+        write_file("bindgen2.wasm", &wasm2);
+
+        // Merging worlds may fail but if successful then a `Resolve` is asserted
+        // to be valid which is what we're interested in here. Note that failure
+        // here can be due to the structure of worlds which aren't reasonable to
+        // control in this generator, so it's just done to see what happens and try
+        // to trigger panics in `Resolve::assert_valid`.
+        let _ = b1.merge(b2);
+    } else {
+        log::debug!("merging world imports based on semver {world1}");
+        write_file("bindgen1.wasm", &wasm1);
+        let _ = b1.resolve.merge_world_imports_based_on_semver(b1.world);
+    }
     Ok(())
 }
 
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index 7857d86e..9c95f14d 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -141,6 +141,13 @@ pub struct NewOpts {
     /// Use memory.grow to realloc memory and stack allocation.
     #[clap(long)]
     realloc_via_memory_grow: bool,
+
+    /// Indicates whether imports into the final component are merged based on
+    /// semver ranges.
+    ///
+    /// This is enabled by default.
+    #[clap(long, value_name = "MERGE")]
+    merge_imports_based_on_semver: Option<bool>,
 }
 
 impl NewOpts {
@@ -151,9 +158,12 @@ impl NewOpts {
     /// Executes the application.
     fn run(self) -> Result<()> {
         let wasm = self.io.parse_input_wasm()?;
-        let mut encoder = ComponentEncoder::default()
-            .validate(!self.skip_validation)
-            .module(&wasm)?;
+        let mut encoder = ComponentEncoder::default().validate(!self.skip_validation);
+
+        if let Some(merge) = self.merge_imports_based_on_semver {
+            encoder = encoder.merge_imports_based_on_semver(merge);
+        }
+        encoder = encoder.module(&wasm)?;
 
         for (name, wasm) in self.adapters.iter() {
             encoder = encoder.adapter(name, wasm)?;
@@ -392,6 +402,13 @@ pub struct LinkOpts {
     /// Use built-in implementations of `dlopen`/`dlsym`
     #[clap(long)]
     use_built_in_libdl: bool,
+
+    /// Indicates whether imports into the final component are merged based on
+    /// semver ranges.
+    ///
+    /// This is enabled by default.
+    #[clap(long, value_name = "MERGE")]
+    merge_imports_based_on_semver: Option<bool>,
 }
 
 impl LinkOpts {
@@ -410,6 +427,10 @@ impl LinkOpts {
             linker = linker.stack_size(stack_size);
         }
 
+        if let Some(merge) = self.merge_imports_based_on_semver {
+            linker = linker.merge_imports_based_on_semver(merge);
+        }
+
         for (name, wasm) in &self.inputs {
             linker = linker.library(name, wasm, false)?;
         }
@@ -508,7 +529,11 @@ pub struct WitOpts {
     /// of the component which is input.
     ///
     /// This is similar to `--importize-world`, but is used with components.
-    #[clap(long, conflicts_with = "importize_world")]
+    #[clap(
+        long,
+        conflicts_with = "importize_world",
+        conflicts_with = "merge_world_imports_based_on_semver"
+    )]
     importize: bool,
 
     /// The name of the world to generate when using `--importize` or `importize-world`.
@@ -525,9 +550,29 @@ pub struct WitOpts {
     /// to imports.
     ///
     /// This is similar to `--importize`, but is used with WIT packages.
-    #[clap(long, conflicts_with = "importize", value_name = "WORLD")]
+    #[clap(
+        long,
+        conflicts_with = "importize",
+        conflicts_with = "merge_world_imports_based_on_semver",
+        value_name = "WORLD"
+    )]
     importize_world: Option<String>,
 
+    /// Updates the world specified to deduplicate all of its imports based on
+    /// semver versions.
+    ///
+    /// This option can be used to read a WIT world from a package and update it
+    /// to deduplicate WIT imports based on their version. This happens by
+    /// default in the `component new` subcommand for example and this flag can
+    /// be used to explore outside of that command what's happening to the WIT.
+    #[clap(
+        long,
+        conflicts_with = "importize",
+        conflicts_with = "importize_world",
+        value_name = "WORLD"
+    )]
+    merge_world_imports_based_on_semver: Option<String>,
+
     /// Features to enable when parsing the `wit` option.
     ///
     /// This flag enables the `@unstable` feature in WIT documents where the
@@ -560,6 +605,24 @@ impl WitOpts {
                 self.importize_world.as_deref(),
                 self.importize_out_world_name.as_ref(),
             )?;
+        } else if let Some(world) = &self.merge_world_imports_based_on_semver {
+            let (resolve, world_id) = match &mut decoded {
+                DecodedWasm::Component(..) => {
+                    bail!(
+                        "the `--merge-world-imports-based-on-semver` flag is \
+                        not compatible with a component input"
+                    );
+                }
+                DecodedWasm::WitPackage(resolve, id) => {
+                    let world = resolve.select_world(*id, Some(world))?;
+                    (resolve, world)
+                }
+            };
+            resolve
+                .merge_world_imports_based_on_semver(world_id)
+                .context("failed to merge world imports based on semver")?;
+            let resolve = mem::take(resolve);
+            decoded = DecodedWasm::Component(resolve, world_id);
         }
 
         // Now that the WIT document has been decoded, it's time to emit it.
@@ -663,8 +726,6 @@ impl WitOpts {
                 (resolve, world)
             }
         };
-        // let pkg = decoded.package();
-        // let world_id = decoded.resolve().select_world(main, None)?;
         resolve
             .importize(world_id, out_world_name.cloned())
             .context("failed to move world exports to imports")?;


Test patch:
diff --git a/crates/wasm-wave/tests/ui/accept-nullary.out b/crates/wasm-wave/tests/ui/accept-nullary.out
new file mode 100644
index 00000000..34e32f9e
--- /dev/null
+++ b/crates/wasm-wave/tests/ui/accept-nullary.out
@@ -0,0 +1,1 @@
+nullary()
diff --git a/crates/wasm-wave/tests/ui/accept-nullary.waves b/crates/wasm-wave/tests/ui/accept-nullary.waves
new file mode 100644
index 00000000..10c6c5f7
--- /dev/null
+++ b/crates/wasm-wave/tests/ui/accept-nullary.waves
@@ -0,0 +1,1 @@
+nullary();
\ No newline at end of file
diff --git a/crates/wasm-wave/tests/ui/ui.wit b/crates/wasm-wave/tests/ui/ui.wit
index 435a765b..49ba1427 100644
--- a/crates/wasm-wave/tests/ui/ui.wit
+++ b/crates/wasm-wave/tests/ui/ui.wit
@@ -1,6 +1,7 @@
 package ui:tests;
 
 interface ui {
+    nullary: func();
     %float32: func(val: f32);
     %list-float32: func(vals: list<f32>);
     %float64: func(val: f64);
diff --git a/crates/wit-component/tests/components/merge-import-versions-with-adapter/adapt-old.wat b/crates/wit-component/tests/components/merge-import-versions-with-adapter/adapt-old.wat
new file mode 100644
index 00000000..6a32ffa4
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions-with-adapter/adapt-old.wat
@@ -0,0 +1,10 @@
+(module
+  (import "a:b/c@0.1.1" "[constructor]r" (func (result i32)))
+  (import "a:b/c@0.1.1" "[resource-drop]r" (func (param i32)))
+  (import "a:b/c@0.1.1" "[method]r.x" (func (param i32)))
+  (import "a:b/c@0.1.1" "x" (func (param i32 i32)))
+  (import "a:b/c@0.1.1" "y" (func))
+
+  (func (export "f"))
+)
+
diff --git a/crates/wit-component/tests/components/merge-import-versions-with-adapter/adapt-old.wit b/crates/wit-component/tests/components/merge-import-versions-with-adapter/adapt-old.wit
new file mode 100644
index 00000000..842ec11b
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions-with-adapter/adapt-old.wit
@@ -0,0 +1,19 @@
+package foo:foo;
+
+world adapt-old {
+  import a:b/c@0.1.1;
+}
+
+package a:b@0.1.1 {
+  interface c {
+    x: func(x: string);
+    y: func();
+
+    resource r {
+      constructor();
+
+      x: func();
+    }
+  }
+}
+
diff --git a/crates/wit-component/tests/components/merge-import-versions-with-adapter/component.wat b/crates/wit-component/tests/components/merge-import-versions-with-adapter/component.wat
new file mode 100644
index 00000000..8f3e0657
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions-with-adapter/component.wat
@@ -0,0 +1,104 @@
+(component
+  (type (;0;)
+    (instance
+      (export (;0;) "r" (type (sub resource)))
+      (type (;1;) (own 0))
+      (type (;2;) (func (result 1)))
+      (export (;0;) "[constructor]r" (func (type 2)))
+      (type (;3;) (func (param "x" string)))
+      (export (;1;) "x" (func (type 3)))
+    )
+  )
+  (import "a:b/c@0.1.1" (instance (;0;) (type 0)))
+  (core module (;0;)
+    (type (;0;) (func))
+    (type (;1;) (func (result i32)))
+    (type (;2;) (func (param i32)))
+    (type (;3;) (func (param i32 i32)))
+    (import "old" "f" (func (;0;) (type 0)))
+    (import "a:b/c@0.1.0" "[constructor]r" (func (;1;) (type 1)))
+    (import "a:b/c@0.1.0" "[resource-drop]r" (func (;2;) (type 2)))
+    (import "a:b/c@0.1.0" "x" (func (;3;) (type 3)))
+    (memory (;0;) 1)
+    (export "memory" (memory 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+      (processed-by "my-fake-bindgen" "123.45")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func))
+    (export "f" (func 0))
+    (func (;0;) (type 0))
+  )
+  (core module (;2;)
+    (type (;0;) (func))
+    (type (;1;) (func (param i32 i32)))
+    (table (;0;) 2 2 funcref)
+    (export "0" (func $adapt-old-f))
+    (export "1" (func $indirect-a:b/c@0.1.0-x))
+    (export "$imports" (table 0))
+    (func $adapt-old-f (;0;) (type 0)
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (func $indirect-a:b/c@0.1.0-x (;1;) (type 1) (param i32 i32)
+      local.get 0
+      local.get 1
+      i32.const 1
+      call_indirect (type 1)
+    )
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;3;)
+    (type (;0;) (func))
+    (type (;1;) (func (param i32 i32)))
+    (import "" "0" (func (;0;) (type 0)))
+    (import "" "1" (func (;1;) (type 1)))
+    (import "" "$imports" (table (;0;) 2 2 funcref))
+    (elem (;0;) (i32.const 0) func 0 1)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 2))
+  (alias core export 0 "0" (core func (;0;)))
+  (core instance (;1;)
+    (export "f" (func 0))
+  )
+  (alias export 0 "[constructor]r" (func (;0;)))
+  (core func (;1;) (canon lower (func 0)))
+  (alias export 0 "r" (type (;1;)))
+  (core func (;2;) (canon resource.drop 1))
+  (alias core export 0 "1" (core func (;3;)))
+  (core instance (;2;)
+    (export "[constructor]r" (func 1))
+    (export "[resource-drop]r" (func 2))
+    (export "x" (func 3))
+  )
+  (core instance (;3;) (instantiate 0
+      (with "old" (instance 1))
+      (with "a:b/c@0.1.0" (instance 2))
+    )
+  )
+  (alias core export 3 "memory" (core memory (;0;)))
+  (core instance (;4;) (instantiate 1))
+  (alias core export 0 "$imports" (core table (;0;)))
+  (alias core export 4 "f" (core func (;4;)))
+  (alias export 0 "x" (func (;1;)))
+  (core func (;5;) (canon lower (func 1) (memory 0) string-encoding=utf8))
+  (core instance (;5;)
+    (export "$imports" (table 0))
+    (export "0" (func 4))
+    (export "1" (func 5))
+  )
+  (core instance (;6;) (instantiate 3
+      (with "" (instance 5))
+    )
+  )
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/merge-import-versions-with-adapter/component.wit.print b/crates/wit-component/tests/components/merge-import-versions-with-adapter/component.wit.print
new file mode 100644
index 00000000..1a2ed3c5
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions-with-adapter/component.wit.print
@@ -0,0 +1,5 @@
+package root:component;
+
+world root {
+  import a:b/c@0.1.1;
+}
diff --git a/crates/wit-component/tests/components/merge-import-versions-with-adapter/module.wat b/crates/wit-component/tests/components/merge-import-versions-with-adapter/module.wat
new file mode 100644
index 00000000..dafd1568
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions-with-adapter/module.wat
@@ -0,0 +1,9 @@
+(module
+  (import "old" "f" (func))
+
+  (import "a:b/c@0.1.0" "[constructor]r" (func (result i32)))
+  (import "a:b/c@0.1.0" "[resource-drop]r" (func (param i32)))
+  (import "a:b/c@0.1.0" "x" (func (param i32 i32)))
+
+  (memory (export "memory") 1)
+)
diff --git a/crates/wit-component/tests/components/merge-import-versions-with-adapter/module.wit b/crates/wit-component/tests/components/merge-import-versions-with-adapter/module.wit
new file mode 100644
index 00000000..6e60c348
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions-with-adapter/module.wit
@@ -0,0 +1,14 @@
+package foo:foo;
+
+world module {
+  import a:b/c@0.1.0;
+}
+
+package a:b@0.1.0 {
+  interface c {
+    resource r {
+      constructor();
+    }
+    x: func(x: string);
+  }
+}
diff --git a/crates/wit-component/tests/components/merge-import-versions/component.wat b/crates/wit-component/tests/components/merge-import-versions/component.wat
new file mode 100644
index 00000000..aaafd1f0
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions/component.wat
@@ -0,0 +1,108 @@
+(component
+  (type (;0;)
+    (instance
+      (export (;0;) "r" (type (sub resource)))
+      (type (;1;) (own 0))
+      (type (;2;) (func (result 1)))
+      (export (;0;) "[constructor]r" (func (type 2)))
+      (type (;3;) (borrow 0))
+      (type (;4;) (func (param "self" 3)))
+      (export (;1;) "[method]r.x" (func (type 4)))
+      (type (;5;) (func (param "x" string)))
+      (export (;2;) "x" (func (type 5)))
+      (type (;6;) (func))
+      (export (;3;) "y" (func (type 6)))
+    )
+  )
+  (import "a:b/c@0.1.1" (instance (;0;) (type 0)))
+  (core module (;0;)
+    (type (;0;) (func (result i32)))
+    (type (;1;) (func (param i32)))
+    (type (;2;) (func (param i32 i32)))
+    (type (;3;) (func))
+    (import "a:b/c@0.1.0" "[constructor]r" (func (;0;) (type 0)))
+    (import "a:b/c@0.1.0" "[resource-drop]r" (func (;1;) (type 1)))
+    (import "a:b/c@0.1.0" "x" (func (;2;) (type 2)))
+    (import "a:b/c@0.1.1" "[constructor]r" (func (;3;) (type 0)))
+    (import "a:b/c@0.1.1" "[resource-drop]r" (func (;4;) (type 1)))
+    (import "a:b/c@0.1.1" "[method]r.x" (func (;5;) (type 1)))
+    (import "a:b/c@0.1.1" "x" (func (;6;) (type 2)))
+    (import "a:b/c@0.1.1" "y" (func (;7;) (type 3)))
+    (memory (;0;) 1)
+    (export "memory" (memory 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+      (processed-by "my-fake-bindgen" "123.45")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func (param i32 i32)))
+    (table (;0;) 1 1 funcref)
+    (export "1" (func $indirect-a:b/c@0.1.1-x))
+    (export "$imports" (table 0))
+    (func $indirect-a:b/c@0.1.1-x (;0;) (type 0) (param i32 i32)
+      local.get 0
+      local.get 1
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;2;)
+    (type (;0;) (func (param i32 i32)))
+    (import "" "1" (func (;0;) (type 0)))
+    (import "" "$imports" (table (;0;) 1 1 funcref))
+    (elem (;0;) (i32.const 0) func 0)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 1))
+  (alias export 0 "[constructor]r" (func (;0;)))
+  (core func (;0;) (canon lower (func 0)))
+  (alias export 0 "r" (type (;1;)))
+  (core func (;1;) (canon resource.drop 1))
+  (alias core export 0 "1" (core func (;2;)))
+  (core instance (;1;)
+    (export "[constructor]r" (func 0))
+    (export "[resource-drop]r" (func 1))
+    (export "x" (func 2))
+  )
+  (alias export 0 "[constructor]r" (func (;1;)))
+  (core func (;3;) (canon lower (func 1)))
+  (alias export 0 "r" (type (;2;)))
+  (core func (;4;) (canon resource.drop 2))
+  (alias export 0 "[method]r.x" (func (;2;)))
+  (core func (;5;) (canon lower (func 2)))
+  (alias export 0 "y" (func (;3;)))
+  (core func (;6;) (canon lower (func 3)))
+  (core instance (;2;)
+    (export "[constructor]r" (func 3))
+    (export "[resource-drop]r" (func 4))
+    (export "[method]r.x" (func 5))
+    (export "x" (func 2))
+    (export "y" (func 6))
+  )
+  (core instance (;3;) (instantiate 0
+      (with "a:b/c@0.1.0" (instance 1))
+      (with "a:b/c@0.1.1" (instance 2))
+    )
+  )
+  (alias core export 3 "memory" (core memory (;0;)))
+  (alias core export 0 "$imports" (core table (;0;)))
+  (alias export 0 "x" (func (;4;)))
+  (core func (;7;) (canon lower (func 4) (memory 0) string-encoding=utf8))
+  (core instance (;4;)
+    (export "$imports" (table 0))
+    (export "1" (func 7))
+  )
+  (core instance (;5;) (instantiate 2
+      (with "" (instance 4))
+    )
+  )
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/merge-import-versions/component.wit.print b/crates/wit-component/tests/components/merge-import-versions/component.wit.print
new file mode 100644
index 00000000..1a2ed3c5
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions/component.wit.print
@@ -0,0 +1,5 @@
+package root:component;
+
+world root {
+  import a:b/c@0.1.1;
+}
diff --git a/crates/wit-component/tests/components/merge-import-versions/module.wat b/crates/wit-component/tests/components/merge-import-versions/module.wat
new file mode 100644
index 00000000..ce158de3
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions/module.wat
@@ -0,0 +1,13 @@
+(module
+  (import "a:b/c@0.1.0" "[constructor]r" (func (result i32)))
+  (import "a:b/c@0.1.0" "[resource-drop]r" (func (param i32)))
+  (import "a:b/c@0.1.0" "x" (func (param i32 i32)))
+
+  (import "a:b/c@0.1.1" "[constructor]r" (func (result i32)))
+  (import "a:b/c@0.1.1" "[resource-drop]r" (func (param i32)))
+  (import "a:b/c@0.1.1" "[method]r.x" (func (param i32)))
+  (import "a:b/c@0.1.1" "x" (func (param i32 i32)))
+  (import "a:b/c@0.1.1" "y" (func))
+
+  (memory (export "memory") 1)
+)
diff --git a/crates/wit-component/tests/components/merge-import-versions/module.wit b/crates/wit-component/tests/components/merge-import-versions/module.wit
new file mode 100644
index 00000000..eb7234ec
--- /dev/null
+++ b/crates/wit-component/tests/components/merge-import-versions/module.wit
@@ -0,0 +1,28 @@
+package foo:foo;
+
+world module {
+  import a:b/c@0.1.0;
+  import a:b/c@0.1.1;
+}
+
+package a:b@0.1.0 {
+  interface c {
+    resource r {
+      constructor();
+    }
+    x: func(x: string);
+  }
+}
+
+package a:b@0.1.1 {
+  interface c {
+    x: func(x: string);
+    y: func();
+
+    resource r {
+      constructor();
+
+      x: func();
+    }
+  }
+}
diff --git a/tests/cli.rs b/tests/cli.rs
index cf6dd14d..289555b1 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -81,7 +81,7 @@ fn run_test(test: &Path, bless: bool) -> Result<()> {
             }
         }
 
-        match commands.insert(name, (cmd, should_fail)) {
+        match commands.insert(name, (cmd, should_fail, i)) {
             Some(_) => bail!("line {i}: duplicate directive named {name:?}"),
             None => {}
         }
@@ -92,44 +92,62 @@ fn run_test(test: &Path, bless: bool) -> Result<()> {
     }
     let exe = Path::new(env!("CARGO_BIN_EXE_wasm-tools"));
     let tempdir = TempDir::new_in(exe.parent().unwrap())?;
-    for (name, (line, should_fail)) in commands {
-        let mut cmd = Command::new(exe);
-        let mut stdin = None;
-        for arg in line.split_whitespace() {
-            let arg = arg.replace("%tmpdir", tempdir.path().to_str().unwrap());
-            if arg == "|" {
-                let output = execute(&mut cmd, stdin.as_deref(), false)?;
-                stdin = Some(output.stdout);
-                cmd = Command::new(exe);
-            } else if arg == "%" {
-                cmd.arg(test);
-            } else {
-                cmd.arg(arg);
-            }
-        }
+    for (name, (line, should_fail, i)) in commands {
+        run_test_directive(test, &name, &line, bless, should_fail, exe, &tempdir).with_context(
+            || {
+                let kind = if should_fail { "FAIL" } else { "RUN" };
+                format!("failed {kind} directive `{name}` on line {i}")
+            },
+        )?;
+    }
+    Ok(())
+}
 
-        let output = execute(&mut cmd, stdin.as_deref(), should_fail)?;
-        let extension = test.extension().unwrap().to_str().unwrap();
-        let extension = if name.is_empty() {
-            extension.to_string()
+fn run_test_directive(
+    test: &Path,
+    name: &str,
+    line: &str,
+    bless: bool,
+    should_fail: bool,
+    exe: &Path,
+    tempdir: &TempDir,
+) -> Result<()> {
+    let mut cmd = Command::new(exe);
+    let mut stdin = None;
+    for arg in line.split_whitespace() {
+        let arg = arg.replace("%tmpdir", tempdir.path().to_str().unwrap());
+        if arg == "|" {
+            let output = execute(&mut cmd, stdin.as_deref(), false)?;
+            stdin = Some(output.stdout);
+            cmd = Command::new(exe);
+        } else if arg == "%" {
+            cmd.arg(test);
         } else {
-            format!("{extension}.{name}")
-        };
-        assert_output(
-            bless,
-            &output.stdout,
-            &test.with_extension(&format!("{extension}.stdout")),
-            &tempdir,
-        )
-        .context("failed to check stdout expectation (auto-update with BLESS=1)")?;
-        assert_output(
-            bless,
-            &output.stderr,
-            &test.with_extension(&format!("{extension}.stderr")),
-            &tempdir,
-        )
-        .context("failed to check stderr expectation (auto-update with BLESS=1)")?;
+            cmd.arg(arg);
+        }
     }
+
+    let output = execute(&mut cmd, stdin.as_deref(), should_fail)?;
+    let extension = test.extension().unwrap().to_str().unwrap();
+    let extension = if name.is_empty() {
+        extension.to_string()
+    } else {
+        format!("{extension}.{name}")
+    };
+    assert_output(
+        bless,
+        &output.stdout,
+        &test.with_extension(&format!("{extension}.stdout")),
+        &tempdir,
+    )
+    .context("failed to check stdout expectation (auto-update with BLESS=1)")?;
+    assert_output(
+        bless,
+        &output.stderr,
+        &test.with_extension(&format!("{extension}.stderr")),
+        &tempdir,
+    )
+    .context("failed to check stderr expectation (auto-update with BLESS=1)")?;
     Ok(())
 }
 
diff --git a/tests/cli/merge-semver-edge-cases.wit b/tests/cli/merge-semver-edge-cases.wit
new file mode 100644
index 00000000..56c8b1c4
--- /dev/null
+++ b/tests/cli/merge-semver-edge-cases.wit
@@ -0,0 +1,37 @@
+// RUN[new-import-new-dep]: component wit % --merge-world-imports-based-on-semver new-dep
+// RUN[reorder-new-dep]: component wit % --merge-world-imports-based-on-semver reorder-dep
+
+package foo:bar;
+
+world new-dep {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+
+world reorder-dep {
+  import a:b/add-dep-on-prev-foo@0.1.0;
+  import a:b/foo@0.1.0;
+  import a:b/add-dep-on-prev-foo@0.1.1;
+}
+
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+
+  interface add-dep-on-prev-foo {}
+}
+
+package a:b@0.1.1 {
+  interface foo {
+    use bar.{t};
+  }
+
+  interface bar {
+    type t = u32;
+  }
+
+  interface add-dep-on-prev-foo {
+    use a:b/foo@0.1.0.{t};
+  }
+}
diff --git a/tests/cli/merge-semver-edge-cases.wit.new-import-new-dep.stdout b/tests/cli/merge-semver-edge-cases.wit.new-import-new-dep.stdout
new file mode 100644
index 00000000..2148050a
--- /dev/null
+++ b/tests/cli/merge-semver-edge-cases.wit.new-import-new-dep.stdout
@@ -0,0 +1,33 @@
+/// RUN[new-import-new-dep]: component wit % --merge-world-imports-based-on-semver new-dep
+/// RUN[reorder-new-dep]: component wit % --merge-world-imports-based-on-semver reorder-dep
+package foo:bar;
+
+world new-dep {
+  import a:b/bar@0.1.1;
+  import a:b/foo@0.1.1;
+}
+world reorder-dep {
+  import a:b/add-dep-on-prev-foo@0.1.0;
+  import a:b/foo@0.1.0;
+  import a:b/add-dep-on-prev-foo@0.1.1;
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+  interface add-dep-on-prev-foo {
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface bar {
+    type t = u32;
+  }
+  interface foo {
+    use bar.{t};
+  }
+  interface add-dep-on-prev-foo {
+    use a:b/foo@0.1.0.{t};
+  }
+}
diff --git a/tests/cli/merge-semver-edge-cases.wit.reorder-new-dep.stdout b/tests/cli/merge-semver-edge-cases.wit.reorder-new-dep.stdout
new file mode 100644
index 00000000..de791dc0
--- /dev/null
+++ b/tests/cli/merge-semver-edge-cases.wit.reorder-new-dep.stdout
@@ -0,0 +1,33 @@
+/// RUN[new-import-new-dep]: component wit % --merge-world-imports-based-on-semver new-dep
+/// RUN[reorder-new-dep]: component wit % --merge-world-imports-based-on-semver reorder-dep
+package foo:bar;
+
+world new-dep {
+  import a:b/foo@0.1.0;
+  import a:b/bar@0.1.1;
+  import a:b/foo@0.1.1;
+}
+world reorder-dep {
+  import a:b/foo@0.1.0;
+  import a:b/add-dep-on-prev-foo@0.1.1;
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+  interface add-dep-on-prev-foo {
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface bar {
+    type t = u32;
+  }
+  interface foo {
+    use bar.{t};
+  }
+  interface add-dep-on-prev-foo {
+    use a:b/foo@0.1.0.{t};
+  }
+}
diff --git a/tests/cli/merge-semver-update-all-worlds.wit b/tests/cli/merge-semver-update-all-worlds.wit
new file mode 100644
index 00000000..5cf816ff
--- /dev/null
+++ b/tests/cli/merge-semver-update-all-worlds.wit
@@ -0,0 +1,33 @@
+// RUN: component wit --merge-world-imports-based-on-semver a %
+
+package foo:bar;
+
+world a {
+  import a:b/bar@0.1.0;
+  import a:b/foo@0.1.1;
+}
+
+world b {
+  import a:b/bar@0.1.0;
+}
+
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+
+  interface bar {
+    use foo.{t};
+  }
+}
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+  }
+
+  interface bar {
+    use foo.{t};
+  }
+}
+
diff --git a/tests/cli/merge-semver-update-all-worlds.wit.stdout b/tests/cli/merge-semver-update-all-worlds.wit.stdout
new file mode 100644
index 00000000..ea94749d
--- /dev/null
+++ b/tests/cli/merge-semver-update-all-worlds.wit.stdout
@@ -0,0 +1,30 @@
+/// RUN: component wit --merge-world-imports-based-on-semver a %
+package foo:bar;
+
+world a {
+  import a:b/foo@0.1.1;
+  import a:b/bar@0.1.0;
+}
+world b {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/bar@0.1.0;
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+  interface bar {
+    use a:b/foo@0.1.1.{t};
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+  }
+  interface bar {
+    use foo.{t};
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit b/tests/cli/merge-with-similar-versions.wit
new file mode 100644
index 00000000..cf3b4085
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit
@@ -0,0 +1,99 @@
+// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+
+world imports-deps {
+  import a:b/depend-on-foo@0.1.0;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/depend-on-foo@0.1.1;
+  import a:b/depend-on-foo@0.1.0;
+}
+
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+
+world valid-semver {
+  import a:b/valid-semver@0.1.0;
+  import a:b/valid-semver@0.1.1;
+}
+
+world fix-transitive {
+  import a:b/use-foo1@0.1.0;
+  use a:b/foo@0.1.0.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+    x: func();
+  }
+
+  interface depend-on-foo {
+    use foo.{t};
+    x: func() -> t;
+  }
+
+  interface invalid-semver {
+    x: func();
+  }
+
+  interface valid-semver {
+    x: func();
+  }
+
+  interface use-foo1 {
+    use foo.{t};
+    x: func() -> t;
+  }
+}
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+    x: func();
+  }
+
+  interface depend-on-foo {
+    use foo.{t};
+    x: func() -> t;
+  }
+
+  interface invalid-semver {
+    y: func();
+  }
+
+  interface valid-semver {
+    x: func();
+    y: func();
+  }
+
+  interface use-foo2 {
+    use foo.{t};
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit.exports.stdout b/tests/cli/merge-with-similar-versions.wit.exports.stdout
new file mode 100644
index 00000000..0b754fbe
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.exports.stdout
@@ -0,0 +1,97 @@
+/// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+/// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+/// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+/// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+/// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+/// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+/// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+world imports-deps {
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+}
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+world valid-semver {
+  import a:b/valid-semver@0.1.0;
+  import a:b/valid-semver@0.1.1;
+}
+world fix-transitive {
+  import a:b/foo@0.1.0;
+  import a:b/use-foo1@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.0.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    x: func();
+  }
+  interface valid-semver {
+    x: func();
+  }
+  interface use-foo1 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    y: func();
+  }
+  interface valid-semver {
+    x: func();
+
+    y: func();
+  }
+  interface use-foo2 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout b/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout
new file mode 100644
index 00000000..592dfe5a
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout
@@ -0,0 +1,96 @@
+/// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+/// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+/// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+/// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+/// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+/// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+/// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+world imports-deps {
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+}
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+world valid-semver {
+  import a:b/valid-semver@0.1.0;
+  import a:b/valid-semver@0.1.1;
+}
+world fix-transitive {
+  import a:b/foo@0.1.1;
+  import a:b/use-foo1@0.1.0;
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.1.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    x: func();
+  }
+  interface valid-semver {
+    x: func();
+  }
+  interface use-foo1 {
+    use a:b/foo@0.1.1.{t};
+
+    x: func() -> t;
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    y: func();
+  }
+  interface valid-semver {
+    x: func();
+
+    y: func();
+  }
+  interface use-foo2 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout b/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout
new file mode 100644
index 00000000..43ded819
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout
@@ -0,0 +1,95 @@
+/// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+/// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+/// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+/// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+/// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+/// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+/// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+world imports-deps {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+}
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+world valid-semver {
+  import a:b/valid-semver@0.1.0;
+  import a:b/valid-semver@0.1.1;
+}
+world fix-transitive {
+  import a:b/foo@0.1.0;
+  import a:b/use-foo1@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.0.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    x: func();
+  }
+  interface valid-semver {
+    x: func();
+  }
+  interface use-foo1 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    y: func();
+  }
+  interface valid-semver {
+    x: func();
+
+    y: func();
+  }
+  interface use-foo2 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout b/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout
new file mode 100644
index 00000000..786ea17d
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout
@@ -0,0 +1,95 @@
+/// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+/// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+/// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+/// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+/// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+/// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+/// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+world imports-deps {
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+world valid-semver {
+  import a:b/valid-semver@0.1.0;
+  import a:b/valid-semver@0.1.1;
+}
+world fix-transitive {
+  import a:b/foo@0.1.0;
+  import a:b/use-foo1@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.0.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    x: func();
+  }
+  interface valid-semver {
+    x: func();
+  }
+  interface use-foo1 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    y: func();
+  }
+  interface valid-semver {
+    x: func();
+
+    y: func();
+  }
+  interface use-foo2 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit.imports.stdout b/tests/cli/merge-with-similar-versions.wit.imports.stdout
new file mode 100644
index 00000000..e5c716fe
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.imports.stdout
@@ -0,0 +1,96 @@
+/// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+/// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+/// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+/// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+/// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+/// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+/// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.1;
+}
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+world imports-deps {
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+}
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+world valid-semver {
+  import a:b/valid-semver@0.1.0;
+  import a:b/valid-semver@0.1.1;
+}
+world fix-transitive {
+  import a:b/foo@0.1.0;
+  import a:b/use-foo1@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.0.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    x: func();
+  }
+  interface valid-semver {
+    x: func();
+  }
+  interface use-foo1 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    y: func();
+  }
+  interface valid-semver {
+    x: func();
+
+    y: func();
+  }
+  interface use-foo2 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr b/tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr
new file mode 100644
index 00000000..fa9de156
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr
@@ -0,0 +1,6 @@
+error: failed to merge world imports based on semver
+
+Caused by:
+    0: failed to upgrade `a:b/invalid-semver@0.1.0` to `a:b/invalid-semver@0.1.1`, was this semver-compatible update not semver compatible?
+    1: failed to merge interfaces
+    2: expected function `x` to be present
diff --git a/tests/cli/merge-with-similar-versions.wit.valid-semver.stdout b/tests/cli/merge-with-similar-versions.wit.valid-semver.stdout
new file mode 100644
index 00000000..be8d0727
--- /dev/null
+++ b/tests/cli/merge-with-similar-versions.wit.valid-semver.stdout
@@ -0,0 +1,96 @@
+/// RUN[imports]: component wit % --merge-world-imports-based-on-semver imports
+/// RUN[exports]: component wit % --merge-world-imports-based-on-semver exports
+/// RUN[imports-deps]: component wit % --merge-world-imports-based-on-semver imports-deps
+/// RUN[imports-deps2]: component wit % --merge-world-imports-based-on-semver imports-deps2
+/// FAIL[invalid-semver]: component wit % --merge-world-imports-based-on-semver invalid-semver
+/// RUN[valid-semver]: component wit % --merge-world-imports-based-on-semver valid-semver
+/// RUN[fix-transitive]: component wit % --merge-world-imports-based-on-semver fix-transitive
+package foo:bar;
+
+world imports {
+  import a:b/foo@0.1.0;
+  import a:b/foo@0.1.1;
+}
+world exports {
+  export a:b/foo@0.1.0;
+  export a:b/foo@0.1.1;
+}
+world imports-deps {
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+}
+world imports-deps2 {
+  import a:b/foo@0.1.1;
+  import a:b/depend-on-foo@0.1.1;
+  import a:b/foo@0.1.0;
+  import a:b/depend-on-foo@0.1.0;
+}
+world invalid-semver {
+  import a:b/invalid-semver@0.1.0;
+  import a:b/invalid-semver@0.1.1;
+}
+world valid-semver {
+  import a:b/valid-semver@0.1.1;
+}
+world fix-transitive {
+  import a:b/foo@0.1.0;
+  import a:b/use-foo1@0.1.0;
+  import a:b/foo@0.1.1;
+  import a:b/use-foo2@0.1.1;
+  use a:b/foo@0.1.0.{t as t1};
+  use a:b/use-foo1@0.1.0.{t as t2};
+  use a:b/foo@0.1.1.{t as t3};
+  use a:b/use-foo2@0.1.1.{t as t4};
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    x: func();
+  }
+  interface valid-semver {
+    x: func();
+  }
+  interface use-foo1 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+
+    x: func();
+  }
+  interface depend-on-foo {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+  interface invalid-semver {
+    y: func();
+  }
+  interface valid-semver {
+    x: func();
+
+    y: func();
+  }
+  interface use-foo2 {
+    use foo.{t};
+
+    x: func() -> t;
+  }
+}
diff --git a/tests/cli/merge-world-imports-based-on-semver.wit b/tests/cli/merge-world-imports-based-on-semver.wit
new file mode 100644
index 00000000..2b283c93
--- /dev/null
+++ b/tests/cli/merge-world-imports-based-on-semver.wit
@@ -0,0 +1,16 @@
+// RUN: component wit --merge-world-imports-based-on-semver foo %
+
+package a:b;
+
+world foo {
+  import a:b/c@0.1.0;
+  import a:b/c@0.1.1;
+}
+
+package a:b@0.1.0 {
+  interface c {}
+}
+
+package a:b@0.1.1 {
+  interface c {}
+}
diff --git a/tests/cli/merge-world-imports-based-on-semver.wit.stdout b/tests/cli/merge-world-imports-based-on-semver.wit.stdout
new file mode 100644
index 00000000..1c410fd4
--- /dev/null
+++ b/tests/cli/merge-world-imports-based-on-semver.wit.stdout
@@ -0,0 +1,16 @@
+/// RUN: component wit --merge-world-imports-based-on-semver foo %
+package a:b;
+
+world foo {
+  import a:b/c@0.1.1;
+}
+package a:b@0.1.0 {
+  interface c {
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface c {
+  }
+}
diff --git a/tests/cli/merge-world-imports-fixup-exports.wit b/tests/cli/merge-world-imports-fixup-exports.wit
new file mode 100644
index 00000000..d8a0fa31
--- /dev/null
+++ b/tests/cli/merge-world-imports-fixup-exports.wit
@@ -0,0 +1,24 @@
+// RUN: component wit % --merge-world-imports-based-on-semver a
+
+package foo:bar;
+
+world a {
+  import a:b/foo@0.1.1;
+  export a:b/bar@0.1.0;
+}
+
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+
+  interface bar {
+    use foo.{t};
+  }
+}
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+  }
+}
diff --git a/tests/cli/merge-world-imports-fixup-exports.wit.stdout b/tests/cli/merge-world-imports-fixup-exports.wit.stdout
new file mode 100644
index 00000000..813cb1d6
--- /dev/null
+++ b/tests/cli/merge-world-imports-fixup-exports.wit.stdout
@@ -0,0 +1,23 @@
+/// RUN: component wit % --merge-world-imports-based-on-semver a
+package foo:bar;
+
+world a {
+  import a:b/foo@0.1.1;
+
+  export a:b/bar@0.1.0;
+}
+package a:b@0.1.0 {
+  interface foo {
+    type t = u32;
+  }
+  interface bar {
+    use a:b/foo@0.1.1.{t};
+  }
+}
+
+
+package a:b@0.1.1 {
+  interface foo {
+    type t = u32;
+  }
+}