Skip to content

TAIT: hidden type cannot be another opaque type from the same scope #96406

@Dirbaio

Description

@Dirbaio

I wanted to write a function that statically allocates an async fn future: playground

fn task(arg: Foo) -> &'static mut impl Future {
    async fn task_inner(arg: Foo) {
        // code
    }

    type Fut = impl Future;
    static mut FUT: MaybeUninit<Fut> = MaybeUninit::uninit();
    unsafe { FUT.write(task_inner(arg)) }
}

This fails with the following error:

error: opaque type's hidden type cannot be another opaque type from the same scope
  [--> src/lib.rs:8:47
](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021#)   |
8  |   fn task(arg: Foo) -> &'static mut impl Future {
   |  _______________________________________________^
9  | |     async fn task_inner(arg: Foo) {
10 | |         // code
11 | |     }
...  |
15 | |     unsafe { FUT.write(task_inner(arg)) }
16 | | }
   | |_^ one of the two opaque types used here has to be outside its defining scope
   |
note: opaque type whose hidden type is being assigned
  [--> src/lib.rs:8:35
](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021#)   |
8  | fn task(arg: Foo) -> &'static mut impl Future {
   |                                   ^^^^^^^^^^^
note: opaque type being used as hidden type
  [--> src/lib.rs:13:16
](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021#)   |
13 |     type Fut = impl Future;
   |                ^^^^^^^^^^^

One workaround is to move the Fut to the top level, but that's undesirable because it makes the entire file to be the defining scope, which interferes with other code.

A better workaround suggested by @oli-obk is to wrap the inner future in a "dumb" wrapper, so that the types are no longer equal (the RPIT can't be another opaque type itself, but it can still contain one): playground

struct FutureWrapper<F>(F);
impl<F: Future> Future for FutureWrapper<F> {
    type Output = F::Output;
    fn poll(
        self: Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }.poll(cx)
    }
}

fn task(arg: Foo) -> &'static mut impl Future {
    async fn task_inner(arg: Foo) {
        // code
    }

    type Fut = impl Future;
    static mut FUT: MaybeUninit<FutureWrapper<Fut>> = MaybeUninit::uninit();
    unsafe { FUT.write(FutureWrapper(task_inner(arg))) }
}

First, it would be interesting to see if these "nested opaque types" can be allowed in some cases such as this one.

@oli-obk we could possibly allow your case (which has an obvious order of which opaque type should be the hidden type of the other one, or just by equating their hidden types)
The fixes to just allow it are likely not complex in implementation, but require quite some design work to make sure we don't paint ourselves into a corner
We could just put it behind a feature gate tho

Second, it would be helpful if the diagnostic suggested the "wrapper" workaround. It would've saved me a lot of time at least :)

See also: Zulip thread

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-diagnosticsArea: Messages for errors, warnings, and lintsA-impl-traitArea: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch.F-type_alias_impl_trait`#[feature(type_alias_impl_trait)]`P-lowLow priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Can do after stabilization

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions