Skip to content
Open
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
131 changes: 129 additions & 2 deletions crates/rust/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -655,12 +655,46 @@ macro_rules! {macro_name} {{
}
}

// `use` statements to bring every named type reachable from the
// payload into scope of the generated `vtable{ordinal}` module.
//
// Without these, a type like `future<result<R, string>>` where
// `R` is brought in via `use t.{r}` at world level emits
// `-> Result<R, super::super::_rt::String>` inside `vtable1`,
// with `R` unresolved: the composite type is anonymous so no
// owner-qualified path is inserted, and the world-level alias
// `pub type R = ...;` lives two modules up. Importing it here
// makes the bare identifier resolve regardless of how deeply
// nested the payload type is.
let mut type_imports = BTreeSet::new();
if let Some(ty) = payload_type {
collect_named_type_ids(self.resolve, ty, &mut type_imports);
}
let type_imports: String = type_imports
.into_iter()
.map(|id| {
let path = self.type_path(id, /*owned=*/ true);
// `type_path` emits the full `super::super::...::Name`
// when the type is owned by an interface, but falls
// back to a bare name when the owner is a world (the
// `use interface.{T}` alias case). Bare names need the
// `super::super::` prefix to reach the world-level
// `pub type T = ...;` alias at macro root.
let path = if path.starts_with("super::") {
path
} else {
format!("super::super::{path}")
};
format!(" use {path};\n")
})
.collect();

let code = format!(
r#"
#[doc(hidden)]
#[allow(unused_unsafe)]
#[allow(unused_unsafe, unused_imports)]
pub mod vtable{ordinal} {{

{type_imports}
#[cfg(not(target_arch = "wasm32"))]
unsafe extern "C" fn cancel_write(_: u32) -> u32 {{ unreachable!() }}
#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -3157,3 +3191,96 @@ impl<'a, 'b> wit_bindgen_core::AnonymousTypeGenerator<'a> for AnonTypeGenerator<
self.interface.push_str(&format!("; {size}]"));
}
}

/// Walk `ty`'s type tree and insert every named type id into `out`.
///
/// A type id is "named" if `resolve.types[id].name` is set, i.e. it is
/// a user-defined type (record, variant, enum, flags, resource, type
/// alias) as opposed to an anonymous structural one (`result<_, _>`,
/// `tuple<_, _>`, `list<_>`, etc.). We still recurse through the
/// anonymous composites to collect their inner named types — that's
/// the whole reason this exists: `future<result<R, _>>` hides `R` one
/// level below an anonymous `result` that the emitter never flags.
///
/// Used by stream/future payload emission to pre-compute `use`
/// statements for every named type the emitted `vtable{ordinal}`
/// module will reference, so bare identifiers inside the `fn lift` /
/// `fn lower` / `impl StreamPayload` bodies resolve even when the
/// type lives at macro-root via a world-level `use` alias.
fn collect_named_type_ids(resolve: &Resolve, ty: &Type, out: &mut BTreeSet<TypeId>) {
let Type::Id(id) = ty else { return };
collect_named_type_ids_from_id(resolve, *id, out);
}

fn collect_named_type_ids_from_id(resolve: &Resolve, id: TypeId, out: &mut BTreeSet<TypeId>) {
// Avoid cycles: a named type we've already queued doesn't need a
// second walk through its structure.
if out.contains(&id) {
return;
}
let ty = &resolve.types[id];
let named = ty.name.is_some();
if named {
out.insert(id);
// A named alias (e.g. `use t.{r};` or `type x = r;`) is what
// the emitter references verbatim — its target is a separate
// type def reachable by its own name. Walking through the
// alias would import both the alias and its target under the
// same Rust identifier and trip E0252. Stop at the alias.
if matches!(ty.kind, TypeDefKind::Type(_)) {
return;
}
}
match &ty.kind {
TypeDefKind::Record(r) => {
for field in &r.fields {
collect_named_type_ids(resolve, &field.ty, out);
}
}
TypeDefKind::Variant(v) => {
for case in &v.cases {
if let Some(ty) = &case.ty {
collect_named_type_ids(resolve, ty, out);
}
}
}
TypeDefKind::Tuple(t) => {
for ty in &t.types {
collect_named_type_ids(resolve, ty, out);
}
}
TypeDefKind::Option(t)
| TypeDefKind::List(t)
| TypeDefKind::FixedLengthList(t, _)
| TypeDefKind::Type(t) => {
collect_named_type_ids(resolve, t, out);
}
TypeDefKind::Result(r) => {
if let Some(ty) = &r.ok {
collect_named_type_ids(resolve, ty, out);
}
if let Some(ty) = &r.err {
collect_named_type_ids(resolve, ty, out);
}
}
TypeDefKind::Map(k, v) => {
collect_named_type_ids(resolve, k, out);
collect_named_type_ids(resolve, v, out);
}
TypeDefKind::Future(inner) | TypeDefKind::Stream(inner) => {
if let Some(ty) = inner {
collect_named_type_ids(resolve, ty, out);
}
}
TypeDefKind::Handle(h) => {
let resource_id = match h {
Handle::Own(id) | Handle::Borrow(id) => *id,
};
collect_named_type_ids_from_id(resolve, resource_id, out);
}
TypeDefKind::Enum(_)
| TypeDefKind::Flags(_)
| TypeDefKind::Resource
| TypeDefKind::Unknown => {}
}
}
26 changes: 26 additions & 0 deletions crates/rust/tests/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,29 @@ mod method_chaining {
enable_method_chaining: true
});
}

// Regression: a named type brought in via `use interface.{T}` at world
// level, embedded inside `future<result<T, _>>` on an exported function,
// used to emit unresolved bare `T` references in the generated
// `wit_future::vtable{N}` module. See issue #1598. The fix walks the
// payload type tree and emits a `use super::super::...;` per named type
// at the top of each vtable module.
#[allow(unused, reason = "testing codegen, not functionality")]
mod issue_1598_future_result_use {
wit_bindgen::generate!({
inline: r#"
package a:b;

world w {
use t.{r};
export f: func() -> future<result<r, string>>;
}

interface t {
record r {
x: u32,
}
}
"#,
});
}
Loading