Skip to content

Commit 33f8d08

Browse files
committed
fix: disambiguate colliding WIT import interface names
When multiple WIT packages exports an interface with the same name (e.g. a:pkg/types and b:pkg/types), the generated Rust trait would have duplicate associated types and getters, causing compilation to hang or fail. Add collision detection that scans import declarations for duplicate interface names. When a collision is found, the full namespace path is prepended to produce unique names (e.g. APkgTypes vs BPkgTypes). Signed-off-by: James Sturtevant <jsturtevant@gmail.com>
1 parent f9d124f commit 33f8d08

File tree

7 files changed

+362
-20
lines changed

7 files changed

+362
-20
lines changed

src/hyperlight_component_util/src/emit.rs

Lines changed: 229 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,51 @@ limitations under the License.
1515
*/
1616

1717
//! A bunch of utilities used by the actual code emit functions
18-
use std::collections::{BTreeMap, BTreeSet, VecDeque};
18+
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
1919
use std::vec::Vec;
2020

2121
use proc_macro2::TokenStream;
2222
use quote::{format_ident, quote};
2323
use syn::Ident;
2424

25-
use crate::etypes::{BoundedTyvar, Defined, Handleable, ImportExport, TypeBound, Tyvar};
25+
use crate::etypes::{
26+
BoundedTyvar, Defined, ExternDecl, ExternDesc, Handleable, ImportExport, TypeBound, Tyvar,
27+
};
28+
29+
/// Scan a list of import extern decls for interface name collisions.
30+
/// Returns the set of interface names that appear more than once.
31+
pub fn find_colliding_import_names(imports: &[ExternDecl]) -> HashSet<String> {
32+
let mut counts = std::collections::HashMap::<String, usize>::new();
33+
for ed in imports {
34+
if let ExternDesc::Instance(_) = &ed.desc {
35+
let wn = split_wit_name(ed.kebab_name);
36+
*counts.entry(wn.name.to_string()).or_default() += 1;
37+
}
38+
}
39+
counts
40+
.into_iter()
41+
.filter(|(_, c)| *c > 1)
42+
.map(|(n, _)| n)
43+
.collect()
44+
}
45+
46+
/// Get the disambiguated type and getter names for an import instance.
47+
/// If the interface name collides with another import, prepend the full
48+
/// kebab-joined namespace path to disambiguate
49+
/// (e.g. "types" from "wasi:http" becomes "WasiHttpTypes"/"wasi_http_types").
50+
pub fn import_member_names(wn: &WitName, collisions: &HashSet<String>) -> (Ident, Ident) {
51+
if collisions.contains(wn.name) {
52+
let prefix = if wn.namespaces.is_empty() {
53+
wn.name.to_string()
54+
} else {
55+
wn.namespaces.join("-")
56+
};
57+
let qualified = format!("{}-{}", prefix, wn.name);
58+
(kebab_to_type(&qualified), kebab_to_getter(&qualified))
59+
} else {
60+
(kebab_to_type(wn.name), kebab_to_getter(wn.name))
61+
}
62+
}
2663

2764
/// A representation of a trait definition that we will eventually
2865
/// emit. This is used to allow easily adding onto the trait each time
@@ -284,6 +321,11 @@ pub struct State<'a, 'b> {
284321
pub is_wasmtime_guest: bool,
285322
/// Are we working on an export or an import of the component type?
286323
pub is_export: bool,
324+
/// Set of interface names that collide across different packages
325+
/// (e.g. "types" appears in both wasi:filesystem/types and wasi:http/types).
326+
/// When a name is in this set, the parent namespace is prepended to
327+
/// disambiguate the trait member name.
328+
pub colliding_import_names: HashSet<String>,
287329
}
288330

289331
/// Create a State with all of its &mut references pointing to
@@ -336,6 +378,7 @@ impl<'a, 'b> State<'a, 'b> {
336378
is_guest,
337379
is_wasmtime_guest,
338380
is_export: false,
381+
colliding_import_names: HashSet::new(),
339382
}
340383
}
341384
pub fn clone<'c>(&'c mut self) -> State<'c, 'b> {
@@ -357,6 +400,7 @@ impl<'a, 'b> State<'a, 'b> {
357400
is_guest: self.is_guest,
358401
is_wasmtime_guest: self.is_wasmtime_guest,
359402
is_export: self.is_export,
403+
colliding_import_names: self.colliding_import_names.clone(),
360404
}
361405
}
362406
/// Obtain a reference to the [`Mod`] that we are currently
@@ -437,10 +481,12 @@ impl<'a, 'b> State<'a, 'b> {
437481
/// variable, given its absolute index (i.e. ignoring
438482
/// [`State::var_offset`])
439483
pub fn noff_var_id(&self, n: u32) -> Ident {
440-
let Some(n) = self.bound_vars[n as usize].origin.last_name() else {
484+
let Some(name) = self.bound_vars[n as usize].origin.last_name() else {
441485
panic!("missing origin on tyvar in rust emit")
442486
};
443-
kebab_to_type(n)
487+
let wn = split_wit_name(name);
488+
let (tn, _) = import_member_names(&wn, &self.colliding_import_names);
489+
tn
444490
}
445491
/// Copy the state, changing it to emit into the helper module of
446492
/// the current trait
@@ -803,3 +849,182 @@ pub fn kebab_to_fn(n: &str) -> FnName {
803849
}
804850
FnName::Plain(kebab_to_snake(n))
805851
}
852+
853+
#[cfg(test)]
854+
mod tests {
855+
use super::*;
856+
use crate::etypes::{ExternDecl, ExternDesc, Instance};
857+
858+
/// Helper to build a minimal `ExternDecl` whose desc is an Instance.
859+
fn instance_decl(kebab_name: &str) -> ExternDecl<'_> {
860+
ExternDecl {
861+
kebab_name,
862+
desc: ExternDesc::Instance(Instance {
863+
exports: Vec::new(),
864+
}),
865+
}
866+
}
867+
868+
/// Helper to build a minimal `ExternDecl` whose desc is a Func (not an Instance).
869+
fn func_decl(kebab_name: &str) -> ExternDecl<'_> {
870+
ExternDecl {
871+
kebab_name,
872+
desc: ExternDesc::Func(crate::etypes::Func {
873+
params: Vec::new(),
874+
result: None,
875+
}),
876+
}
877+
}
878+
879+
// --- split_wit_name tests ---
880+
881+
#[test]
882+
fn split_wit_name_simple() {
883+
let wn = split_wit_name("my-interface");
884+
assert_eq!(wn.name, "my-interface");
885+
assert!(wn.namespaces.is_empty());
886+
}
887+
888+
#[test]
889+
fn split_wit_name_with_package() {
890+
let wn = split_wit_name("wasi:http/types");
891+
assert_eq!(wn.name, "types");
892+
assert_eq!(wn.namespaces, vec!["wasi", "http"]);
893+
}
894+
895+
#[test]
896+
fn split_wit_name_with_version() {
897+
let wn = split_wit_name("wasi:http/types@0.2.0");
898+
assert_eq!(wn.name, "types");
899+
assert_eq!(wn.namespaces, vec!["wasi", "http"]);
900+
}
901+
902+
#[test]
903+
fn split_wit_name_nested_package() {
904+
let wn = split_wit_name("wasi:filesystem/types");
905+
assert_eq!(wn.name, "types");
906+
assert_eq!(wn.namespaces, vec!["wasi", "filesystem"]);
907+
}
908+
909+
// --- find_colliding_import_names tests ---
910+
911+
#[test]
912+
fn no_collisions_with_distinct_names() {
913+
let imports = vec![
914+
instance_decl("wasi:http/types"),
915+
instance_decl("wasi:filesystem/preopens"),
916+
];
917+
let collisions = find_colliding_import_names(&imports);
918+
assert!(collisions.is_empty());
919+
}
920+
921+
#[test]
922+
fn detects_collision_on_same_short_name() {
923+
let imports = vec![
924+
instance_decl("wasi:http/types"),
925+
instance_decl("wasi:filesystem/types"),
926+
];
927+
let collisions = find_colliding_import_names(&imports);
928+
assert_eq!(collisions.len(), 1);
929+
assert!(collisions.contains("types"));
930+
}
931+
932+
#[test]
933+
fn no_collision_for_non_instance_decls() {
934+
let imports = vec![instance_decl("wasi:http/types"), func_decl("types")];
935+
let collisions = find_colliding_import_names(&imports);
936+
assert!(collisions.is_empty());
937+
}
938+
939+
#[test]
940+
fn multiple_collisions() {
941+
let imports = vec![
942+
instance_decl("a:foo/types"),
943+
instance_decl("b:bar/types"),
944+
instance_decl("a:foo/handler"),
945+
instance_decl("c:baz/handler"),
946+
];
947+
let collisions = find_colliding_import_names(&imports);
948+
assert_eq!(collisions.len(), 2);
949+
assert!(collisions.contains("types"));
950+
assert!(collisions.contains("handler"));
951+
}
952+
953+
#[test]
954+
fn single_import_no_collision() {
955+
let imports = vec![instance_decl("wasi:http/types")];
956+
let collisions = find_colliding_import_names(&imports);
957+
assert!(collisions.is_empty());
958+
}
959+
960+
#[test]
961+
fn empty_imports_no_collision() {
962+
let collisions = find_colliding_import_names(&[]);
963+
assert!(collisions.is_empty());
964+
}
965+
966+
// --- import_member_names tests ---
967+
968+
#[test]
969+
fn no_collision_uses_short_name() {
970+
let wn = split_wit_name("wasi:http/types");
971+
let collisions = HashSet::new();
972+
let (ty, getter) = import_member_names(&wn, &collisions);
973+
assert_eq!(ty.to_string(), "Types");
974+
assert_eq!(getter.to_string(), "r#types");
975+
}
976+
977+
#[test]
978+
fn collision_prepends_parent_namespace() {
979+
let wn = split_wit_name("wasi:http/types");
980+
let mut collisions = HashSet::new();
981+
collisions.insert("types".to_string());
982+
let (ty, getter) = import_member_names(&wn, &collisions);
983+
assert_eq!(ty.to_string(), "WasiHttpTypes");
984+
assert_eq!(getter.to_string(), "r#wasi_http_types");
985+
}
986+
987+
#[test]
988+
fn collision_different_parents_produce_different_names() {
989+
let mut collisions = HashSet::new();
990+
collisions.insert("types".to_string());
991+
992+
let wn_http = split_wit_name("wasi:http/types");
993+
let (ty_http, getter_http) = import_member_names(&wn_http, &collisions);
994+
995+
let wn_fs = split_wit_name("wasi:filesystem/types");
996+
let (ty_fs, getter_fs) = import_member_names(&wn_fs, &collisions);
997+
998+
assert_eq!(ty_http.to_string(), "WasiHttpTypes");
999+
assert_eq!(ty_fs.to_string(), "WasiFilesystemTypes");
1000+
assert_ne!(ty_http.to_string(), ty_fs.to_string());
1001+
assert_ne!(getter_http.to_string(), getter_fs.to_string());
1002+
}
1003+
1004+
#[test]
1005+
fn collision_same_parent_different_package_produces_different_names() {
1006+
let mut collisions = HashSet::new();
1007+
collisions.insert("types".to_string());
1008+
1009+
let wn_a = split_wit_name("a:pkg/types");
1010+
let (ty_a, _) = import_member_names(&wn_a, &collisions);
1011+
1012+
let wn_b = split_wit_name("b:pkg/types");
1013+
let (ty_b, _) = import_member_names(&wn_b, &collisions);
1014+
1015+
assert_eq!(ty_a.to_string(), "APkgTypes");
1016+
assert_eq!(ty_b.to_string(), "BPkgTypes");
1017+
assert_ne!(ty_a.to_string(), ty_b.to_string());
1018+
}
1019+
1020+
#[test]
1021+
fn collision_simple_name_uses_name_as_parent() {
1022+
let wn = split_wit_name("types");
1023+
let mut collisions = HashSet::new();
1024+
collisions.insert("types".to_string());
1025+
let (ty, getter) = import_member_names(&wn, &collisions);
1026+
// When there are no namespaces, the name itself is used as prefix
1027+
assert_eq!(ty.to_string(), "TypesTypes");
1028+
assert_eq!(getter.to_string(), "r#types_types");
1029+
}
1030+
}

