Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/dep_build_guests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,12 @@ jobs:
path: src/tests/rust_guests/witguest/twoworlds.wasm
retention-days: 1
if-no-files-found: error

- name: Upload collision.wasm
if: inputs.config == 'debug'
uses: actions/upload-artifact@v7
with:
name: collision-wasm
path: src/tests/rust_guests/witguest/collision.wasm
retention-days: 1
if-no-files-found: error
6 changes: 6 additions & 0 deletions .github/workflows/dep_build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ jobs:
name: twoworlds-wasm
path: src/tests/rust_guests/witguest/

- name: Download collision.wasm
uses: actions/download-artifact@v8
with:
name: collision-wasm
path: src/tests/rust_guests/witguest/

- name: Build
run: just build ${{ inputs.config }}

Expand Down
1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ witguest-wit:
{{ if os() == "windows" { "if (-not (Get-Command wasm-tools -ErrorAction SilentlyContinue)) { cargo install --locked wasm-tools }" } else { "command -v wasm-tools >/dev/null 2>&1 || cargo install --locked wasm-tools" } }}
cd src/tests/rust_guests/witguest && wasm-tools component wit guest.wit -w -o interface.wasm
cd src/tests/rust_guests/witguest && wasm-tools component wit two_worlds.wit -w -o twoworlds.wasm
cd src/tests/rust_guests/witguest && wasm-tools component wit collision-test/ -w -o collision.wasm

build-rust-guests target=default-target features="": (witguest-wit) (ensure-cargo-hyperlight)
cd src/tests/rust_guests/simpleguest && cargo hyperlight build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }}
Expand Down
233 changes: 229 additions & 4 deletions src/hyperlight_component_util/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,51 @@ limitations under the License.
*/

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

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::Ident;

use crate::etypes::{BoundedTyvar, Defined, Handleable, ImportExport, TypeBound, Tyvar};
use crate::etypes::{
BoundedTyvar, Defined, ExternDecl, ExternDesc, Handleable, ImportExport, TypeBound, Tyvar,
};

/// Scan a list of import extern decls for interface name collisions.
/// Returns the set of interface names that appear more than once.
pub fn find_colliding_import_names(imports: &[ExternDecl]) -> HashSet<String> {
let mut counts = std::collections::HashMap::<String, usize>::new();
for ed in imports {
if let ExternDesc::Instance(_) = &ed.desc {
let wn = split_wit_name(ed.kebab_name);
*counts.entry(wn.name.to_string()).or_default() += 1;
}
}
counts
.into_iter()
.filter(|(_, c)| *c > 1)
.map(|(n, _)| n)
.collect()
}

/// Get the disambiguated type and getter names for an import instance.
/// If the interface name collides with another import, prepend the full
/// kebab-joined namespace path to disambiguate
/// (e.g. "types" from "wasi:http" becomes "WasiHttpTypes"/"wasi_http_types").
pub fn import_member_names(wn: &WitName, collisions: &HashSet<String>) -> (Ident, Ident) {
if collisions.contains(wn.name) {
let prefix = if wn.namespaces.is_empty() {
wn.name.to_string()
} else {
wn.namespaces.join("-")
};
let qualified = format!("{}-{}", prefix, wn.name);
(kebab_to_type(&qualified), kebab_to_getter(&qualified))
} else {
(kebab_to_type(wn.name), kebab_to_getter(wn.name))
}
}

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

/// Create a State with all of its &mut references pointing to
Expand Down Expand Up @@ -336,6 +378,7 @@ impl<'a, 'b> State<'a, 'b> {
is_guest,
is_wasmtime_guest,
is_export: false,
colliding_import_names: HashSet::new(),
}
}
pub fn clone<'c>(&'c mut self) -> State<'c, 'b> {
Expand All @@ -357,6 +400,7 @@ impl<'a, 'b> State<'a, 'b> {
is_guest: self.is_guest,
is_wasmtime_guest: self.is_wasmtime_guest,
is_export: self.is_export,
colliding_import_names: self.colliding_import_names.clone(),
}
}
/// Obtain a reference to the [`Mod`] that we are currently
Expand Down Expand Up @@ -437,10 +481,12 @@ impl<'a, 'b> State<'a, 'b> {
/// variable, given its absolute index (i.e. ignoring
/// [`State::var_offset`])
pub fn noff_var_id(&self, n: u32) -> Ident {
let Some(n) = self.bound_vars[n as usize].origin.last_name() else {
let Some(name) = self.bound_vars[n as usize].origin.last_name() else {
panic!("missing origin on tyvar in rust emit")
};
kebab_to_type(n)
let wn = split_wit_name(name);
let (tn, _) = import_member_names(&wn, &self.colliding_import_names);
tn
}
/// Copy the state, changing it to emit into the helper module of
/// the current trait
Expand Down Expand Up @@ -803,3 +849,182 @@ pub fn kebab_to_fn(n: &str) -> FnName {
}
FnName::Plain(kebab_to_snake(n))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::etypes::{ExternDecl, ExternDesc, Instance};

/// Helper to build a minimal `ExternDecl` whose desc is an Instance.
fn instance_decl(kebab_name: &str) -> ExternDecl<'_> {
ExternDecl {
kebab_name,
desc: ExternDesc::Instance(Instance {
exports: Vec::new(),
}),
}
}

/// Helper to build a minimal `ExternDecl` whose desc is a Func (not an Instance).
fn func_decl(kebab_name: &str) -> ExternDecl<'_> {
ExternDecl {
kebab_name,
desc: ExternDesc::Func(crate::etypes::Func {
params: Vec::new(),
result: None,
}),
}
}

