`async` failing to unify invariant lifetime?

This doesn't compile, but I'm not exactly certain why: [playground]

pub struct Scope</* invariant */ 'scope, /* covariant */ 'env: 'scope> { /* .. */ }
pub struct ScopedJoinHandle</* covariant */ 'scope, F> { /* .. */ }

impl<'scope, 'env> Scope<'scope, 'env> {
    pub fn spawn<F>(&'scope self, _: F) -> ScopedJoinHandle<'scope, F>
    where F: Future + 'scope
    { /* .. */ }
}

pub async fn scope<'env, F, T>(f: F) -> T::Output
where
    F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,
    T: IntoFuture.
{ /* .. */ }

fn task<'scope>(s: &'scope Scope<'scope, '_>)
    -> impl Future<Output = ()> + 'scope
{
    async move { s.spawn(async {}); }
}

fn main() {
    scope(|s| task(s));
}

gives

error: lifetime may not live long enough
  --> src/main.rs:40:15
   |
40 |     scope(|s| task(s));
   |            -- ^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |            ||
   |            |return type of closure is impl Future<Output = ()> + '2
   |            has type `&'1 Scope<'1, '_>`

(Yes, I know that scoped future spawns are fundamentally unsound. I'm not trying to do the unsound thing, only imitate its API[1].)

There should be no way that '1 and '2 are different lifetimes, given the signature of fn task and the fact that 'scope is an invariant lifetime. The only way for '2 to be shorter than '1 is if the lifetime got reborrowed to a shorter lifetime, which shouldn't be possible for invariant lifetimes.

If my understanding is correct here, I'll file a rustc issue for this.


  1. Specifically, what I'm trying to do is to provided a scoped "spawn" looking interface that essentially "spawns" them into a FuturesUnordered and polls them inline. They only get polled when the Scope gets polled and thus everything is sound; this could theoretically be written without any unsafe at all. ↩︎

Correct me if I'm wrong but I simply see scope expecting an F with a single return type T independent of the 'scope lifetime, whereas task's (impl Trait) return type does contain (and thus depends on) the 'scope lifetime.

This also looks maybe similar to the kind of situation discussed here.

... That's a GAT problem, isn't it, in that the return type of the function wants to (be able to) capture the input lifetime. Ugh, I should've spotted that... it's quite unfortunate but probably unavoidable.

The error is unfortunately unhelpful, in that "outlives" is not a very helpful (though accurate) way to communicate "does not capture" in this context.

Yes, errors when lifetimes don't match are sometimes remarkably bad. Sometimes I say to myseld "it's a simple and straightforward type error, the types don't match 'cuz of different lifetime", but then the compiler comes and somehow manages to phrase the error message in the most opaque and weird way.

I was almost able to stuff it through a trait AsyncFn shaped hole, but it's quite unpretty and unfortunately both absolutely hates type inference and fails to satisfy lifetime constraints because of the use of a custom trait, even before trying to do anything interesting. [playground]

Trying to force it to "work"
pub trait AsyncFn<Args>: FnOnce(Args) -> Self::ImplFuture {
    type Output;
    type ImplFuture: Future<Output = <Self as AsyncFn<Args>>::Output>;
    fn into_future(self, args: Args) -> Self::ImplFuture;
}

impl<Fn, Args, Ret> AsyncFn<Args> for Fn
where
    Fn: FnOnce(Args) -> Ret,
    Ret: Future,
{
    type Output = Ret::Output;
    type ImplFuture = Ret;
    fn into_future(self, args: Args) -> Ret {
        self(args)
    }
}

pub async fn scope<'env, F, T>(f: F) -> T
where
    F: for<'scope> AsyncFn<&'scope Scope<'scope, 'env>, Output = T>,
{ /* .. */ }
fn task<'scope>(s: &'scope Scope<'scope, '_>) -> impl Future<Output = ()> + 'scope {
    async move {
        s.spawn(async {});
    }
}

fn main() {
    // okay
    scope(task);

    // error
    scope(|s: &Scope| {
        s.spawn(async move {});
        async {}
    });
}
error: lifetime may not live long enough
  --> src/main.rs:61:9
   |