src/hyperlight_component_util/src/host.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ use proc_macro2::{Ident, TokenStream};
1818
use quote::{format_ident, quote};
1919

2020
use crate::emit::{
21-
FnName, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, kebab_to_getter,
22-
kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name,
21+
FnName, ResourceItemName, State, WitName, find_colliding_import_names, import_member_names,
22+
kebab_to_exports_name, kebab_to_fn, kebab_to_getter, kebab_to_imports_name, kebab_to_namespace,
23+
kebab_to_type, kebab_to_var, split_wit_name,
2324
};
2425
use crate::etypes::{Component, ExternDecl, ExternDesc, Instance, Tyvar};
2526
use crate::hl::{
@@ -264,10 +265,9 @@ fn emit_import_extern_decl<'a, 'b, 'c>(
264265
ExternDesc::Instance(it) => {
265266
let mut s = s.clone();
266267
let wn = split_wit_name(ed.kebab_name);
267-
let type_name = kebab_to_type(wn.name);
268-
let getter = kebab_to_getter(wn.name);
268+
let (type_name, getter) = import_member_names(&wn, &s.colliding_import_names);
269269
let tp = s.cur_trait_path();
270-
let get_self = get_self.with_getter(tp, type_name, getter); //quote! { #get_self let mut slf = &mut #tp::#getter(&mut *slf); };
270+
let get_self = get_self.with_getter(tp, type_name, getter);
271271
emit_import_instance(&mut s, get_self, wn.clone(), it)
272272
}
273273
ExternDesc::Component(_) => {
@@ -326,6 +326,7 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com
326326

327327
let rtsid = format_ident!("{}Resources", r#trait);
328328
s.import_param_var = Some(format_ident!("I"));
329+
s.colliding_import_names = find_colliding_import_names(&ct.imports);
329330
resource::emit_tables(
330331
&mut s,
331332
rtsid.clone(),

src/hyperlight_component_util/src/rtypes.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ use quote::{format_ident, quote};
2424
use syn::Ident;
2525

2626
use crate::emit::{
27-
FnName, ResourceItemName, State, WitName, kebab_to_cons, kebab_to_exports_name, kebab_to_fn,
28-
kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var,
29-
split_wit_name,
27+
FnName, ResourceItemName, State, WitName, find_colliding_import_names, import_member_names,
28+
kebab_to_cons, kebab_to_exports_name, kebab_to_fn, kebab_to_imports_name, kebab_to_namespace,
29+
kebab_to_type, kebab_to_var, split_wit_name,
3030
};
3131
use crate::etypes::{
3232
self, Component, Defined, ExternDecl, ExternDesc, Func, Handleable, ImportExport, Instance,
@@ -98,14 +98,25 @@ fn emit_resource_ref(s: &mut State, n: u32, path: Vec<ImportExport>) -> TokenStr
9898
.iter()
9999
.map(|p| {
100100
let wn = split_wit_name(p.name());
101-
kebab_to_type(wn.name)
101+
if p.imported() && s.colliding_import_names.contains(wn.name) {
102+
let (tn, _) = import_member_names(&wn, &s.colliding_import_names);
103+
tn
104+
} else {
105+
kebab_to_type(wn.name)
106+
}
102107
})
103108
.collect::<Vec<_>>();
104109
let extras = quote! { #(#extras::)* };
105110
let rp = s.root_path();
106111
let tns = iwn.namespace_path();
107112
let instance_mod = kebab_to_namespace(iwn.name);
108-
let instance_type = kebab_to_type(iwn.name);
113+
// Use qualified name for the trait member when accessed through the import type
114+
let instance_type = if path[path.len() - 2].imported() {
115+
let (tn, _) = import_member_names(&iwn, &s.colliding_import_names);
116+
tn
117+
} else {
118+
kebab_to_type(iwn.name)
119+
};
109120
let mut sv = quote! { Self };
110121
if path[path.len() - 2].imported() {
111122
if let Some(iv) = &s.import_param_var {
@@ -740,18 +751,18 @@ fn emit_extern_decl<'a, 'b, 'c>(
740751
TokenStream::new()
741752
};
742753

743-
let getter = kebab_to_getter(wn.name);
754+
let (member_tn, member_getter) = import_member_names(&wn, &s.colliding_import_names);
744755
let rp = s.root_path();
745756
let tns = wn.namespace_path();
746-
let tn = kebab_to_type(wn.name);
757+
let trait_tn = kebab_to_type(wn.name);
747758
let trait_bound = if tns.is_empty() {
748-
quote! { #rp #tn }
759+
quote! { #rp #trait_tn }
749760
} else {
750-
quote! { #rp #tns::#tn }
761+
quote! { #rp #tns::#trait_tn }
751762
};
752763
quote! {
753-
type #tn: #trait_bound #vs;
754-
fn #getter(&mut self) -> impl ::core::borrow::BorrowMut<Self::#tn>;
764+
type #member_tn: #trait_bound #vs;
765+
fn #member_getter(&mut self) -> impl ::core::borrow::BorrowMut<Self::#member_tn>;
755766
}
756767
}
757768
ExternDesc::Component(_) => {
@@ -844,6 +855,7 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com
844855
.map(Clone::clone)
845856
.collect::<VecDeque<_>>();
846857
s.cur_trait = Some(import_name.clone());
858+
s.colliding_import_names = find_colliding_import_names(&ct.imports);
847859
let imports = ct
848860
.imports
849861
.iter()

0 commit comments

Comments
 (0)