@@ -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 } ;
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+ /// 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+ }
0 commit comments