diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index e2dc13816..702505578 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -111,3 +111,12 @@ jobs: path: src/tests/rust_guests/witguest/twoworlds.wasm retention-days: 1 if-no-files-found: error + + - name: Upload collision.wasm + if: inputs.config == 'debug' + uses: actions/upload-artifact@v7 + with: + name: collision-wasm + path: src/tests/rust_guests/witguest/collision.wasm + retention-days: 1 + if-no-files-found: error diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index a6a535780..1254d628b 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -87,6 +87,12 @@ jobs: name: twoworlds-wasm path: src/tests/rust_guests/witguest/ + - name: Download collision.wasm + uses: actions/download-artifact@v8 + with: + name: collision-wasm + path: src/tests/rust_guests/witguest/ + - name: Build run: just build ${{ inputs.config }} diff --git a/Justfile b/Justfile index d71630d47..ec7b86278 100644 --- a/Justfile +++ b/Justfile @@ -50,6 +50,7 @@ witguest-wit: {{ if os() == "windows" { "if (-not (Get-Command wasm-tools -ErrorAction SilentlyContinue)) { cargo install --locked wasm-tools }" } else { "command -v wasm-tools >/dev/null 2>&1 || cargo install --locked wasm-tools" } }} cd src/tests/rust_guests/witguest && wasm-tools component wit guest.wit -w -o interface.wasm cd src/tests/rust_guests/witguest && wasm-tools component wit two_worlds.wit -w -o twoworlds.wasm + cd src/tests/rust_guests/witguest && wasm-tools component wit collision-test/ -w -o collision.wasm build-rust-guests target=default-target features="": (witguest-wit) (ensure-cargo-hyperlight) cd src/tests/rust_guests/simpleguest && cargo hyperlight build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} diff --git a/src/hyperlight_component_util/src/emit.rs b/src/hyperlight_component_util/src/emit.rs index d6c67cdf7..427428d47 100644 --- a/src/hyperlight_component_util/src/emit.rs +++ b/src/hyperlight_component_util/src/emit.rs @@ -15,14 +15,51 @@ limitations under the License. */ //! A bunch of utilities used by the actual code emit functions -use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; use std::vec::Vec; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::Ident; -use crate::etypes::{BoundedTyvar, Defined, Handleable, ImportExport, TypeBound, Tyvar}; +use crate::etypes::{ + BoundedTyvar, Defined, ExternDecl, ExternDesc, Handleable, ImportExport, TypeBound, Tyvar, +}; + +/// Scan a list of import extern decls for interface name collisions. +/// Returns the set of interface names that appear more than once. +pub fn find_colliding_import_names(imports: &[ExternDecl]) -> HashSet { + let mut counts = std::collections::HashMap::::new(); + for ed in imports { + if let ExternDesc::Instance(_) = &ed.desc { + let wn = split_wit_name(ed.kebab_name); + *counts.entry(wn.name.to_string()).or_default() += 1; + } + } + counts + .into_iter() + .filter(|(_, c)| *c > 1) + .map(|(n, _)| n) + .collect() +} + +/// Get the disambiguated type and getter names for an import instance. +/// If the interface name collides with another import, prepend the full +/// kebab-joined namespace path to disambiguate +/// (e.g. "types" from "wasi:http" becomes "WasiHttpTypes"/"wasi_http_types"). +pub fn import_member_names(wn: &WitName, collisions: &HashSet) -> (Ident, Ident) { + if collisions.contains(wn.name) { + let prefix = if wn.namespaces.is_empty() { + wn.name.to_string() + } else { + wn.namespaces.join("-") + }; + let qualified = format!("{}-{}", prefix, wn.name); + (kebab_to_type(&qualified), kebab_to_getter(&qualified)) + } else { + (kebab_to_type(wn.name), kebab_to_getter(wn.name)) + } +} /// A representation of a trait definition that we will eventually /// emit. This is used to allow easily adding onto the trait each time @@ -284,6 +321,11 @@ pub struct State<'a, 'b> { pub is_wasmtime_guest: bool, /// Are we working on an export or an import of the component type? pub is_export: bool, + /// Set of interface names that collide across different packages + /// (e.g. "types" appears in both wasi:filesystem/types and wasi:http/types). + /// When a name is in this set, the parent namespace is prepended to + /// disambiguate the trait member name. + pub colliding_import_names: HashSet, } /// Create a State with all of its &mut references pointing to @@ -336,6 +378,7 @@ impl<'a, 'b> State<'a, 'b> { is_guest, is_wasmtime_guest, is_export: false, + colliding_import_names: HashSet::new(), } } pub fn clone<'c>(&'c mut self) -> State<'c, 'b> { @@ -357,6 +400,7 @@ impl<'a, 'b> State<'a, 'b> { is_guest: self.is_guest, is_wasmtime_guest: self.is_wasmtime_guest, is_export: self.is_export, + colliding_import_names: self.colliding_import_names.clone(), } } /// Obtain a reference to the [`Mod`] that we are currently @@ -437,10 +481,12 @@ impl<'a, 'b> State<'a, 'b> { /// variable, given its absolute index (i.e. ignoring /// [`State::var_offset`]) pub fn noff_var_id(&self, n: u32) -> Ident { - let Some(n) = self.bound_vars[n as usize].origin.last_name() else { + let Some(name) = self.bound_vars[n as usize].origin.last_name() else { panic!("missing origin on tyvar in rust emit") }; - kebab_to_type(n) + let wn = split_wit_name(name); + let (tn, _) = import_member_names(&wn, &self.colliding_import_names); + tn } /// Copy the state, changing it to emit into the helper module of /// the current trait @@ -803,3 +849,182 @@ pub fn kebab_to_fn(n: &str) -> FnName { } FnName::Plain(kebab_to_snake(n)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::etypes::{ExternDecl, ExternDesc, Instance}; + + /// Helper to build a minimal `ExternDecl` whose desc is an Instance. + fn instance_decl(kebab_name: &str) -> ExternDecl<'_> { + ExternDecl { + kebab_name, + desc: ExternDesc::Instance(Instance { + exports: Vec::new(), + }), + } + } + + /// Helper to build a minimal `ExternDecl` whose desc is a Func (not an Instance). + fn func_decl(kebab_name: &str) -> ExternDecl<'_> { + ExternDecl { + kebab_name, + desc: ExternDesc::Func(crate::etypes::Func { + params: Vec::new(), + result: None, + }), + } + } + + // --- split_wit_name tests --- + + #[test] + fn split_wit_name_simple() { + let wn = split_wit_name("my-interface"); + assert_eq!(wn.name, "my-interface"); + assert!(wn.namespaces.is_empty()); + } + + #[test] + fn split_wit_name_with_package() { + let wn = split_wit_name("wasi:http/types"); + assert_eq!(wn.name, "types"); + assert_eq!(wn.namespaces, vec!["wasi", "http"]); + } + + #[test] + fn split_wit_name_with_version() { + let wn = split_wit_name("wasi:http/types@0.2.0"); + assert_eq!(wn.name, "types"); + assert_eq!(wn.namespaces, vec!["wasi", "http"]); + } + + #[test] + fn split_wit_name_nested_package() { + let wn = split_wit_name("wasi:filesystem/types"); + assert_eq!(wn.name, "types"); + assert_eq!(wn.namespaces, vec!["wasi", "filesystem"]); + } + + // --- find_colliding_import_names tests --- + + #[test] + fn no_collisions_with_distinct_names() { + let imports = vec![ + instance_decl("wasi:http/types"), + instance_decl("wasi:filesystem/preopens"), + ]; + let collisions = find_colliding_import_names(&imports); + assert!(collisions.is_empty()); + } + + #[test] + fn detects_collision_on_same_short_name() { + let imports = vec![ + instance_decl("wasi:http/types"), + instance_decl("wasi:filesystem/types"), + ]; + let collisions = find_colliding_import_names(&imports); + assert_eq!(collisions.len(), 1); + assert!(collisions.contains("types")); + } + + #[test] + fn no_collision_for_non_instance_decls() { + let imports = vec![instance_decl("wasi:http/types"), func_decl("types")]; + let collisions = find_colliding_import_names(&imports); + assert!(collisions.is_empty()); + } + + #[test] + fn multiple_collisions() { + let imports = vec![ + instance_decl("a:foo/types"), + instance_decl("b:bar/types"), + instance_decl("a:foo/handler"), + instance_decl("c:baz/handler"), + ]; + let collisions = find_colliding_import_names(&imports); + assert_eq!(collisions.len(), 2); + assert!(collisions.contains("types")); + assert!(collisions.contains("handler")); + } + + #[test] + fn single_import_no_collision() { + let imports = vec![instance_decl("wasi:http/types")]; + let collisions = find_colliding_import_names(&imports); + assert!(collisions.is_empty()); + } + + #[test] + fn empty_imports_no_collision() { + let collisions = find_colliding_import_names(&[]); + assert!(collisions.is_empty()); + } + + // --- import_member_names tests --- + + #[test] + fn no_collision_uses_short_name() { + let wn = split_wit_name("wasi:http/types"); + let collisions = HashSet::new(); + let (ty, getter) = import_member_names(&wn, &collisions); + assert_eq!(ty.to_string(), "Types"); + assert_eq!(getter.to_string(), "r#types"); + } + + #[test] + fn collision_prepends_parent_namespace() { + let wn = split_wit_name("wasi:http/types"); + let mut collisions = HashSet::new(); + collisions.insert("types".to_string()); + let (ty, getter) = import_member_names(&wn, &collisions); + assert_eq!(ty.to_string(), "WasiHttpTypes"); + assert_eq!(getter.to_string(), "r#wasi_http_types"); + } + + #[test] + fn collision_different_parents_produce_different_names() { + let mut collisions = HashSet::new(); + collisions.insert("types".to_string()); + + let wn_http = split_wit_name("wasi:http/types"); + let (ty_http, getter_http) = import_member_names(&wn_http, &collisions); + + let wn_fs = split_wit_name("wasi:filesystem/types"); + let (ty_fs, getter_fs) = import_member_names(&wn_fs, &collisions); + + assert_eq!(ty_http.to_string(), "WasiHttpTypes"); + assert_eq!(ty_fs.to_string(), "WasiFilesystemTypes"); + assert_ne!(ty_http.to_string(), ty_fs.to_string()); + assert_ne!(getter_http.to_string(), getter_fs.to_string()); + } + + #[test] + fn collision_same_parent_different_package_produces_different_names() { + let mut collisions = HashSet::new(); + collisions.insert("types".to_string()); + + let wn_a = split_wit_name("a:pkg/types"); + let (ty_a, _) = import_member_names(&wn_a, &collisions); + + let wn_b = split_wit_name("b:pkg/types"); + let (ty_b, _) = import_member_names(&wn_b, &collisions); + + assert_eq!(ty_a.to_string(), "APkgTypes"); + assert_eq!(ty_b.to_string(), "BPkgTypes"); + assert_ne!(ty_a.to_string(), ty_b.to_string()); + } + + #[test] + fn collision_simple_name_uses_name_as_parent() { + let wn = split_wit_name("types"); + let mut collisions = HashSet::new(); + collisions.insert("types".to_string()); + let (ty, getter) = import_member_names(&wn, &collisions); + // When there are no namespaces, the name itself is used as prefix + assert_eq!(ty.to_string(), "TypesTypes"); + assert_eq!(getter.to_string(), "r#types_types"); + } +} diff --git a/src/hyperlight_component_util/src/guest.rs b/src/hyperlight_component_util/src/guest.rs index c6d28c11d..18e555e3d 100644 --- a/src/hyperlight_component_util/src/guest.rs +++ b/src/hyperlight_component_util/src/guest.rs @@ -18,9 +18,9 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use crate::emit::{ - FnName, ResolvedBoundVar, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, - kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, - split_wit_name, + FnName, ResolvedBoundVar, ResourceItemName, State, WitName, find_colliding_import_names, + import_member_names, kebab_to_exports_name, kebab_to_fn, kebab_to_getter, + kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name, }; use crate::etypes::{Component, Defined, ExternDecl, ExternDesc, Handleable, Instance, Tyvar}; use crate::hl::{ @@ -117,8 +117,7 @@ fn emit_import_extern_decl<'a, 'b, 'c>( let wn = split_wit_name(ed.kebab_name); emit_import_instance(s, wn.clone(), it); - let getter = kebab_to_getter(wn.name); - let tn = kebab_to_type(wn.name); + let (tn, getter) = import_member_names(&wn, &s.colliding_import_names); quote! { type #tn = Self; #[allow(refining_impl_trait)] @@ -287,6 +286,7 @@ fn emit_component<'a, 'b, 'c>( let export_trait = kebab_to_exports_name(wn.name); s.import_param_var = Some(format_ident!("I")); s.self_param_var = Some(format_ident!("S")); + s.colliding_import_names = find_colliding_import_names(&ct.imports); let rtsid = format_ident!("{}Resources", r#trait); resource::emit_tables( diff --git a/src/hyperlight_component_util/src/host.rs b/src/hyperlight_component_util/src/host.rs index 79b682332..eb94c699e 100644 --- a/src/hyperlight_component_util/src/host.rs +++ b/src/hyperlight_component_util/src/host.rs @@ -18,8 +18,9 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use crate::emit::{ - FnName, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, kebab_to_getter, - kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name, + FnName, ResourceItemName, State, WitName, find_colliding_import_names, import_member_names, + kebab_to_exports_name, kebab_to_fn, kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, + kebab_to_type, kebab_to_var, split_wit_name, }; use crate::etypes::{Component, ExternDecl, ExternDesc, Instance, Tyvar}; use crate::hl::{ @@ -135,8 +136,13 @@ fn emit_export_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: & let (root_ns, root_base_name) = s.root_component_name.unwrap(); let wrapper_name = kebab_to_wrapper_name(root_base_name); let imports_name = kebab_to_imports_name(root_base_name); + let trait_path = if ns.is_empty() { + quote! { #trait_name } + } else { + quote! { #ns::#trait_name } + }; s.root_mod.items.extend(quote! { - impl #ns::#trait_name <#(#tvs),*> for #wrapper_name { + impl #trait_path <#(#tvs),*> for #wrapper_name { #(#exports)* } }); @@ -259,10 +265,9 @@ fn emit_import_extern_decl<'a, 'b, 'c>( ExternDesc::Instance(it) => { let mut s = s.clone(); let wn = split_wit_name(ed.kebab_name); - let type_name = kebab_to_type(wn.name); - let getter = kebab_to_getter(wn.name); + let (type_name, getter) = import_member_names(&wn, &s.colliding_import_names); let tp = s.cur_trait_path(); - let get_self = get_self.with_getter(tp, type_name, getter); //quote! { #get_self let mut slf = &mut #tp::#getter(&mut *slf); }; + let get_self = get_self.with_getter(tp, type_name, getter); emit_import_instance(&mut s, get_self, wn.clone(), it) } ExternDesc::Component(_) => { @@ -321,6 +326,7 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com let rtsid = format_ident!("{}Resources", r#trait); s.import_param_var = Some(format_ident!("I")); + s.colliding_import_names = find_colliding_import_names(&ct.imports); resource::emit_tables( &mut s, rtsid.clone(), diff --git a/src/hyperlight_component_util/src/rtypes.rs b/src/hyperlight_component_util/src/rtypes.rs index 427344dae..f6e45b9c6 100644 --- a/src/hyperlight_component_util/src/rtypes.rs +++ b/src/hyperlight_component_util/src/rtypes.rs @@ -24,9 +24,9 @@ use quote::{format_ident, quote}; use syn::Ident; use crate::emit::{ - FnName, ResourceItemName, State, WitName, kebab_to_cons, kebab_to_exports_name, kebab_to_fn, - kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, - split_wit_name, + FnName, ResourceItemName, State, WitName, find_colliding_import_names, import_member_names, + kebab_to_cons, kebab_to_exports_name, kebab_to_fn, kebab_to_imports_name, kebab_to_namespace, + kebab_to_type, kebab_to_var, split_wit_name, }; use crate::etypes::{ self, Component, Defined, ExternDecl, ExternDesc, Func, Handleable, ImportExport, Instance, @@ -98,14 +98,25 @@ fn emit_resource_ref(s: &mut State, n: u32, path: Vec) -> TokenStr .iter() .map(|p| { let wn = split_wit_name(p.name()); - kebab_to_type(wn.name) + if p.imported() && s.colliding_import_names.contains(wn.name) { + let (tn, _) = import_member_names(&wn, &s.colliding_import_names); + tn + } else { + kebab_to_type(wn.name) + } }) .collect::>(); let extras = quote! { #(#extras::)* }; let rp = s.root_path(); let tns = iwn.namespace_path(); let instance_mod = kebab_to_namespace(iwn.name); - let instance_type = kebab_to_type(iwn.name); + // Use qualified name for the trait member when accessed through the import type + let instance_type = if path[path.len() - 2].imported() { + let (tn, _) = import_member_names(&iwn, &s.colliding_import_names); + tn + } else { + kebab_to_type(iwn.name) + }; let mut sv = quote! { Self }; if path[path.len() - 2].imported() { if let Some(iv) = &s.import_param_var { @@ -120,7 +131,12 @@ fn emit_resource_ref(s: &mut State, n: u32, path: Vec) -> TokenStr trait_path.push(rtrait.clone()); let t = s.resolve_trait_immut(true, &trait_path); let tvis = emit_tvis(s, t.tv_idxs()); - quote! { <#sv::#extras #instance_type as #rp #tns::#instance_mod::#rtrait #tvis>::T } + let trait_ref = if tns.is_empty() { + quote! { #rp #instance_mod::#rtrait } + } else { + quote! { #rp #tns::#instance_mod::#rtrait } + }; + quote! { <#sv::#extras #instance_type as #trait_ref #tvis>::T } } /// Try to find a way to refer to the given type variable from the @@ -174,7 +190,11 @@ fn try_find_local_var_id( let rp = s.root_path(); let tns = wn.namespace_path(); let helper = kebab_to_namespace(wn.name); - Some(quote! { #rp #tns::#helper::#name }) + if tns.is_empty() { + Some(quote! { #rp #helper::#name }) + } else { + Some(quote! { #rp #tns::#helper::#name }) + } } else { let hp = s.helper_path(); Some(quote! { #hp #name }) @@ -731,13 +751,18 @@ fn emit_extern_decl<'a, 'b, 'c>( TokenStream::new() }; - let getter = kebab_to_getter(wn.name); + let (member_tn, member_getter) = import_member_names(&wn, &s.colliding_import_names); let rp = s.root_path(); let tns = wn.namespace_path(); - let tn = kebab_to_type(wn.name); + let trait_tn = kebab_to_type(wn.name); + let trait_bound = if tns.is_empty() { + quote! { #rp #trait_tn } + } else { + quote! { #rp #tns::#trait_tn } + }; quote! { - type #tn: #rp #tns::#tn #vs; - fn #getter(&mut self) -> impl ::core::borrow::BorrowMut; + type #member_tn: #trait_bound #vs; + fn #member_getter(&mut self) -> impl ::core::borrow::BorrowMut; } } ExternDesc::Component(_) => { @@ -830,6 +855,7 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com .map(Clone::clone) .collect::>(); s.cur_trait = Some(import_name.clone()); + s.colliding_import_names = find_colliding_import_names(&ct.imports); let imports = ct .imports .iter() diff --git a/src/hyperlight_host/tests/wit_test.rs b/src/hyperlight_host/tests/wit_test.rs index 7d8f646f9..7c5f8c90e 100644 --- a/src/hyperlight_host/tests/wit_test.rs +++ b/src/hyperlight_host/tests/wit_test.rs @@ -472,3 +472,82 @@ mod pick_world_binding_test2 { assert_eq!(first_import.r#value, "dummyValue"); } } + +// Test that name collision disambiguation works correctly. +// Both a:pkg and b:pkg define an interface called "types". +// The collision detection should generate disambiguated names +// (e.g., PkgTypes instead of just Types) for the import trait members. +mod collision_bindings { + hyperlight_component_macro::host_bindgen!("../tests/rust_guests/witguest/collision.wasm"); +} +mod collision_test { + use crate::collision_bindings::*; + + // Verify that the disambiguated types and record types exist + // and can be instantiated. This confirms that two interfaces + // both named "types" from different packages (a:pkg and b:pkg) + // generate distinct trait members (APkgTypes vs BPkgTypes). + #[test] + fn collision_types_are_distinct() { + let info = a::pkg::types::Info { + name: String::from("hello"), + value: 42, + }; + let detail = b::pkg::types::Detail { + label: String::from("world"), + count: 99, + }; + assert_eq!(info.name, "hello"); + assert_eq!(info.value, 42); + assert_eq!(detail.label, "world"); + assert_eq!(detail.count, 99); + } + + // Verify the imports trait has distinct associated types for each + // colliding interface, ensuring a host implementation can provide + // separate implementations for each. + struct CollisionHost; + + impl a::pkg::Types for CollisionHost { + fn get_info(&mut self) -> a::pkg::types::Info { + a::pkg::types::Info { + name: String::from("test-info"), + value: 1, + } + } + } + + impl b::pkg::Types for CollisionHost { + fn get_detail(&mut self) -> b::pkg::types::Detail { + b::pkg::types::Detail { + label: String::from("test-detail"), + count: 2, + } + } + } + + #[allow(refining_impl_trait)] + impl test::collision::CollisionTestImports for CollisionHost { + type APkgTypes = Self; + fn a_pkg_types(&mut self) -> &mut Self { + self + } + type BPkgTypes = Self; + fn b_pkg_types(&mut self) -> &mut Self { + self + } + } + + #[test] + fn collision_host_impl_compiles() { + // This test verifies that the trait implementation compiles correctly, + // meaning the generated trait has distinct associated types for each + // colliding import. The existence of this test (and the fact it compiles) + // is the assertion. + let mut host = CollisionHost; + let info = a::pkg::Types::get_info(&mut host); + assert_eq!(info.name, "test-info"); + let detail = b::pkg::Types::get_detail(&mut host); + assert_eq!(detail.label, "test-detail"); + } +} diff --git a/src/tests/rust_guests/witguest/collision-test/deps/a-pkg/types.wit b/src/tests/rust_guests/witguest/collision-test/deps/a-pkg/types.wit new file mode 100644 index 000000000..7a6b7ad68 --- /dev/null +++ b/src/tests/rust_guests/witguest/collision-test/deps/a-pkg/types.wit @@ -0,0 +1,9 @@ +package a:pkg; + +interface types { + record info { + name: string, + value: u32, + } + get-info: func() -> info; +} diff --git a/src/tests/rust_guests/witguest/collision-test/deps/b-pkg/types.wit b/src/tests/rust_guests/witguest/collision-test/deps/b-pkg/types.wit new file mode 100644 index 000000000..c393342b5 --- /dev/null +++ b/src/tests/rust_guests/witguest/collision-test/deps/b-pkg/types.wit @@ -0,0 +1,9 @@ +package b:pkg; + +interface types { + record detail { + label: string, + count: u64, + } + get-detail: func() -> detail; +} diff --git a/src/tests/rust_guests/witguest/collision-test/world.wit b/src/tests/rust_guests/witguest/collision-test/world.wit new file mode 100644 index 000000000..122db1d69 --- /dev/null +++ b/src/tests/rust_guests/witguest/collision-test/world.wit @@ -0,0 +1,7 @@ +package test:collision; + +world collision-test { + import a:pkg/types; + import b:pkg/types; + export handle: func() -> string; +}