From e85c370520d5b1134d867233d8cac69661c70101 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Wed, 25 Feb 2026 14:04:59 -0700 Subject: [PATCH 1/2] Make WIT merging commutative Signed-off-by: Brian Hardock --- crates/wit-parser/src/resolve/mod.rs | 351 +++++++++++++++++++++++++-- 1 file changed, 331 insertions(+), 20 deletions(-) diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index 12900fe4d0..fea6a2218a 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -537,9 +537,43 @@ package {name} is defined in two different locations:\n\ let mut moved_interfaces = Vec::new(); for (id, mut iface) in interfaces { let new_id = match interface_map.get(&id).copied() { - Some(id) => { - update_stability(&iface.stability, &mut self.interfaces[id].stability)?; - id + Some(into_id) => { + update_stability(&iface.stability, &mut self.interfaces[into_id].stability)?; + + // Add any extra types from `from`'s interface that + // don't exist in `into`'s interface. These types were + // already moved as new types above (since they weren't + // in `type_map`), but they still need to be registered + // in the target interface's `types` map. + for (name, from_type_id) in iface.types.iter() { + if self.interfaces[into_id].types.contains_key(name) { + continue; + } + let new_type_id = remap.map_type(*from_type_id, Default::default())?; + self.interfaces[into_id] + .types + .insert(name.clone(), new_type_id); + } + + // Add any extra functions from `from`'s interface that + // don't exist in `into`'s interface. These need their + // type references remapped and spans adjusted. + let extra_funcs: Vec<_> = iface + .functions + .into_iter() + .filter(|(name, _)| { + !self.interfaces[into_id] + .functions + .contains_key(name.as_str()) + }) + .collect(); + for (name, mut func) in extra_funcs { + remap.update_function(self, &mut func, Default::default())?; + func.adjust_spans(span_offset); + self.interfaces[into_id].functions.insert(name, func); + } + + into_id } None => { log::debug!("moving interface {:?}", iface.name); @@ -676,6 +710,11 @@ package {name} is defined in two different locations:\n\ log::trace!("now have {} packages", self.packages.len()); + // Ensure the interfaces arena is in topological order after the + // merge. Newly-added interfaces may have been appended after + // existing interfaces that now depend on them. + self.topologically_sort_interfaces(Some(&mut remap)); + #[cfg(debug_assertions)] self.assert_valid(); Ok(remap) @@ -1486,6 +1525,149 @@ package {name} is defined in two different locations:\n\ pushed[id.index()] = true; } + /// Rebuilds the interfaces arena in topological order and updates all + /// InterfaceId references throughout this `Resolve`. + /// + /// After a merge, newly-added interfaces may appear after existing + /// interfaces that now depend on them. This method re-orders the + /// interfaces arena so that dependencies always precede dependents + /// within each package. + /// + /// The optional `remap` is updated so that its interface entries reflect + /// the new interface IDs. + fn topologically_sort_interfaces(&mut self, remap: Option<&mut Remap>) { + // Compute a global topological order: iterate packages in topological + // order, and within each package topologically sort interfaces based + // on their `use`-type dependencies. + let mut order = Vec::with_capacity(self.interfaces.len()); + let mut visited = vec![false; self.interfaces.len()]; + + for pkg_id in self.topological_packages() { + let pkg = &self.packages[pkg_id]; + for (_, &iface_id) in pkg.interfaces.iter() { + self.visit_interface_topo(iface_id, pkg_id, &mut visited, &mut order); + } + } + + // Also include interfaces that don't belong to any package (shouldn't + // normally happen, but be safe). + for (id, _) in self.interfaces.iter() { + if !visited[id.index()] { + order.push(id); + } + } + + // Check if already in order — if so, skip the rebuild. + let already_sorted = order + .iter() + .zip(order.iter().skip(1)) + .all(|(a, b)| a.index() < b.index()); + if already_sorted { + return; + } + + // Build old-to-new mapping. + let mut old_to_new: Vec> = vec![None; self.interfaces.len()]; + + // Consume the old arena and put items into a vec for random access. + let old_arena = mem::take(&mut self.interfaces); + let mut items: Vec> = old_arena + .into_iter() + .map(|(_, iface)| Some(iface)) + .collect(); + + // Rebuild the arena in topological order. + for &old_id in &order { + let iface = items[old_id.index()].take().unwrap(); + let new_id = self.interfaces.alloc(iface); + old_to_new[old_id.index()] = Some(new_id); + } + + // Helper closure to map an old InterfaceId to the new one. + let map_iface = |id: InterfaceId| -> InterfaceId { old_to_new[id.index()].unwrap() }; + + // Update all InterfaceId references throughout the Resolve. + + // 1. Package::interfaces + for (_, pkg) in self.packages.iter_mut() { + for (_, id) in pkg.interfaces.iter_mut() { + *id = map_iface(*id); + } + } + + // 2. Types: TypeOwner::Interface + for (_, ty) in self.types.iter_mut() { + if let TypeOwner::Interface(id) = &mut ty.owner { + *id = map_iface(*id); + } + } + + // 3. Worlds: imports/exports with WorldKey::Interface and WorldItem::Interface + for (_, world) in self.worlds.iter_mut() { + let imports = mem::take(&mut world.imports); + for (key, mut item) in imports { + let new_key = match key { + WorldKey::Interface(id) => WorldKey::Interface(map_iface(id)), + other => other, + }; + if let WorldItem::Interface { id, .. } = &mut item { + *id = map_iface(*id); + } + world.imports.insert(new_key, item); + } + let exports = mem::take(&mut world.exports); + for (key, mut item) in exports { + let new_key = match key { + WorldKey::Interface(id) => WorldKey::Interface(map_iface(id)), + other => other, + }; + if let WorldItem::Interface { id, .. } = &mut item { + *id = map_iface(*id); + } + world.exports.insert(new_key, item); + } + } + + // 4. Interface::clone_of + for (_, iface) in self.interfaces.iter_mut() { + if let Some(id) = &mut iface.clone_of { + *id = map_iface(*id); + } + } + + // 5. Update the Remap if provided. + if let Some(remap) = remap { + for entry in remap.interfaces.iter_mut() { + if let Some(id) = entry { + *id = map_iface(*id); + } + } + } + } + + /// Depth-first visit for topological sorting of interfaces within a package. + fn visit_interface_topo( + &self, + id: InterfaceId, + pkg_id: PackageId, + visited: &mut Vec, + order: &mut Vec, + ) { + if visited[id.index()] { + return; + } + visited[id.index()] = true; + + // Visit same-package dependencies first. + for dep in self.interface_direct_deps(id) { + if self.interfaces[dep].package == Some(pkg_id) { + self.visit_interface_topo(dep, pkg_id, visited, order); + } + } + + order.push(id); + } + #[doc(hidden)] pub fn assert_valid(&self) { let mut package_interfaces = Vec::new(); @@ -3942,24 +4124,54 @@ impl<'a> MergeMap<'a> { let from_interface = &self.from.interfaces[from_id]; let into_interface = &self.into.interfaces[into_id]; - // Unlike documents/interfaces above if an interface in `from` - // differs from the interface in `into` then that's considered an - // error. Changing interfaces can reflect changes in imports/exports - // which may not be expected so it's currently required that all - // interfaces, when merged, exactly match. - // - // One case to consider here, for example, is that if a world in - // `into` exports the interface `into_id` then if `from_id` were to - // add more items into `into` then it would unexpectedly require more - // items to be exported which may not work. In an import context this - // might work since it's "just more items available for import", but - // for now a conservative route of "interfaces must match" is taken. + // When merging interfaces, types and functions that exist in both + // `from` and `into` must match structurally. One interface is + // allowed to be a superset of the other (i.e., contain extra types + // or functions), which enables commutative merging of partial views + // of the same interface. However, if BOTH sides have items the + // other doesn't, they're considered incompatible. - for (name, from_type_id) in from_interface.types.iter() { - let into_type_id = *into_interface + let from_has_extra_types = from_interface + .types + .keys() + .any(|name| !into_interface.types.contains_key(name)); + let into_has_extra_types = into_interface + .types + .keys() + .any(|name| !from_interface.types.contains_key(name)); + if from_has_extra_types && into_has_extra_types { + let from_extra: Vec<_> = from_interface .types - .get(name) - .ok_or_else(|| anyhow!("expected type `{name}` to be present"))?; + .keys() + .filter(|n| !into_interface.types.contains_key(n.as_str())) + .collect(); + bail!("expected type `{}` to be present", from_extra[0],); + } + + let from_has_extra_funcs = from_interface + .functions + .keys() + .any(|name| !into_interface.functions.contains_key(name)); + let into_has_extra_funcs = into_interface + .functions + .keys() + .any(|name| !from_interface.functions.contains_key(name)); + if from_has_extra_funcs && into_has_extra_funcs { + let from_extra: Vec<_> = from_interface + .functions + .keys() + .filter(|n| !into_interface.functions.contains_key(n.as_str())) + .collect(); + bail!("expected function `{}` to be present", from_extra[0],); + } + + for (name, from_type_id) in from_interface.types.iter() { + let into_type_id = match into_interface.types.get(name) { + Some(id) => *id, + // Extra type in `from` not present in `into`; it will be + // moved as a new type and added to the interface later. + None => continue, + }; let prev = self.type_map.insert(*from_type_id, into_type_id); assert!(prev.is_none()); @@ -3970,7 +4182,9 @@ impl<'a> MergeMap<'a> { for (name, from_func) in from_interface.functions.iter() { let into_func = match into_interface.functions.get(name) { Some(func) => func, - None => bail!("expected function `{name}` to be present"), + // Extra function in `from` not present in `into`; it will + // be added to the interface during the merge phase. + None => continue, }; self.build_function(from_func, into_func) .with_context(|| format!("mismatch in function `{name}`"))?; @@ -5192,4 +5406,101 @@ interface iface { Ok(()) } + + #[test] + fn merging_is_commutative() -> Result<()> { + let mut resolve1 = Resolve::default(); + resolve1.push_str( + "test1.wit", + r#" + package wasi:io@0.2.0; + + interface error { + resource error; + } + interface streams { + use error.{error}; + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + } + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream; + } + "#, + )?; + + let mut resolve2 = Resolve::default(); + resolve2.push_str( + "test2.wit", + r#" + package wasi:io@0.2.0; + + interface error { + resource error { + to-debug-string: func() -> string; + } + } + interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; + } + interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } + } + world imports { + import error; + import poll; + import streams; + } + "#, + )?; + + let resolve1_clone = resolve1.clone(); + let resolve2_clone = resolve2.clone(); + + let _ = resolve1.merge(resolve2_clone)?; + let _ = resolve2.merge(resolve1_clone)?; + + Ok(()) + } } From 8cf874e748bf2989a15c41da5fcf7f1414bd4764 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Tue, 7 Apr 2026 11:05:54 -0600 Subject: [PATCH 2/2] Address PR feedback Signed-off-by: Brian Hardock --- crates/wit-component/tests/merge.rs | 10 ++ .../tests/merge/bad-interface1/error.txt | 1 - .../tests/merge/bad-interface2/error.txt | 1 - .../commutative-disjoint-funcs/from/a.wit | 5 + .../commutative-disjoint-funcs/into/a.wit | 5 + .../commutative-disjoint-funcs/merge/b.wit | 8 + .../tests/merge/commutative/from/a.wit | 23 +++ .../tests/merge/commutative/into/a.wit | 53 +++++++ .../tests/merge/commutative/merge/io.wit | 53 +++++++ .../from/a.wit | 0 .../from/deps/foo/a.wit | 0 .../into/a.wit | 0 .../into/deps/foo/a.wit | 0 .../tests/merge/disjoint-funcs/merge/foo.wit | 10 ++ .../tests/merge/disjoint-funcs/merge/from.wit | 6 + .../tests/merge/disjoint-funcs/merge/into.wit | 6 + .../from/a.wit | 0 .../from/deps/foo/a.wit | 0 .../into/a.wit | 0 .../into/deps/foo/a.wit | 0 .../tests/merge/disjoint-types/merge/foo.wit | 8 + .../tests/merge/disjoint-types/merge/from.wit | 6 + .../tests/merge/disjoint-types/merge/into.wit | 6 + .../tests/merge/topo-sort-needed/README.md | 14 ++ .../tests/merge/topo-sort-needed/from/a.wit | 19 +++ .../tests/merge/topo-sort-needed/into/a.wit | 7 + .../merge/topo-sort-needed/merge/bar.wit | 21 +++ crates/wit-parser/src/resolve/mod.rs | 141 +----------------- fuzz/src/roundtrip_wit.rs | 35 ++++- tests/cli/merge-with-similar-versions.wit | 2 +- ...e-with-similar-versions.wit.exports.stdout | 2 +- ...similar-versions.wit.fix-transitive.stdout | 2 +- ...h-similar-versions.wit.imports-deps.stdout | 2 +- ...-similar-versions.wit.imports-deps2.stdout | 2 +- ...e-with-similar-versions.wit.imports.stdout | 2 +- ...similar-versions.wit.invalid-semver.stderr | 6 - ...similar-versions.wit.invalid-semver.stdout | 96 ++++++++++++ ...h-similar-versions.wit.valid-semver.stdout | 2 +- 38 files changed, 397 insertions(+), 157 deletions(-) delete mode 100644 crates/wit-component/tests/merge/bad-interface1/error.txt delete mode 100644 crates/wit-component/tests/merge/bad-interface2/error.txt create mode 100644 crates/wit-component/tests/merge/commutative-disjoint-funcs/from/a.wit create mode 100644 crates/wit-component/tests/merge/commutative-disjoint-funcs/into/a.wit create mode 100644 crates/wit-component/tests/merge/commutative-disjoint-funcs/merge/b.wit create mode 100644 crates/wit-component/tests/merge/commutative/from/a.wit create mode 100644 crates/wit-component/tests/merge/commutative/into/a.wit create mode 100644 crates/wit-component/tests/merge/commutative/merge/io.wit rename crates/wit-component/tests/merge/{bad-interface2 => disjoint-funcs}/from/a.wit (100%) rename crates/wit-component/tests/merge/{bad-interface2 => disjoint-funcs}/from/deps/foo/a.wit (100%) rename crates/wit-component/tests/merge/{bad-interface2 => disjoint-funcs}/into/a.wit (100%) rename crates/wit-component/tests/merge/{bad-interface2 => disjoint-funcs}/into/deps/foo/a.wit (100%) create mode 100644 crates/wit-component/tests/merge/disjoint-funcs/merge/foo.wit create mode 100644 crates/wit-component/tests/merge/disjoint-funcs/merge/from.wit create mode 100644 crates/wit-component/tests/merge/disjoint-funcs/merge/into.wit rename crates/wit-component/tests/merge/{bad-interface1 => disjoint-types}/from/a.wit (100%) rename crates/wit-component/tests/merge/{bad-interface1 => disjoint-types}/from/deps/foo/a.wit (100%) rename crates/wit-component/tests/merge/{bad-interface1 => disjoint-types}/into/a.wit (100%) rename crates/wit-component/tests/merge/{bad-interface1 => disjoint-types}/into/deps/foo/a.wit (100%) create mode 100644 crates/wit-component/tests/merge/disjoint-types/merge/foo.wit create mode 100644 crates/wit-component/tests/merge/disjoint-types/merge/from.wit create mode 100644 crates/wit-component/tests/merge/disjoint-types/merge/into.wit create mode 100644 crates/wit-component/tests/merge/topo-sort-needed/README.md create mode 100644 crates/wit-component/tests/merge/topo-sort-needed/from/a.wit create mode 100644 crates/wit-component/tests/merge/topo-sort-needed/into/a.wit create mode 100644 crates/wit-component/tests/merge/topo-sort-needed/merge/bar.wit delete mode 100644 tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr create mode 100644 tests/cli/merge-with-similar-versions.wit.invalid-semver.stdout diff --git a/crates/wit-component/tests/merge.rs b/crates/wit-component/tests/merge.rs index e1cca07c56..2ff7739eef 100644 --- a/crates/wit-component/tests/merge.rs +++ b/crates/wit-component/tests/merge.rs @@ -34,6 +34,9 @@ fn merging() -> Result<()> { from.assert_valid(); into.assert_valid(); + let from_clone = from.clone(); + let into_clone = into.clone(); + match into.merge(from) { Ok(_) => { assert!( @@ -51,6 +54,13 @@ fn merging() -> Result<()> { let output = printer.output.to_string(); assert_output(&expected, &output)?; } + + // Also assert merging in the reverse direction succeeds. + let mut reverse = from_clone; + reverse + .merge(into_clone) + .context("merge succeeded in one direction but failed in reverse")?; + reverse.assert_valid(); } Err(e) => { assert!(test_case.starts_with("bad-"), "failed to merge with {e:?}"); diff --git a/crates/wit-component/tests/merge/bad-interface1/error.txt b/crates/wit-component/tests/merge/bad-interface1/error.txt deleted file mode 100644 index 0558df13d3..0000000000 --- a/crates/wit-component/tests/merge/bad-interface1/error.txt +++ /dev/null @@ -1 +0,0 @@ -failed to merge package `foo:foo` into existing copy: failed to merge interface `a`: expected type `a` to be present \ No newline at end of file diff --git a/crates/wit-component/tests/merge/bad-interface2/error.txt b/crates/wit-component/tests/merge/bad-interface2/error.txt deleted file mode 100644 index 0b3f674bf8..0000000000 --- a/crates/wit-component/tests/merge/bad-interface2/error.txt +++ /dev/null @@ -1 +0,0 @@ -failed to merge package `foo:foo` into existing copy: failed to merge interface `a`: expected function `a` to be present \ No newline at end of file diff --git a/crates/wit-component/tests/merge/commutative-disjoint-funcs/from/a.wit b/crates/wit-component/tests/merge/commutative-disjoint-funcs/from/a.wit new file mode 100644 index 0000000000..1e150dfdc2 --- /dev/null +++ b/crates/wit-component/tests/merge/commutative-disjoint-funcs/from/a.wit @@ -0,0 +1,5 @@ +package a:b; + +interface a { + f1: func(); +} diff --git a/crates/wit-component/tests/merge/commutative-disjoint-funcs/into/a.wit b/crates/wit-component/tests/merge/commutative-disjoint-funcs/into/a.wit new file mode 100644 index 0000000000..80d8f11d24 --- /dev/null +++ b/crates/wit-component/tests/merge/commutative-disjoint-funcs/into/a.wit @@ -0,0 +1,5 @@ +package a:b; + +interface a { + f2: func(); +} diff --git a/crates/wit-component/tests/merge/commutative-disjoint-funcs/merge/b.wit b/crates/wit-component/tests/merge/commutative-disjoint-funcs/merge/b.wit new file mode 100644 index 0000000000..98dd08b029 --- /dev/null +++ b/crates/wit-component/tests/merge/commutative-disjoint-funcs/merge/b.wit @@ -0,0 +1,8 @@ +package a:b; + +interface a { + f2: func(); + + f1: func(); +} + diff --git a/crates/wit-component/tests/merge/commutative/from/a.wit b/crates/wit-component/tests/merge/commutative/from/a.wit new file mode 100644 index 0000000000..bfd8c8f297 --- /dev/null +++ b/crates/wit-component/tests/merge/commutative/from/a.wit @@ -0,0 +1,23 @@ +package wasi:io@0.2.0; + +interface error { + resource error; +} + +interface streams { + use error.{error}; + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + } + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream; +} diff --git a/crates/wit-component/tests/merge/commutative/into/a.wit b/crates/wit-component/tests/merge/commutative/into/a.wit new file mode 100644 index 0000000000..2a95e70eff --- /dev/null +++ b/crates/wit-component/tests/merge/commutative/into/a.wit @@ -0,0 +1,53 @@ +package wasi:io@0.2.0; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + +world imports { + import error; + import poll; + import streams; +} diff --git a/crates/wit-component/tests/merge/commutative/merge/io.wit b/crates/wit-component/tests/merge/commutative/merge/io.wit new file mode 100644 index 0000000000..2a95e70eff --- /dev/null +++ b/crates/wit-component/tests/merge/commutative/merge/io.wit @@ -0,0 +1,53 @@ +package wasi:io@0.2.0; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + +world imports { + import error; + import poll; + import streams; +} diff --git a/crates/wit-component/tests/merge/bad-interface2/from/a.wit b/crates/wit-component/tests/merge/disjoint-funcs/from/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface2/from/a.wit rename to crates/wit-component/tests/merge/disjoint-funcs/from/a.wit diff --git a/crates/wit-component/tests/merge/bad-interface2/from/deps/foo/a.wit b/crates/wit-component/tests/merge/disjoint-funcs/from/deps/foo/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface2/from/deps/foo/a.wit rename to crates/wit-component/tests/merge/disjoint-funcs/from/deps/foo/a.wit diff --git a/crates/wit-component/tests/merge/bad-interface2/into/a.wit b/crates/wit-component/tests/merge/disjoint-funcs/into/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface2/into/a.wit rename to crates/wit-component/tests/merge/disjoint-funcs/into/a.wit diff --git a/crates/wit-component/tests/merge/bad-interface2/into/deps/foo/a.wit b/crates/wit-component/tests/merge/disjoint-funcs/into/deps/foo/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface2/into/deps/foo/a.wit rename to crates/wit-component/tests/merge/disjoint-funcs/into/deps/foo/a.wit diff --git a/crates/wit-component/tests/merge/disjoint-funcs/merge/foo.wit b/crates/wit-component/tests/merge/disjoint-funcs/merge/foo.wit new file mode 100644 index 0000000000..c3fc6683f6 --- /dev/null +++ b/crates/wit-component/tests/merge/disjoint-funcs/merge/foo.wit @@ -0,0 +1,10 @@ +package foo:foo; + +interface a { + type t = u32; + + b: func(); + + a: func(); +} + diff --git a/crates/wit-component/tests/merge/disjoint-funcs/merge/from.wit b/crates/wit-component/tests/merge/disjoint-funcs/merge/from.wit new file mode 100644 index 0000000000..c8b6f48811 --- /dev/null +++ b/crates/wit-component/tests/merge/disjoint-funcs/merge/from.wit @@ -0,0 +1,6 @@ +package foo:%from; + +interface a { + use foo:foo/a.{t}; +} + diff --git a/crates/wit-component/tests/merge/disjoint-funcs/merge/into.wit b/crates/wit-component/tests/merge/disjoint-funcs/merge/into.wit new file mode 100644 index 0000000000..ab8bb5c657 --- /dev/null +++ b/crates/wit-component/tests/merge/disjoint-funcs/merge/into.wit @@ -0,0 +1,6 @@ +package foo:into; + +interface a { + use foo:foo/a.{t}; +} + diff --git a/crates/wit-component/tests/merge/bad-interface1/from/a.wit b/crates/wit-component/tests/merge/disjoint-types/from/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface1/from/a.wit rename to crates/wit-component/tests/merge/disjoint-types/from/a.wit diff --git a/crates/wit-component/tests/merge/bad-interface1/from/deps/foo/a.wit b/crates/wit-component/tests/merge/disjoint-types/from/deps/foo/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface1/from/deps/foo/a.wit rename to crates/wit-component/tests/merge/disjoint-types/from/deps/foo/a.wit diff --git a/crates/wit-component/tests/merge/bad-interface1/into/a.wit b/crates/wit-component/tests/merge/disjoint-types/into/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface1/into/a.wit rename to crates/wit-component/tests/merge/disjoint-types/into/a.wit diff --git a/crates/wit-component/tests/merge/bad-interface1/into/deps/foo/a.wit b/crates/wit-component/tests/merge/disjoint-types/into/deps/foo/a.wit similarity index 100% rename from crates/wit-component/tests/merge/bad-interface1/into/deps/foo/a.wit rename to crates/wit-component/tests/merge/disjoint-types/into/deps/foo/a.wit diff --git a/crates/wit-component/tests/merge/disjoint-types/merge/foo.wit b/crates/wit-component/tests/merge/disjoint-types/merge/foo.wit new file mode 100644 index 0000000000..8527263d2a --- /dev/null +++ b/crates/wit-component/tests/merge/disjoint-types/merge/foo.wit @@ -0,0 +1,8 @@ +package foo:foo; + +interface a { + type b = s32; + + type a = u32; +} + diff --git a/crates/wit-component/tests/merge/disjoint-types/merge/from.wit b/crates/wit-component/tests/merge/disjoint-types/merge/from.wit new file mode 100644 index 0000000000..96ed27f59a --- /dev/null +++ b/crates/wit-component/tests/merge/disjoint-types/merge/from.wit @@ -0,0 +1,6 @@ +package foo:%from; + +interface a { + use foo:foo/a.{a}; +} + diff --git a/crates/wit-component/tests/merge/disjoint-types/merge/into.wit b/crates/wit-component/tests/merge/disjoint-types/merge/into.wit new file mode 100644 index 0000000000..604327da70 --- /dev/null +++ b/crates/wit-component/tests/merge/disjoint-types/merge/into.wit @@ -0,0 +1,6 @@ +package foo:into; + +interface a { + use foo:foo/a.{b}; +} + diff --git a/crates/wit-component/tests/merge/topo-sort-needed/README.md b/crates/wit-component/tests/merge/topo-sort-needed/README.md new file mode 100644 index 0000000000..660ef4123d --- /dev/null +++ b/crates/wit-component/tests/merge/topo-sort-needed/README.md @@ -0,0 +1,14 @@ +During a merge, when `from` contributes a **new** interface that doesn't exist in `into`, that interface gets appended to the **end** of the arena. If an existing interface in `into` now depends on that newly-appended interface (because extra types/functions referencing it were also added), the ordering invariant is violated — the dependency comes *after* its dependent. + +The `topo-sort-needed` test sets up exactly this scenario: + +- **`from`** has two interfaces: `dep` (defines `type t = u32`) and `user` (has `use dep.{t}`, shared function `get`, and extra function `get-dep` that returns `t`). +- **`into`** has only `user` with just the `get` function — no `dep` at all. + +When merging `from` into `into`: + +1. `user` is matched between both sides (same name, same package). The shared function `get` checks out structurally. +2. `dep` has no counterpart in `into`, so it's **appended to the end** of the arena — after `user`. +3. The extra function `get-dep` and the `use dep.{t}` type from `from`'s `user` are added to `into`'s `user`, creating a dependency from `user` → `dep`. + +Now the arena has `[user, dep]` but the dependency goes `user → dep` — `dep` should come first. Without the topological sort, `assert_topologically_sorted` panics with `assertion failed: other_interface_pos <= my_interface_pos`. With the sort, the arena is reordered to `[dep, user]` and the invariant holds. \ No newline at end of file diff --git a/crates/wit-component/tests/merge/topo-sort-needed/from/a.wit b/crates/wit-component/tests/merge/topo-sort-needed/from/a.wit new file mode 100644 index 0000000000..46e5627824 --- /dev/null +++ b/crates/wit-component/tests/merge/topo-sort-needed/from/a.wit @@ -0,0 +1,19 @@ +package foo:bar; + +/// `from` has `dep` and `user`. `user` has a function `get` shared with +/// `into`, and an extra function `get-dep` that references `dep.{t}`. +/// During the merge, `dep` is added to the arena after `user` (because +/// it's a brand-new interface). The extra `use dep.{t}` type in `user` +/// then creates a dependency user->dep where dep comes later in the +/// arena. The topological sort corrects this. + +interface dep { + type t = u32; +} + +interface user { + use dep.{t}; + + get: func() -> u32; + get-dep: func() -> t; +} diff --git a/crates/wit-component/tests/merge/topo-sort-needed/into/a.wit b/crates/wit-component/tests/merge/topo-sort-needed/into/a.wit new file mode 100644 index 0000000000..35b9d0814b --- /dev/null +++ b/crates/wit-component/tests/merge/topo-sort-needed/into/a.wit @@ -0,0 +1,7 @@ +package foo:bar; + +/// `into` only has `user` with `get` — no `dep` interface at all. + +interface user { + get: func() -> u32; +} diff --git a/crates/wit-component/tests/merge/topo-sort-needed/merge/bar.wit b/crates/wit-component/tests/merge/topo-sort-needed/merge/bar.wit new file mode 100644 index 0000000000..9f1309de76 --- /dev/null +++ b/crates/wit-component/tests/merge/topo-sort-needed/merge/bar.wit @@ -0,0 +1,21 @@ +package foo:bar; + +/// `into` only has `user` with `get` — no `dep` interface at all. +interface user { + use dep.{t}; + + get: func() -> u32; + + get-dep: func() -> t; +} + +/// `from` has `dep` and `user`. `user` has a function `get` shared with +/// `into`, and an extra function `get-dep` that references `dep.{t}`. +/// During the merge, `dep` is added to the arena after `user` (because +/// it's a brand-new interface). The extra `use dep.{t}` type in `user` +/// then creates a dependency user->dep where dep comes later in the +/// arena. The topological sort corrects this. +interface dep { + type t = u32; +} + diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index fea6a2218a..011ce59e9a 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -4125,45 +4125,11 @@ impl<'a> MergeMap<'a> { let into_interface = &self.into.interfaces[into_id]; // When merging interfaces, types and functions that exist in both - // `from` and `into` must match structurally. One interface is - // allowed to be a superset of the other (i.e., contain extra types - // or functions), which enables commutative merging of partial views - // of the same interface. However, if BOTH sides have items the - // other doesn't, they're considered incompatible. - - let from_has_extra_types = from_interface - .types - .keys() - .any(|name| !into_interface.types.contains_key(name)); - let into_has_extra_types = into_interface - .types - .keys() - .any(|name| !from_interface.types.contains_key(name)); - if from_has_extra_types && into_has_extra_types { - let from_extra: Vec<_> = from_interface - .types - .keys() - .filter(|n| !into_interface.types.contains_key(n.as_str())) - .collect(); - bail!("expected type `{}` to be present", from_extra[0],); - } - - let from_has_extra_funcs = from_interface - .functions - .keys() - .any(|name| !into_interface.functions.contains_key(name)); - let into_has_extra_funcs = into_interface - .functions - .keys() - .any(|name| !from_interface.functions.contains_key(name)); - if from_has_extra_funcs && into_has_extra_funcs { - let from_extra: Vec<_> = from_interface - .functions - .keys() - .filter(|n| !into_interface.functions.contains_key(n.as_str())) - .collect(); - bail!("expected function `{}` to be present", from_extra[0],); - } + // `from` and `into` must match structurally. Either side is allowed + // to have extra types or functions not present in the other, which + // enables commutative merging of partial views of the same + // interface. The only requirement is that the intersection of the + // two interfaces is compatible. for (name, from_type_id) in from_interface.types.iter() { let into_type_id = match into_interface.types.get(name) { @@ -5406,101 +5372,4 @@ interface iface { Ok(()) } - - #[test] - fn merging_is_commutative() -> Result<()> { - let mut resolve1 = Resolve::default(); - resolve1.push_str( - "test1.wit", - r#" - package wasi:io@0.2.0; - - interface error { - resource error; - } - interface streams { - use error.{error}; - - resource output-stream { - check-write: func() -> result; - write: func(contents: list) -> result<_, stream-error>; - blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; - blocking-flush: func() -> result<_, stream-error>; - } - - variant stream-error { - last-operation-failed(error), - closed, - } - - resource input-stream; - } - "#, - )?; - - let mut resolve2 = Resolve::default(); - resolve2.push_str( - "test2.wit", - r#" - package wasi:io@0.2.0; - - interface error { - resource error { - to-debug-string: func() -> string; - } - } - interface poll { - resource pollable { - ready: func() -> bool; - block: func(); - } - - poll: func(in: list>) -> list; - } - interface streams { - use error.{error}; - use poll.{pollable}; - - variant stream-error { - last-operation-failed(error), - closed, - } - - resource input-stream { - read: func(len: u64) -> result, stream-error>; - blocking-read: func(len: u64) -> result, stream-error>; - skip: func(len: u64) -> result; - blocking-skip: func(len: u64) -> result; - subscribe: func() -> pollable; - } - - resource output-stream { - check-write: func() -> result; - write: func(contents: list) -> result<_, stream-error>; - blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; - flush: func() -> result<_, stream-error>; - blocking-flush: func() -> result<_, stream-error>; - subscribe: func() -> pollable; - write-zeroes: func(len: u64) -> result<_, stream-error>; - blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; - splice: func(src: borrow, len: u64) -> result; - blocking-splice: func(src: borrow, len: u64) -> result; - } - } - world imports { - import error; - import poll; - import streams; - } - "#, - )?; - - let resolve1_clone = resolve1.clone(); - let resolve2_clone = resolve2.clone(); - - let _ = resolve1.merge(resolve2_clone)?; - let _ = resolve2.merge(resolve1_clone)?; - - Ok(()) - } } diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index 480093bb64..500c19ddc7 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -79,8 +79,11 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { // Decode what was just created and record it later for testing merging // worlds together. - let (_, decoded) = wit_component::metadata::decode(&dummy).unwrap(); - decoded_bindgens.push((decoded, dummy, world.name.clone())); + let (dresolve, dworldid) = match wit_component::decode(&wasm).unwrap() { + wit_component::DecodedWasm::Component(r, w) => (r, w), + _ => unreachable!(), + }; + decoded_bindgens.push((dresolve, dworldid, dummy, world.name.clone())); log::debug!("... decoding the component itself"); wit_component::decode(&wasm).unwrap(); @@ -102,27 +105,47 @@ 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 (mut b1, worldid1, wasm1, world1) = decoded_bindgens.swap_remove(i); if u.arbitrary()? { let i = u.choose_index(decoded_bindgens.len())?; - let (b2, wasm2, world2) = decoded_bindgens.swap_remove(i); + let (mut b2, worldid2, 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); + let pkg2 = b2.worlds[worldid2].package.unwrap(); + let mut name = b2.packages[pkg2].name.clone(); + b2.package_names.swap_remove(&name).unwrap(); + name.name.push_str("2"); + let prev = b2.package_names.insert(name.clone(), pkg2); + assert!(prev.is_none()); + b2.packages[pkg2].name = name; + + let only_interfaces = b1.worlds[worldid1] + .imports + .iter() + .chain(b1.worlds[worldid1].exports.iter()) + .chain(b2.worlds[worldid2].imports.iter()) + .chain(b2.worlds[worldid2].exports.iter()) + .all(|(id, _)| matches!(id, wit_parser::WorldKey::Interface(_))); + // 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); + let merge_result = b1.merge(b2); + + if only_interfaces { + merge_result.unwrap(); + } } 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); + let _ = b1.merge_world_imports_based_on_semver(worldid1); } Ok(()) } diff --git a/tests/cli/merge-with-similar-versions.wit b/tests/cli/merge-with-similar-versions.wit index cf3b4085e7..44d8686e15 100644 --- a/tests/cli/merge-with-similar-versions.wit +++ b/tests/cli/merge-with-similar-versions.wit @@ -2,7 +2,7 @@ // 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[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 diff --git a/tests/cli/merge-with-similar-versions.wit.exports.stdout b/tests/cli/merge-with-similar-versions.wit.exports.stdout index 0b754fbe02..c9ed724099 100644 --- a/tests/cli/merge-with-similar-versions.wit.exports.stdout +++ b/tests/cli/merge-with-similar-versions.wit.exports.stdout @@ -2,7 +2,7 @@ /// 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[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; diff --git a/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout b/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout index 592dfe5a83..6cd668ef6a 100644 --- a/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout +++ b/tests/cli/merge-with-similar-versions.wit.fix-transitive.stdout @@ -2,7 +2,7 @@ /// 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[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; diff --git a/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout b/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout index 43ded819a8..996c951f16 100644 --- a/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout +++ b/tests/cli/merge-with-similar-versions.wit.imports-deps.stdout @@ -2,7 +2,7 @@ /// 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[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; diff --git a/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout b/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout index 786ea17da5..dc62c2b35b 100644 --- a/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout +++ b/tests/cli/merge-with-similar-versions.wit.imports-deps2.stdout @@ -2,7 +2,7 @@ /// 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[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; diff --git a/tests/cli/merge-with-similar-versions.wit.imports.stdout b/tests/cli/merge-with-similar-versions.wit.imports.stdout index e5c716fe44..0151bd53c9 100644 --- a/tests/cli/merge-with-similar-versions.wit.imports.stdout +++ b/tests/cli/merge-with-similar-versions.wit.imports.stdout @@ -2,7 +2,7 @@ /// 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[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; diff --git a/tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr b/tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr deleted file mode 100644 index fa9de15642..0000000000 --- a/tests/cli/merge-with-similar-versions.wit.invalid-semver.stderr +++ /dev/null @@ -1,6 +0,0 @@ -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.invalid-semver.stdout b/tests/cli/merge-with-similar-versions.wit.invalid-semver.stdout new file mode 100644 index 0000000000..b6a9da879b --- /dev/null +++ b/tests/cli/merge-with-similar-versions.wit.invalid-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 +/// RUN[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.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.valid-semver.stdout b/tests/cli/merge-with-similar-versions.wit.valid-semver.stdout index be8d0727be..32fe73a9e9 100644 --- a/tests/cli/merge-with-similar-versions.wit.valid-semver.stdout +++ b/tests/cli/merge-with-similar-versions.wit.valid-semver.stdout @@ -2,7 +2,7 @@ /// 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[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;