Skip to content

Commit a9ab9e4

Browse files
committed
fix: handle versioned import name collisions
Signed-off-by: James Sturtevant <jsturtevant@gmail.com>
1 parent 0a4d8b1 commit a9ab9e4

1 file changed

Lines changed: 119 additions & 20 deletions

File tree

  • src/hyperlight_component_util/src

src/hyperlight_component_util/src/emit.rs

Lines changed: 119 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

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

2121
use proc_macro2::TokenStream;
@@ -26,35 +26,95 @@ use crate::etypes::{
2626
BoundedTyvar, Defined, ExternDecl, ExternDesc, Handleable, ImportExport, TypeBound, Tyvar,
2727
};
2828

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+
2971
/// 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();
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();
3377
for ed in imports {
3478
if let ExternDesc::Instance(_) = &ed.desc {
3579
let wn = split_wit_name(ed.kebab_name);
36-
*counts.entry(wn.name.to_string()).or_default() += 1;
80+
*short_counts.entry(wn.name.to_string()).or_default() += 1;
81+
*qualified_counts
82+
.entry(qualified_import_key(&wn))
83+
.or_default() += 1;
3784
}
3885
}
39-
counts
86+
let short_names = short_counts
4087
.into_iter()
4188
.filter(|(_, c)| *c > 1)
4289
.map(|(n, _)| n)
43-
.collect()
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+
}
44100
}
45101

46102
/// Get the disambiguated type and getter names for an import instance.
47103
/// If the interface name collides with another import, prepend the full
48104
/// kebab-joined namespace path to disambiguate
49105
/// (e.g. "types" from "wasi:http" becomes "WasiHttpTypes"/"wasi_http_types").
50-
pub fn import_member_names(wn: &WitName, collisions: &HashSet<String>) -> (Ident, Ident) {
106+
pub fn import_member_names(wn: &WitName, collisions: &ImportNameCollisions) -> (Ident, Ident) {
51107
if collisions.contains(wn.name) {
52108
let prefix = if wn.namespaces.is_empty() {
53109
wn.name.to_string()
54110
} else {
55111
wn.namespaces.join("-")
56112
};
57-
let qualified = format!("{}-{}", prefix, wn.name);
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+
}
58118
(kebab_to_type(&qualified), kebab_to_getter(&qualified))
59119
} else {
60120
(kebab_to_type(wn.name), kebab_to_getter(wn.name))
@@ -325,7 +385,7 @@ pub struct State<'a, 'b> {
325385
/// (e.g. "types" appears in both wasi:filesystem/types and wasi:http/types).
326386
/// When a name is in this set, the parent namespace is prepended to
327387
/// disambiguate the trait member name.
328-
pub colliding_import_names: HashSet<String>,
388+
pub colliding_import_names: ImportNameCollisions,
329389
}
330390

331391
/// Create a State with all of its &mut references pointing to
@@ -378,7 +438,7 @@ impl<'a, 'b> State<'a, 'b> {
378438
is_guest,
379439
is_wasmtime_guest,
380440
is_export: false,
381-
colliding_import_names: HashSet::new(),
441+
colliding_import_names: ImportNameCollisions::default(),
382442
}
383443
}
384444
pub fn clone<'c>(&'c mut self) -> State<'c, 'b> {
@@ -968,7 +1028,7 @@ mod tests {
9681028
#[test]
9691029
fn no_collision_uses_short_name() {
9701030
let wn = split_wit_name("wasi:http/types");
971-
let collisions = HashSet::new();
1031+
let collisions = ImportNameCollisions::default();
9721032
let (ty, getter) = import_member_names(&wn, &collisions);
9731033
assert_eq!(ty.to_string(), "Types");
9741034
assert_eq!(getter.to_string(), "r#types");
@@ -977,17 +1037,21 @@ mod tests {
9771037
#[test]
9781038
fn collision_prepends_parent_namespace() {
9791039
let wn = split_wit_name("wasi:http/types");
980-
let mut collisions = HashSet::new();
981-
collisions.insert("types".to_string());
1040+
let collisions = find_colliding_import_names(&[
1041+
instance_decl("wasi:http/types"),
1042+
instance_decl("wasi:filesystem/types"),
1043+
]);
9821044
let (ty, getter) = import_member_names(&wn, &collisions);
9831045
assert_eq!(ty.to_string(), "WasiHttpTypes");
9841046
assert_eq!(getter.to_string(), "r#wasi_http_types");
9851047
}
9861048

9871049
#[test]
9881050
fn collision_different_parents_produce_different_names() {
989-
let mut collisions = HashSet::new();
990-
collisions.insert("types".to_string());
1051+
let collisions = find_colliding_import_names(&[
1052+
instance_decl("wasi:http/types"),
1053+
instance_decl("wasi:filesystem/types"),
1054+
]);
9911055

9921056
let wn_http = split_wit_name("wasi:http/types");
9931057
let (ty_http, getter_http) = import_member_names(&wn_http, &collisions);
@@ -1003,8 +1067,10 @@ mod tests {
10031067

10041068
#[test]
10051069
fn collision_same_parent_different_package_produces_different_names() {
1006-
let mut collisions = HashSet::new();
1007-
collisions.insert("types".to_string());
1070+
let collisions = find_colliding_import_names(&[
1071+
instance_decl("a:pkg/types"),
1072+
instance_decl("b:pkg/types"),
1073+
]);
10081074

10091075
let wn_a = split_wit_name("a:pkg/types");
10101076
let (ty_a, _) = import_member_names(&wn_a, &collisions);
@@ -1020,11 +1086,44 @@ mod tests {
10201086
#[test]
10211087
fn collision_simple_name_uses_name_as_parent() {
10221088
let wn = split_wit_name("types");
1023-
let mut collisions = HashSet::new();
1024-
collisions.insert("types".to_string());
1089+
let collisions =
1090+
find_colliding_import_names(&[instance_decl("types"), instance_decl("pkg:types")]);
10251091
let (ty, getter) = import_member_names(&wn, &collisions);
10261092
// When there are no namespaces, the name itself is used as prefix
10271093
assert_eq!(ty.to_string(), "TypesTypes");
10281094
assert_eq!(getter.to_string(), "r#types_types");
10291095
}
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+
}
10301129
}

0 commit comments

Comments
 (0)