// --- split_wit_name tests ---

#[test]
fn split_wit_name_simple() {
let wn = split_wit_name("my-interface");
assert_eq!(wn.name, "my-interface");
assert!(wn.namespaces.is_empty());
}

#[test]
fn split_wit_name_with_package() {
let wn = split_wit_name("wasi:http/types");
assert_eq!(wn.name, "types");
assert_eq!(wn.namespaces, vec!["wasi", "http"]);
}

#[test]
fn split_wit_name_with_version() {
let wn = split_wit_name("wasi:http/types@0.2.0");
assert_eq!(wn.name, "types");
assert_eq!(wn.namespaces, vec!["wasi", "http"]);
}

#[test]
fn split_wit_name_nested_package() {
let wn = split_wit_name("wasi:filesystem/types");
assert_eq!(wn.name, "types");
assert_eq!(wn.namespaces, vec!["wasi", "filesystem"]);
}

// --- find_colliding_import_names tests ---

#[test]
fn no_collisions_with_distinct_names() {
let imports = vec![
instance_decl("wasi:http/types"),
instance_decl("wasi:filesystem/preopens"),
];
let collisions = find_colliding_import_names(&imports);
assert!(collisions.is_empty());
}

#[test]
fn detects_collision_on_same_short_name() {
let imports = vec![
instance_decl("wasi:http/types"),
instance_decl("wasi:filesystem/types"),
];
let collisions = find_colliding_import_names(&imports);
assert_eq!(collisions.len(), 1);
assert!(collisions.contains("types"));
}

#[test]
fn no_collision_for_non_instance_decls() {
let imports = vec![instance_decl("wasi:http/types"), func_decl("types")];
let collisions = find_colliding_import_names(&imports);
assert!(collisions.is_empty());
}

#[test]
fn multiple_collisions() {
let imports = vec![
instance_decl("a:foo/types"),
instance_decl("b:bar/types"),
instance_decl("a:foo/handler"),
instance_decl("c:baz/handler"),
];
let collisions = find_colliding_import_names(&imports);
assert_eq!(collisions.len(), 2);
assert!(collisions.contains("types"));
assert!(collisions.contains("handler"));
}

#[test]
fn single_import_no_collision() {
let imports = vec![instance_decl("wasi:http/types")];
let collisions = find_colliding_import_names(&imports);
assert!(collisions.is_empty());
}

#[test]
fn empty_imports_no_collision() {
let collisions = find_colliding_import_names(&[]);
assert!(collisions.is_empty());
}

// --- import_member_names tests ---

#[test]
fn no_collision_uses_short_name() {
let wn = split_wit_name("wasi:http/types");
let collisions = HashSet::new();
let (ty, getter) = import_member_names(&wn, &collisions);
assert_eq!(ty.to_string(), "Types");
assert_eq!(getter.to_string(), "r#types");
}

#[test]
fn collision_prepends_parent_namespace() {
let wn = split_wit_name("wasi:http/types");
let mut collisions = HashSet::new();
collisions.insert("types".to_string());
let (ty, getter) = import_member_names(&wn, &collisions);
assert_eq!(ty.to_string(), "WasiHttpTypes");
assert_eq!(getter.to_string(), "r#wasi_http_types");
}

#[test]
fn collision_different_parents_produce_different_names() {
let mut collisions = HashSet::new();
collisions.insert("types".to_string());

let wn_http = split_wit_name("wasi:http/types");
let (ty_http, getter_http) = import_member_names(&wn_http, &collisions);

let wn_fs = split_wit_name("wasi:filesystem/types");
let (ty_fs, getter_fs) = import_member_names(&wn_fs, &collisions);

assert_eq!(ty_http.to_string(), "WasiHttpTypes");
assert_eq!(ty_fs.to_string(), "WasiFilesystemTypes");
assert_ne!(ty_http.to_string(), ty_fs.to_string());
assert_ne!(getter_http.to_string(), getter_fs.to_string());
}