60 |     scope(|s: &Scope| {
   |            -  - let's call the lifetime of this reference `'1`
   |            |
   |            has type `&Scope<'2, '_>`
61 |         s.spawn(async move {});
   |         ^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'2`

This isn't directly resolvable without inline for<'a> closure lifetime binder syntax to force the closure to use the same lifetime for 'scope. Alternatively, I could relax Scope::spawn to take &self instead of &'scope self (I don't think that restriction is relevant to soundness), and then things start to look like they're working, but it's still a hack that runs into lifetime issues for the closure when trying to actually capture the scope parameter into an async block, e.g.

fn task<'scope>(s: &'scope Scope<'scope, '_>) -> impl Future<Output = ()> + 'scope {
    async move {
        s.spawn(async {});
    }
}

fn main() {
    // this is okay
    scope(task);
    
    // but this is an error
    scope(|s: &Scope| async move {
        s.spawn(async {});
    });
}
error: lifetime may not live long enough
  --> src/main.rs:61:23
   |
61 |       scope(|s: &Scope| async move {
   |  _______________-_____-_^
   | |               |     |
   | |               |     return type of closure `[async block@src/main.rs:61:23: 63:6]` contains a lifetime `'2`
   | |               let's call the lifetime of this reference `'1`
62 | |         s.spawn(async {});
63 | |     });
   | |_____^ returning this value requires that `'1` must outlive `'2`

error: lifetime may not live long enough
  --> src/main.rs:61:23
   |
61 |       scope(|s: &Scope| async move {
   |  ____________-________-_^
   | |            |        |
   | |            |        return type of closure `[async block@src/main.rs:61:23: 63:6]` contains a lifetime `'4`
   | |            has type `&Scope<'3, '_>`
62 | |         s.spawn(async {});
63 | |     });
   | |_____^ returning this value requires that `'3` must outlive `'4`
   |
   = note: requirement occurs because of the type `Scope<'_, '_>`, which makes the generic argument `'_` invariant
   = note: the struct `Scope<'scope, 'env>` is invariant over the parameter `'scope`
   = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance

error: lifetime may not live long enough
  --> src/main.rs:61:23
   |
61 |       scope(|s: &Scope| async move {
   |  ____________-________-_^
   | |            |        |
   | |            |        return type of closure `[async block@src/main.rs:61:23: 63:6]` contains a lifetime `'2`
   | |            has type `&Scope<'_, '5>`
62 | |         s.spawn(async {});
63 | |     });
   | |_____^ returning this value requires that `'5` must outlive `'2`

Unstably using FnOnce directly also almost works, but also runs into a weird error, likely due to the Fn* traits not really being intended to be used to project to their Output associated type. [playground]

Attempted crimes against stability
pub async fn scope<'env, F, T>(f: F) -> T
where
    F: for<'scope> FnOnce<(&'scope Scope<'scope, 'env>,)>,
    for<'scope> <F as FnOnce<(&'scope Scope<'scope, 'env>,)>>::Output: Future<Output = T>,
{ /* .. */ }

async fn task<'scope>(_: &'scope Scope<'scope, '_>) {}

fn main() {
    scope(task);
}
error[E0277]: `<_ as FnOnce<(&'scope Scope<'scope, '_>,)>>::Output` is not a future
  --> src/main.rs:38:11
   |
38 |     scope(task);
   |     ----- ^^^^ `<_ as FnOnce<(&'scope Scope<'scope, '_>,)>>::Output` is not a future
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `for<'scope> Future` is not implemented for `<_ as FnOnce<(&'scope Scope<'scope, '_>,)>>::Output`
   = note: <_ as FnOnce<(&'scope Scope<'scope, '_>,)>>::Output must be a future or must implement `IntoFuture` to be awaited
note: required by a bound in `scope`
  --> src/main.rs:29:72
   |
26 | pub async fn scope<'env, F, T>(f: F) -> T
   |              ----- required by a bound in this function
...
29 |     for<'scope> <F as FnOnce<(&'scope Scope<'scope, 'env>,)>>::Output: Future<Output = T>,
   |                                                                        ^^^^^^^^^^^^^^^^^^ required by this bound in `scope`