@@ -15,14 +15,111 @@ 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 , HashMap , HashSet , VecDeque } ;
1919use std:: vec:: Vec ;
2020
2121use proc_macro2:: TokenStream ;
2222use quote:: { format_ident, quote} ;
2323use 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+ /// Import interface names that need generated trait member disambiguation.
30+ #[ derive( Clone , Debug , Default ) ]
31+ pub struct ImportNameCollisions {
32+ short_names : HashSet < String > ,
33+ qualified_names : HashSet < ( Vec < String > , String ) > ,
34+ }
35+ impl ImportNameCollisions {
36+ pub fn is_empty ( & self ) -> bool {
37+ self . short_names . is_empty ( ) && self . qualified_names . is_empty ( )
38+ }
39+ pub fn len ( & self ) -> usize {
40+ self . short_names . len ( )
41+ }
42+ pub fn contains ( & self , name : & str ) -> bool {
43+ self . short_names . contains ( name)
44+ }
45+ fn qualified_contains ( & self , wn : & WitName ) -> bool {
46+ self . qualified_names . contains ( & qualified_import_key ( wn) )
47+ }
48+ }
49+
50+ fn qualified_import_key ( wn : & WitName ) -> ( Vec < String > , String ) {
51+ (
52+ wn. namespaces . iter ( ) . map ( |ns| ns. to_string ( ) ) . collect ( ) ,
53+ wn. name . to_string ( ) ,
54+ )
55+ }
56+
57+ fn version_to_kebab ( version : & [ & str ] ) -> String {
58+ version
59+ . join ( "-" )
60+ . chars ( )
61+ . map ( |c| {
62+ if c. is_ascii_alphanumeric ( ) {
63+ c. to_ascii_lowercase ( )
64+ } else {
65+ '-'
66+ }
67+ } )
68+ . collect ( )
69+ }
70+
71+ /// Scan a list of import extern decls for interface name collisions.
72+ /// Returns import names that need disambiguating, tracking when the
73+ /// namespace-qualified name still collides and must include the version.
74+ pub fn find_colliding_import_names ( imports : & [ ExternDecl ] ) -> ImportNameCollisions {
75+ let mut short_counts = HashMap :: < String , usize > :: new ( ) ;
76+ let mut qualified_counts = HashMap :: < ( Vec < String > , String ) , usize > :: new ( ) ;
77+ for ed in imports {
78+ if let ExternDesc :: Instance ( _) = & ed. desc {
79+ let wn = split_wit_name ( ed. kebab_name ) ;
80+ * short_counts. entry ( wn. name . to_string ( ) ) . or_default ( ) += 1 ;
81+ * qualified_counts
82+ . entry ( qualified_import_key ( & wn) )
83+ . or_default ( ) += 1 ;
84+ }
85+ }
86+ let short_names = short_counts
87+ . into_iter ( )
88+ . filter ( |( _, c) | * c > 1 )
89+ . map ( |( n, _) | n)
90+ . collect ( ) ;
91+ let qualified_names = qualified_counts
92+ . into_iter ( )
93+ . filter ( |( _, c) | * c > 1 )
94+ . map ( |( n, _) | n)
95+ . collect ( ) ;
96+ ImportNameCollisions {
97+ short_names,
98+ qualified_names,
99+ }
100+ }
101+
102+ /// Get the disambiguated type and getter names for an import instance.
103+ /// If the interface name collides with another import, prepend the full
104+ /// kebab-joined namespace path to disambiguate
105+ /// (e.g. "types" from "wasi:http" becomes "WasiHttpTypes"/"wasi_http_types").
106+ pub fn import_member_names ( wn : & WitName , collisions : & ImportNameCollisions ) -> ( Ident , Ident ) {
107+ if collisions. contains ( wn. name ) {
108+ let prefix = if wn. namespaces . is_empty ( ) {
109+ wn. name . to_string ( )
110+ } else {
111+ wn. namespaces . join ( "-" )
112+ } ;
113+ let mut qualified = format ! ( "{}-{}" , prefix, wn. name) ;
114+ if collisions. qualified_contains ( wn) && !wn. _version . is_empty ( ) {
115+ qualified. push_str ( "-v" ) ;
116+ qualified. push_str ( & version_to_kebab ( & wn. _version ) ) ;
117+ }
118+ ( kebab_to_type ( & qualified) , kebab_to_getter ( & qualified) )
119+ } else {
120+ ( kebab_to_type ( wn. name ) , kebab_to_getter ( wn. name ) )
121+ }
122+ }
26123
27124/// A representation of a trait definition that we will eventually
28125/// emit. This is used to allow easily adding onto the trait each time
@@ -284,6 +381,11 @@ pub struct State<'a, 'b> {
284381 pub is_wasmtime_guest : bool ,
285382 /// Are we working on an export or an import of the component type?
286383 pub is_export : bool ,
384+ /// Set of interface names that collide across different packages
385+ /// (e.g. "types" appears in both wasi:filesystem/types and wasi:http/types).
386+ /// When a name is in this set, the parent namespace is prepended to
387+ /// disambiguate the trait member name.
388+ pub colliding_import_names : ImportNameCollisions ,
287389}
288390
289391/// Create a State with all of its &mut references pointing to
@@ -336,6 +438,7 @@ impl<'a, 'b> State<'a, 'b> {
336438 is_guest,
337439 is_wasmtime_guest,
338440 is_export : false ,
441+ colliding_import_names : ImportNameCollisions :: default ( ) ,
339442 }
340443 }
341444 pub fn clone < ' c > ( & ' c mut self ) -> State < ' c , ' b > {
@@ -357,6 +460,7 @@ impl<'a, 'b> State<'a, 'b> {
357460 is_guest : self . is_guest ,
358461 is_wasmtime_guest : self . is_wasmtime_guest ,
359462 is_export : self . is_export ,
463+ colliding_import_names : self . colliding_import_names . clone ( ) ,
360464 }
361465 }
362466 /// Obtain a reference to the [`Mod`] that we are currently
@@ -437,10 +541,12 @@ impl<'a, 'b> State<'a, 'b> {
437541 /// variable, given its absolute index (i.e. ignoring
438542 /// [`State::var_offset`])
439543 pub fn noff_var_id ( & self , n : u32 ) -> Ident {
440- let Some ( n ) = self . bound_vars [ n as usize ] . origin . last_name ( ) else {
544+ let Some ( name ) = self . bound_vars [ n as usize ] . origin . last_name ( ) else {
441545 panic ! ( "missing origin on tyvar in rust emit" )
442546 } ;
443- kebab_to_type ( n)
547+ let wn = split_wit_name ( name) ;
548+ let ( tn, _) = import_member_names ( & wn, & self . colliding_import_names ) ;
549+ tn
444550 }
445551 /// Copy the state, changing it to emit into the helper module of
446552 /// the current trait
@@ -803,3 +909,221 @@ pub fn kebab_to_fn(n: &str) -> FnName {
803909 }
804910 FnName :: Plain ( kebab_to_snake ( n) )
805911}
912+
913+ #[ cfg( test) ]
914+ mod tests {
915+ use super :: * ;
916+ use crate :: etypes:: { ExternDecl , ExternDesc , Instance } ;
917+
918+ /// Helper to build a minimal `ExternDecl` whose desc is an Instance.
919+ fn instance_decl ( kebab_name : & str ) -> ExternDecl < ' _ > {
920+ ExternDecl {
921+ kebab_name,
922+ desc : ExternDesc :: Instance ( Instance {
923+ exports : Vec :: new ( ) ,
924+ } ) ,
925+ }
926+ }
927+
928+ /// Helper to build a minimal `ExternDecl` whose desc is a Func (not an Instance).
929+ fn func_decl ( kebab_name : & str ) -> ExternDecl < ' _ > {
930+ ExternDecl {
931+ kebab_name,
932+ desc : ExternDesc :: Func ( crate :: etypes:: Func {
933+ params : Vec :: new ( ) ,
934+ result : None ,
935+ } ) ,
936+ }
937+ }
938+
939+ // --- split_wit_name tests ---
940+
941+ #[ test]
942+ fn split_wit_name_simple ( ) {
943+ let wn = split_wit_name ( "my-interface" ) ;
944+ assert_eq ! ( wn. name, "my-interface" ) ;
945+ assert ! ( wn. namespaces. is_empty( ) ) ;
946+ }
947+
948+ #[ test]
949+ fn split_wit_name_with_package ( ) {
950+ let wn = split_wit_name ( "wasi:http/types" ) ;
951+ assert_eq ! ( wn. name, "types" ) ;
952+ assert_eq ! ( wn. namespaces, vec![ "wasi" , "http" ] ) ;
953+ }
954+
955+ #[ test]
956+ fn split_wit_name_with_version ( ) {
957+ let wn = split_wit_name ( "wasi:http/types@0.2.0" ) ;
958+ assert_eq ! ( wn. name, "types" ) ;
959+ assert_eq ! ( wn. namespaces, vec![ "wasi" , "http" ] ) ;
960+ }
961+
962+ #[ test]
963+ fn split_wit_name_nested_package ( ) {
964+ let wn = split_wit_name ( "wasi:filesystem/types" ) ;
965+ assert_eq ! ( wn. name, "types" ) ;
966+ assert_eq ! ( wn. namespaces, vec![ "wasi" , "filesystem" ] ) ;
967+ }
968+
969+ // --- find_colliding_import_names tests ---
970+
971+ #[ test]
972+ fn no_collisions_with_distinct_names ( ) {
973+ let imports = vec ! [
974+ instance_decl( "wasi:http/types" ) ,
975+ instance_decl( "wasi:filesystem/preopens" ) ,
976+ ] ;
977+ let collisions = find_colliding_import_names ( & imports) ;
978+ assert ! ( collisions. is_empty( ) ) ;
979+ }
980+
981+ #[ test]
982+ fn detects_collision_on_same_short_name ( ) {
983+ let imports = vec ! [
984+ instance_decl( "wasi:http/types" ) ,
985+ instance_decl( "wasi:filesystem/types" ) ,
986+ ] ;
987+ let collisions = find_colliding_import_names ( & imports) ;
988+ assert_eq ! ( collisions. len( ) , 1 ) ;
989+ assert ! ( collisions. contains( "types" ) ) ;
990+ }
991+
992+ #[ test]
993+ fn no_collision_for_non_instance_decls ( ) {
994+ let imports = vec ! [ instance_decl( "wasi:http/types" ) , func_decl( "types" ) ] ;
995+ let collisions = find_colliding_import_names ( & imports) ;
996+ assert ! ( collisions. is_empty( ) ) ;
997+ }
998+
999+ #[ test]
1000+ fn multiple_collisions ( ) {
1001+ let imports = vec ! [
1002+ instance_decl( "a:foo/types" ) ,
1003+ instance_decl( "b:bar/types" ) ,
1004+ instance_decl( "a:foo/handler" ) ,
1005+ instance_decl( "c:baz/handler" ) ,
1006+ ] ;
1007+ let collisions = find_colliding_import_names ( & imports) ;
1008+ assert_eq ! ( collisions. len( ) , 2 ) ;
1009+ assert ! ( collisions. contains( "types" ) ) ;
1010+ assert ! ( collisions. contains( "handler" ) ) ;
1011+ }
1012+
1013+ #[ test]
1014+ fn single_import_no_collision ( ) {
1015+ let imports = vec ! [ instance_decl( "wasi:http/types" ) ] ;
1016+ let collisions = find_colliding_import_names ( & imports) ;
1017+ assert ! ( collisions. is_empty( ) ) ;
1018+ }
1019+
1020+ #[ test]
1021+ fn empty_imports_no_collision ( ) {
1022+ let collisions = find_colliding_import_names ( & [ ] ) ;
1023+ assert ! ( collisions. is_empty( ) ) ;
1024+ }
1025+
1026+ // --- import_member_names tests ---
1027+
1028+ #[ test]
1029+ fn no_collision_uses_short_name ( ) {
1030+ let wn = split_wit_name ( "wasi:http/types" ) ;
1031+ let collisions = ImportNameCollisions :: default ( ) ;
1032+ let ( ty, getter) = import_member_names ( & wn, & collisions) ;
1033+ assert_eq ! ( ty. to_string( ) , "Types" ) ;
1034+ assert_eq ! ( getter. to_string( ) , "r#types" ) ;
1035+ }
1036+
1037+ #[ test]
1038+ fn collision_prepends_parent_namespace ( ) {
1039+ let wn = split_wit_name ( "wasi:http/types" ) ;
1040+ let collisions = find_colliding_import_names ( & [
1041+ instance_decl ( "wasi:http/types" ) ,
1042+ instance_decl ( "wasi:filesystem/types" ) ,
1043+ ] ) ;
1044+ let ( ty, getter) = import_member_names ( & wn, & collisions) ;
1045+ assert_eq ! ( ty. to_string( ) , "WasiHttpTypes" ) ;
1046+ assert_eq ! ( getter. to_string( ) , "r#wasi_http_types" ) ;
1047+ }
1048+
1049+ #[ test]
1050+ fn collision_different_parents_produce_different_names ( ) {
1051+ let collisions = find_colliding_import_names ( & [
1052+ instance_decl ( "wasi:http/types" ) ,
1053+ instance_decl ( "wasi:filesystem/types" ) ,
1054+ ] ) ;
1055+
1056+ let wn_http = split_wit_name ( "wasi:http/types" ) ;
1057+ let ( ty_http, getter_http) = import_member_names ( & wn_http, & collisions) ;
1058+
1059+ let wn_fs = split_wit_name ( "wasi:filesystem/types" ) ;
1060+ let ( ty_fs, getter_fs) = import_member_names ( & wn_fs, & collisions) ;
1061+
1062+ assert_eq ! ( ty_http. to_string( ) , "WasiHttpTypes" ) ;
1063+ assert_eq ! ( ty_fs. to_string( ) , "WasiFilesystemTypes" ) ;
1064+ assert_ne ! ( ty_http. to_string( ) , ty_fs. to_string( ) ) ;
1065+ assert_ne ! ( getter_http. to_string( ) , getter_fs. to_string( ) ) ;
1066+ }
1067+
1068+ #[ test]
1069+ fn collision_same_parent_different_package_produces_different_names ( ) {
1070+ let collisions = find_colliding_import_names ( & [
1071+ instance_decl ( "a:pkg/types" ) ,
1072+ instance_decl ( "b:pkg/types" ) ,
1073+ ] ) ;
1074+
1075+ let wn_a = split_wit_name ( "a:pkg/types" ) ;
1076+ let ( ty_a, _) = import_member_names ( & wn_a, & collisions) ;
1077+
1078+ let wn_b = split_wit_name ( "b:pkg/types" ) ;
1079+ let ( ty_b, _) = import_member_names ( & wn_b, & collisions) ;
1080+
1081+ assert_eq ! ( ty_a. to_string( ) , "APkgTypes" ) ;
1082+ assert_eq ! ( ty_b. to_string( ) , "BPkgTypes" ) ;
1083+ assert_ne ! ( ty_a. to_string( ) , ty_b. to_string( ) ) ;
1084+ }
1085+
1086+ #[ test]
1087+ fn collision_simple_name_uses_name_as_parent ( ) {
1088+ let wn = split_wit_name ( "types" ) ;
1089+ let collisions =
1090+ find_colliding_import_names ( & [ instance_decl ( "types" ) , instance_decl ( "pkg:types" ) ] ) ;
1091+ let ( ty, getter) = import_member_names ( & wn, & collisions) ;
1092+ // When there are no namespaces, the name itself is used as prefix
1093+ assert_eq ! ( ty. to_string( ) , "TypesTypes" ) ;
1094+ assert_eq ! ( getter. to_string( ) , "r#types_types" ) ;
1095+ }
1096+
1097+ #[ test]
1098+ fn versioned_collision_adds_version_after_namespace ( ) {
1099+ let collisions = find_colliding_import_names ( & [
1100+ instance_decl ( "a:pkg/types@1.0.0" ) ,
1101+ instance_decl ( "a:pkg/types@2.0.0" ) ,
1102+ ] ) ;
1103+
1104+ let wn_v1 = split_wit_name ( "a:pkg/types@1.0.0" ) ;
1105+ let ( ty_v1, getter_v1) = import_member_names ( & wn_v1, & collisions) ;
1106+
1107+ let wn_v2 = split_wit_name ( "a:pkg/types@2.0.0" ) ;
1108+ let ( ty_v2, getter_v2) = import_member_names ( & wn_v2, & collisions) ;
1109+
1110+ assert_eq ! ( ty_v1. to_string( ) , "APkgTypesV100" ) ;
1111+ assert_eq ! ( ty_v2. to_string( ) , "APkgTypesV200" ) ;
1112+ assert_eq ! ( getter_v1. to_string( ) , "r#a_pkg_types_v1_0_0" ) ;
1113+ assert_eq ! ( getter_v2. to_string( ) , "r#a_pkg_types_v2_0_0" ) ;
1114+ }
1115+
1116+ #[ test]
1117+ fn version_is_not_added_when_namespace_disambiguates ( ) {
1118+ let collisions = find_colliding_import_names ( & [
1119+ instance_decl ( "a:pkg/types@1.0.0" ) ,
1120+ instance_decl ( "b:pkg/types@1.0.0" ) ,
1121+ ] ) ;
1122+
1123+ let wn = split_wit_name ( "a:pkg/types@1.0.0" ) ;
1124+ let ( ty, getter) = import_member_names ( & wn, & collisions) ;
1125+
1126+ assert_eq ! ( ty. to_string( ) , "APkgTypes" ) ;
1127+ assert_eq ! ( getter. to_string( ) , "r#a_pkg_types" ) ;
1128+ }
1129+ }
0 commit comments