#[test]
fn collision_same_parent_different_package_produces_different_names() {
let mut collisions = HashSet::new();
collisions.insert("types".to_string());

let wn_a = split_wit_name("a:pkg/types");
let (ty_a, _) = import_member_names(&wn_a, &collisions);

let wn_b = split_wit_name("b:pkg/types");
let (ty_b, _) = import_member_names(&wn_b, &collisions);

assert_eq!(ty_a.to_string(), "APkgTypes");
assert_eq!(ty_b.to_string(), "BPkgTypes");
assert_ne!(ty_a.to_string(), ty_b.to_string());
}

#[test]
fn collision_simple_name_uses_name_as_parent() {
let wn = split_wit_name("types");
let mut collisions = HashSet::new();
collisions.insert("types".to_string());
let (ty, getter) = import_member_names(&wn, &collisions);
// When there are no namespaces, the name itself is used as prefix
assert_eq!(ty.to_string(), "TypesTypes");
assert_eq!(getter.to_string(), "r#types_types");
}
}
10 changes: 5 additions & 5 deletions src/hyperlight_component_util/src/guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use crate::emit::{
FnName, ResolvedBoundVar, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn,
kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var,
split_wit_name,
FnName, ResolvedBoundVar, ResourceItemName, State, WitName, find_colliding_import_names,
import_member_names, kebab_to_exports_name, kebab_to_fn, kebab_to_getter,
kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name,
};
use crate::etypes::{Component, Defined, ExternDecl, ExternDesc, Handleable, Instance, Tyvar};
use crate::hl::{
Expand Down Expand Up @@ -117,8 +117,7 @@ fn emit_import_extern_decl<'a, 'b, 'c>(
let wn = split_wit_name(ed.kebab_name);
emit_import_instance(s, wn.clone(), it);

let getter = kebab_to_getter(wn.name);
let tn = kebab_to_type(wn.name);
let (tn, getter) = import_member_names(&wn, &s.colliding_import_names);
quote! {
type #tn = Self;
#[allow(refining_impl_trait)]
Expand Down Expand Up @@ -287,6 +286,7 @@ fn emit_component<'a, 'b, 'c>(
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"));
s.colliding_import_names = find_colliding_import_names(&ct.imports);

let rtsid = format_ident!("{}Resources", r#trait);
resource::emit_tables(
Expand Down
18 changes: 12 additions & 6 deletions src/hyperlight_component_util/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

use crate::emit::{
FnName, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, kebab_to_getter,
kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name,
FnName, ResourceItemName, State, WitName, find_colliding_import_names, import_member_names,
kebab_to_exports_name, kebab_to_fn, kebab_to_getter, kebab_to_imports_name, kebab_to_namespace,
kebab_to_type, kebab_to_var, split_wit_name,
};
use crate::etypes::{Component, ExternDecl, ExternDesc, Instance, Tyvar};
use crate::hl::{
Expand Down Expand Up @@ -135,8 +136,13 @@ fn emit_export_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: &
let (root_ns, root_base_name) = s.root_component_name.unwrap();
let wrapper_name = kebab_to_wrapper_name(root_base_name);
let imports_name = kebab_to_imports_name(root_base_name);
let trait_path = if ns.is_empty() {
quote! { #trait_name }
} else {
quote! { #ns::#trait_name }
};
s.root_mod.items.extend(quote! {
impl<I: #root_ns::#imports_name, S: ::hyperlight_host::sandbox::Callable> #ns::#trait_name <#(#tvs),*> for #wrapper_name<I, S> {
impl<I: #root_ns::#imports_name, S: ::hyperlight_host::sandbox::Callable> #trait_path <#(#tvs),*> for #wrapper_name<I, S> {
#(#exports)*
}
});
Expand Down Expand Up @@ -259,10 +265,9 @@ fn emit_import_extern_decl<'a, 'b, 'c>(
ExternDesc::Instance(it) => {
let mut s = s.clone();
let wn = split_wit_name(ed.kebab_name);
let type_name = kebab_to_type(wn.name);
let getter = kebab_to_getter(wn.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); //quote! { #get_self let mut slf = &mut #tp::#getter(&mut *slf); };
let get_self = get_self.with_getter(tp, type_name, getter);
emit_import_instance(&mut s, get_self, wn.clone(), it)
}
ExternDesc::Component(_) => {
Expand Down Expand Up @@ -321,6 +326,7 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com

let rtsid = format_ident!("{}Resources", r#trait);
s.import_param_var = Some(format_ident!("I"));
s.colliding_import_names = find_colliding_import_names(&ct.imports);
resource::emit_tables(
&mut s,
rtsid.clone(),
Expand Down
Loading
Loading