From 6cb3adf12ea0d0c001429e066e6473230cf7adea Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Wed, 12 Aug 2026 05:11:52 +0100 Subject: [PATCH] [hyperlight_component_util] Properly disambiguate chains of associated types Previously, the Rust component bindgen would in certain circumstances produce long chains of associated type references, e.g. Self::Foo::Bar::Baz::T. Unfortunately, Rust will not actually accept these without disambiguating syntax indicating from which trait the extern types should be drawn. This commit modifies the relevant logic in `emit_resource_ref` to walk through the path properly, and then deals with the consequences of that everywhere else. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_component_util/src/emit.rs | 31 ++++++- src/hyperlight_component_util/src/guest.rs | 35 ++++---- src/hyperlight_component_util/src/host.rs | 63 +++++++++++--- src/hyperlight_component_util/src/rtypes.rs | 96 ++++++++++----------- src/hyperlight_host/tests/wit_test.rs | 21 +++++ src/tests/rust_guests/witguest/src/main.rs | 23 +++++ 6 files changed, 192 insertions(+), 77 deletions(-) diff --git a/src/hyperlight_component_util/src/emit.rs b/src/hyperlight_component_util/src/emit.rs index 143eef91ff..1a4b7a9052 100644 --- a/src/hyperlight_component_util/src/emit.rs +++ b/src/hyperlight_component_util/src/emit.rs @@ -375,8 +375,35 @@ pub struct State<'a, 'b> { /// implements the imports of a component pub import_param_var: Option, /// The Rust type parameter used to represent the current Rust - /// state type - pub self_param_var: Option, + /// state type. In particular, this should let one find a `Self` + /// which Rust understands to be an impl of the instance trait + /// that `s.origin` refers to. + /// + /// # Note \[Origin paths and self parameters in impl codegen for higher-order components\] + /// + /// When extending impl (host.rs/guest.rs, as opposed to + /// rtypes.rs) codegen to higher-order components, it's not + /// entirely clear whether we should have this/the origin path + /// always refer to the true root component, or just to the + /// nearest component in which we are currently working. At first + /// glance, updating the origin path at all during impl codegen + /// seems like a bad idea, since the impl should be instantiating + /// the Rust tyvars and can't generate new ones for + /// non-locally-defined types the way that rtypes codegen + /// can. However, it may be the case that referring to a tyvar + /// that does not follow our local definedness rules at the + /// granularity of components will be impossible due to the + /// `outer_boundary` rules! If this does turn out to be the case, + /// then updating `origin` on every instance the way that rtypes + /// does will still be a bad idea, but we might want to consider + /// updating it once per /component/. + /// + /// Whether or not `origin` gets updated, the trait bounds in + /// `self_param_var` need to match this. Currently, code in + /// host.rs/guest.rs does not update `origin`, but does update + /// `self_param_var`, which will need to be fixed when extending + /// higher-order component bindings generation to impls. + pub self_param_var: Option, /// Whether we are emitting an implementation of the component /// interfaces, or just the types of the interface pub is_impl: bool, diff --git a/src/hyperlight_component_util/src/guest.rs b/src/hyperlight_component_util/src/guest.rs index 840db37c99..a56777d690 100644 --- a/src/hyperlight_component_util/src/guest.rs +++ b/src/hyperlight_component_util/src/guest.rs @@ -146,21 +146,15 @@ fn emit_import_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: & .map(|ed| emit_import_extern_decl(&mut s, ed)) .collect::>(); - let ns = wn.namespace_path(); - let nsi = wn.namespace_idents(); - let trait_name = kebab_to_type(wn.name); - let r#trait = s.r#trait(&nsi, trait_name.clone()); - let tvs = r#trait - .tvs - .iter() - .map(|(_, (tv, _))| tv.unwrap()) - .collect::>(); - let tvs = tvs + let trait_path = wn + .namespace_idents() .iter() - .map(|tv| rtypes::emit_var_ref(&mut s, &Tyvar::Bound(*tv))) + .chain(&[kebab_to_type(wn.name)]) + .cloned() .collect::>(); + let trait_ref = rtypes::trait_ref(&mut s, true, &trait_path); s.root_mod.items.extend(quote! { - impl #ns::#trait_name <#(#tvs),*> for Host { + impl #trait_ref for Host { #(#imports)* } }); @@ -285,8 +279,13 @@ fn emit_component<'a, 'b, 'c>( let r#trait = kebab_to_type(wn.name); let import_trait = kebab_to_imports_name(wn.name); 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")); + // We don't set s.self_param_var or s.import_param_var at all + // here, because they are currently obviated by the (s.is_guest && + // s.is_impl) hack in rtypes::emit_resource_ref. For when we + // eventually do: + // + // See Note [Origin paths and self parameters in impl codegen for higher-order components] + // in emit.rs s.colliding_import_names = find_colliding_import_names(&ct.imports); let rtsid = format_ident!("{}Resources", r#trait); @@ -313,8 +312,14 @@ fn emit_component<'a, 'b, 'c>( .iter() .map(|ed| emit_import_extern_decl(&mut s, ed)) .collect::>(); - s.var_offset = 0; + // We don't set s.self_param_var or s.import_param_var at all + // here, because it is currently obviated by the (s.is_guest && + // s.is_impl) hack in rtypes::emit_resource_ref. For when we + // eventually do: + // + // See Note [Origin paths and self parameters in impl codegen for higher-order components] + // in emit.rs s.is_export = true; s.cur_trait = Some(export_trait.clone()); diff --git a/src/hyperlight_component_util/src/host.rs b/src/hyperlight_component_util/src/host.rs index eb94c699e9..14a82fec78 100644 --- a/src/hyperlight_component_util/src/host.rs +++ b/src/hyperlight_component_util/src/host.rs @@ -15,7 +15,7 @@ limitations under the License. */ use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote}; +use quote::{ToTokens, format_ident, quote}; use crate::emit::{ FnName, ResourceItemName, State, WitName, find_colliding_import_names, import_member_names, @@ -153,18 +153,19 @@ fn emit_export_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: & #[derive(Clone)] struct SelfInfo { orig_id: Ident, - type_id: Vec, + /// identifier + trait bound + type_id: Vec<(Ident, TokenStream)>, outer_id: Ident, inner_preamble: TokenStream, inner_id: Ident, } impl SelfInfo { - fn new(orig_id: Ident) -> Self { + fn new(orig_id: Ident, imports_trait_bound: TokenStream) -> Self { let outer_id = format_ident!("captured_{}", orig_id); let inner_id = format_ident!("slf"); SelfInfo { orig_id, - type_id: vec![format_ident!("I")], + type_id: vec![(format_ident!("I"), imports_trait_bound)], inner_preamble: quote! { let mut #inner_id = #outer_id.lock().unwrap(); let mut #inner_id = ::std::ops::DerefMut::deref_mut(&mut #inner_id); @@ -173,16 +174,37 @@ impl SelfInfo { inner_id, } } + fn type_inst(&self) -> TokenStream { + self.type_id[1..] + .iter() + .fold( + ( + self.type_id[0].0.to_token_stream() as TokenStream, + &self.type_id[0].1, + ), + |(toks, last_bound), (tid, next_bound)| { + (quote! { <#toks as #last_bound>::#tid }, next_bound) + }, + ) + .0 + } /// Adjust a [`SelfInfo`] to get the portion of the state for the /// current instance via calling the given getter - fn with_getter(&self, tp: TokenStream, type_name: Ident, getter: Ident) -> Self { + fn with_getter( + &self, + tp: TokenStream, + type_name: Ident, + type_bound: TokenStream, + getter: Ident, + ) -> Self { let mut toks = self.inner_preamble.clone(); let id = self.inner_id.clone(); - let mut type_id = self.type_id.clone(); + let type_inst = self.type_inst(); toks.extend(quote! { - let mut #id = #tp::#getter(::std::borrow::BorrowMut::<#(#type_id)::*>::borrow_mut(&mut #id)); + let mut #id = #tp::#getter(::std::borrow::BorrowMut::<#type_inst>::borrow_mut(&mut #id)); }); - type_id.push(type_name); + let mut type_id = self.type_id.clone(); + type_id.push((type_name, type_bound)); SelfInfo { orig_id: self.orig_id.clone(), type_id, @@ -232,12 +254,13 @@ fn emit_import_extern_decl<'a, 'b, 'c>( } } }; + let type_inst = get_self.type_inst(); let SelfInfo { orig_id, - type_id, outer_id, inner_preamble, inner_id, + .. } = get_self; let ret = format_ident!("ret"); let marshal_result = emit_hl_marshal_result(s, ret.clone(), &ft.result); @@ -248,7 +271,7 @@ fn emit_import_extern_decl<'a, 'b, 'c>( let mut rts = captured_rts.lock().unwrap(); #inner_preamble let #ret = #callname( - ::std::borrow::BorrowMut::<#(#type_id)::*>::borrow_mut( + ::std::borrow::BorrowMut::<#type_inst>::borrow_mut( &mut #inner_id ), #(#pus),* @@ -267,7 +290,14 @@ fn emit_import_extern_decl<'a, 'b, 'c>( let wn = split_wit_name(ed.kebab_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); + let trait_path = wn + .namespace_idents() + .iter() + .chain(&[kebab_to_type(wn.name)]) + .cloned() + .collect::>(); + let trait_ref = rtypes::trait_ref(&mut s, true, &trait_path); + let get_self = get_self.with_getter(tp, type_name, trait_ref, getter); emit_import_instance(&mut s, get_self, wn.clone(), it) } ExternDesc::Component(_) => { @@ -340,13 +370,22 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com let imports = ct .imports .iter() - .map(|ed| emit_import_extern_decl(&mut s, SelfInfo::new(import_id.clone()), ed)) + .map(|ed| { + emit_import_extern_decl( + &mut s, + SelfInfo::new(import_id.clone(), quote! { #ns::#import_trait }), + ed, + ) + }) .collect::>(); s.var_offset = 0; s.root_component_name = Some((ns.clone(), wn.name)); s.cur_trait = Some(export_trait.clone()); s.import_param_var = Some(format_ident!("I")); + // See Note [Origin paths and self parameters in impl codegen for higher-order components] + // in emit.rs + s.self_param_var = Some(quote! { > }); s.is_export = true; let exports = ct diff --git a/src/hyperlight_component_util/src/rtypes.rs b/src/hyperlight_component_util/src/rtypes.rs index ffa5041c1e..0c50d89353 100644 --- a/src/hyperlight_component_util/src/rtypes.rs +++ b/src/hyperlight_component_util/src/rtypes.rs @@ -39,7 +39,7 @@ use crate::etypes::{ fn emit_tvis(s: &mut State, tvs: Vec) -> TokenStream { let tvs = tvs .iter() - .map(|tv| emit_var_ref(s, &Tyvar::Bound(*tv))) + .map(|tv| emit_var_ref_noff(s, *tv, false)) .collect::>(); if !tvs.is_empty() { quote! { <#(#tvs),*> } @@ -48,6 +48,14 @@ fn emit_tvis(s: &mut State, tvs: Vec) -> TokenStream { } } +/// Construct a token stream referencing a trait at a given trait path +pub(crate) fn trait_ref(s: &mut State, absolute: bool, path: &[Ident]) -> TokenStream { + let rp = s.root_path(); + let t = s.resolve_trait_immut(absolute, path); + let tvis = emit_tvis(s, t.tv_idxs()); + quote! { #rp #(#path)::* #tvis } +} + /// Emit a token stream that references the type of a particular resource /// /// - `n`: the absolute index (i.e. ignoring [`State::var_offset`]) of @@ -90,55 +98,47 @@ fn emit_resource_ref(s: &mut State, n: u32, path: Vec) -> TokenStr // Generally speaking, the structure that we expect to see in // `path` ends in an instance that exports the resource type, - // followed by the resource type itself. We locate the resource - // trait by using that final instance name directly; any other - // names are just used to get to the type that implements it - let instance = &path[path.len() - 2]; - let iwn = split_wit_name(instance.name()); - let extras = path[0..path.len() - 2] - .iter() - .map(|p| { - let wn = split_wit_name(p.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); - // Use the disambiguated trait member only for imported instances. Exported - // instances must keep their public WIT member names. - let instance_type = if instance.imported() { - let (tn, _) = import_member_names(&iwn, &s.colliding_import_names); - tn - } else { - kebab_to_type(iwn.name) - }; - let mut sv = quote! { Self }; - if instance.imported() { + // followed by the resource type itself. + + let mut toks = quote! { Self }; + if path[0].imported() { if let Some(iv) = &s.import_param_var { - sv = quote! { #iv } - }; + toks = quote! { #iv } + } } else if let Some(s) = &s.self_param_var { - sv = quote! { #s } - }; - let mut trait_path = Vec::new(); - trait_path.extend(iwn.namespace_idents()); - trait_path.push(instance_mod.clone()); - trait_path.push(rtrait.clone()); - let t = s.resolve_trait_immut(true, &trait_path); - let tvis = emit_tvis(s, t.tv_idxs()); - 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 } + toks = quote! { #s } + } + // todo:this will need a bit of adjustment to work well with + // plainname externs, which may require keeping track of the last + // interfacename we saw + for (i, p) in path[0..path.len() - 1].iter().enumerate() { + let iwn = split_wit_name(p.name()); + let export_name = if p.imported() { + import_member_names(&iwn, &s.colliding_import_names).0 + } else { + kebab_to_type(iwn.name) + }; + + // The final item in the path will be referring to the + // relevant resource trait, while the others will be referring + // instance traits on the way there. So, we need to treat them + // separately. + let namespace_suffix = if i == path.len() - 2 { + &[kebab_to_namespace(iwn.name), rtrait.clone()] as &[Ident] + } else { + &[kebab_to_type(iwn.name)] as &[Ident] + }; + let trait_path = iwn + .namespace_idents() + .iter() + .chain(namespace_suffix.iter()) + .cloned() + .collect::>(); + let trait_ref = trait_ref(s, true, &trait_path); + + toks = quote! { <#toks::#export_name as #trait_ref> }; + } + quote! { #toks::T } } /// Try to find a way to refer to the given type variable from the diff --git a/src/hyperlight_host/tests/wit_test.rs b/src/hyperlight_host/tests/wit_test.rs index df33425685..ef3f8ff1cc 100644 --- a/src/hyperlight_host/tests/wit_test.rs +++ b/src/hyperlight_host/tests/wit_test.rs @@ -571,3 +571,24 @@ mod inline_bindgen_test { assert_eq!(result.value, "inline"); } } + +mod deeply_nested { + hyperlight_component_macro::host_bindgen!({ + inline: r#" + (component + (type (export "world") (component + (export "test:deeply-nested/world" (component + (import "test:deeply-nested/int1" (instance $I1 + (export "test:deeply-nested/int2" (instance + (export "r" (type $R (sub resource))) + (export "f" (func (param "r" (own $R)))))))) + (alias export $I1 "test:deeply-nested/int2" (instance $I2)) + (alias export $I2 "r" (type $R)) + (import "test:deeply-nested/int3" (instance + (export "test:deeply-nested/int4" (instance + (export "r4" (type $R4 (eq $R))) + (export "g" (func (param "r4" (own $R4)))))))) + (export "h" (func (param "r" (own $R))))))))) + "#, + }); +} diff --git a/src/tests/rust_guests/witguest/src/main.rs b/src/tests/rust_guests/witguest/src/main.rs index 307d9cca62..8fcf5a4e57 100644 --- a/src/tests/rust_guests/witguest/src/main.rs +++ b/src/tests/rust_guests/witguest/src/main.rs @@ -243,3 +243,26 @@ pub fn guest_dispatch_function(function_call: FunctionCall) -> Result> { function_call.function_name.clone(), )) } + +// Tests that bindgen at least compiles for some interesting cases +// that may not yet be expressible in the WIT syntax +mod deeply_nested { + hyperlight_component_macro::guest_bindgen!({ + inline: r#" + (component + (type (export "world") (component + (export "test:deeply-nested/world" (component + (import "test:deeply-nested/int1" (instance $I1 + (export "test:deeply-nested/int2" (instance + (export "r" (type $R (sub resource))) + (export "f" (func (param "r" (own $R)))))))) + (alias export $I1 "test:deeply-nested/int2" (instance $I2)) + (alias export $I2 "r" (type $R)) + (import "test:deeply-nested/int3" (instance + (export "test:deeply-nested/int4" (instance + (export "r4" (type $R4 (eq $R))) + (export "g" (func (param "r4" (own $R4)))))))) + (export "h" (func (param "r" (own $R))))))))) + "#, + }); +}