Skip to content

feat(rust): qualify named types in future/stream payload vtables#1601

Open
mattwilkinsonn wants to merge 3 commits intobytecodealliance:mainfrom
mattwilkinsonn:fix-future-result-use-qualification
Open

feat(rust): qualify named types in future/stream payload vtables#1601
mattwilkinsonn wants to merge 3 commits intobytecodealliance:mainfrom
mattwilkinsonn:fix-future-result-use-qualification

Conversation

@mattwilkinsonn
Copy link
Copy Markdown

Summary

Closes #1598.

future<result<R, _>> where R is brought in via use t.{r} at world level generates Rust code that references R unqualified inside the emitted wit_future::vtableN module, producing error[E0425]: cannot find type R in this scope. Same shape as #1433 (closed by #1528), but the future-of-result path wasn't covered by that fix — #1528 canonicalized the outer payload type but not types nested inside composite structures.

Reproducer

Minimal WIT mirroring the bug report in #1598:

wit_bindgen::generate!({
    inline: r#"
        package a:b;

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

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

fn main() {}

Before this PR (all seven codegen-test modes fail): error[E0425]: cannot find type R in this scope. After this PR: all seven pass.

Root cause

generate_payload in crates/rust/src/interface.rs emits code like:

pub mod vtable1 {
    unsafe fn lift(ptr: *mut u8) -> Result<R, super::super::_rt::String> { ... }
    //                                     ^ unresolved
}

_rt is qualified with super::super:: because path_to_root() has an Identifier::StreamOrFuturePayload arm that emits it. But type_path_with_name only emits the owner-qualified path when the type's TypeOwner is Interface(_). A world-level use t.{r} alias has TypeOwner::World(_), so the qualifier is skipped and the bare R is emitted. R lives at macro-root via the generated pub type R = ...; — two modules up from the vtable — but there's no corresponding use super::super::R; in the vtable module.

#1528 canonicalized the outer payload type so e.g. stream<r> correctly resolves through dealiasing. For future<result<r, string>> the outer result gets canonicalized but nothing dives into its ok slot to resolve r.

Fix

Walk the payload's type tree up-front and emit a use <qualified path>; statement at the top of each generated vtable{ordinal} module for every named type the payload transitively references. The printer's existing bare-identifier emission then resolves naturally inside the vtable scope without any behavioral change to type_path_with_name or print_tyid.

Implementation:

  • New free helper collect_named_type_ids that recursively walks Type/TypeDefKind, collecting the TypeId of every named type it finds. It stops at bare named aliases (TypeDefKind::Type(_) with a name) so we don't import both use t.{r}; (the alias) and r in interface t (its target) under the same Rust identifier — doing both would trip E0252.
  • In generate_payload, call the helper, compute each type's path via self.type_path(id, true), prepend super::super:: when the emitted path is a bare name (the aliased-from-world case that type_path_with_name leaves unqualified), and format a use ...; statement for each.
  • Insert the resulting {type_imports} block inside the pub mod vtable{ordinal} { ... } template. Added unused_imports to the module's existing #[allow] in case a payload happens to only reference types already in scope.

Benefits over fixing the printer:

  • No behavioral change to type_path_with_name / print_tyid — those remain exactly as they are, keeping the PR's blast radius contained to payload emission.
  • Handles arbitrary composites by construction (future<tuple<r, s>>, stream<variant { foo(r), bar(s) }>, future<result<_, r>>, nested records, etc.) — the walker doesn't care about shape, just follows inner Types.
  • Correct for use X as Y renames for free: self.type_path returns the locally-visible name, so the emitted use aliases to whatever the printer actually emits.

Test

tests/codegen/issue-1598.wit mirrors the reproducer in #1598. Run via the existing codegen harness — all seven test modes pass on this branch. Verified the test fails on main pre-fix in all seven modes (cannot find type R in this scope), so it acts as a real regression guard. The existing tests/codegen/issue-1432.wit, issue-1433.wit, and future-same-type-different-names.wit continue to pass (no regression of #1528's work).

$ cargo run test --languages rust tests/codegen --rust-wit-bindgen-path ./crates/guest-rust
...
test rust issue-1598.wit "tests/codegen/issue-1598.wit" ... ok
test rust issue-1598.wit-borrowed ... ok
test rust issue-1598.wit-borrowed-duplicate ... ok
test rust issue-1598.wit-async ... ok
test rust issue-1598.wit-merge-equal ... ok
test rust issue-1598.wit-hashmap ... ok
test rust issue-1598.wit-no-std ... ok
test result: ok. 665 passed; 0 failed; 0 ignored

Clippy (cargo clippy --workspace --all-targets -- -D warnings) and rustfmt (cargo fmt -- --check) both clean.

Downstream

My WIP project (not public yet) hit this bug when using future<result<turn-result, string>> in an async export with use types.{turn-result} at world level. With this patch the project builds cleanly against 0.57.1 + patched wit-bindgen-rust.

@mattwilkinsonn mattwilkinsonn changed the title rust: qualify named types in future/stream payload vtables feat(rust): qualify named types in future/stream payload vtables Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: future<result<T, ...>> where T is from a use produces unresolved type references

1 participant