Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutually recursive async fns are hard to make Send #62284

Open
cramertj opened this issue Jul 1, 2019 · 6 comments
Open

Mutually recursive async fns are hard to make Send #62284

cramertj opened this issue Jul 1, 2019 · 6 comments
Labels
A-async-await Area: Async & Await AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@cramertj
Copy link
Member

cramertj commented Jul 1, 2019

There are several other related issues to this, but I'm opening this to track this one specifically since it's a pain-- there are workarounds, but it'd be lovely (and should be possible) to make this "just work." The following example compiles just fine without + Send, but adding the Send bound causes a cycle error:

#![feature(async_await)]

use {
    std::{
        future::Future,
        pin::Pin,
    },
};

type BoxFuture = Pin<Box<dyn Future<Output = ()> /* + Send */>>; // adding Send causes a cycle error

async fn foo() -> BoxFuture {
    Box::pin(bar()) as _
}

async fn bar() {
    let _ = foo().await;
}

Working around the cycle error is possible, but annoying:

#![feature(async_await)]

use {
    std::{
        future::Future,
        pin::Pin,
    },
};

type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send>>;

async fn foo() -> BoxFuture {
    box_bar()
}

fn box_bar() -> BoxFuture {
    Box::pin(bar())
}

async fn bar() {
    let _ = foo().await;
}

Ideally we wouldn't have a cycle error in either case, since it is possible to see that foo must be Send without ever looking at the body of bar, since bar is immediately boxed into a BoxFuture.

@cramertj cramertj added the A-async-await Area: Async & Await label Jul 1, 2019
@nikomatsakis nikomatsakis added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Jul 2, 2019
@nikomatsakis nikomatsakis added the AsyncAwait-Polish Async-await issues that are part of the "polish" area label Jul 2, 2019
@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jul 5, 2019

So this is tied to cycles amongst impl Trait. The problem arises because we need to know the hidden type of bar, which in turn is tied to the hidden type of foo, in order to figure out the whether the result of bar() will be Send. The easiest workaround here is to desugar to something that returns impl Future + Send:

#![feature(async_await)]

use {
    std::{
        future::Future,
        pin::Pin,
    },
};

type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send>>; // adding Send causes a cycle error

fn foo() -> impl Future<Output = BoxFuture> + Send {
    async {
        Box::pin(bar()) as BoxFuture
    }
}

async fn bar() {
    let _ = foo().await;
}

fn main() { }

@nikomatsakis
Copy link
Contributor

It's tricky but not impossible to imagine a more flexible handling of auto traits and leakage. We've discussed it before and it's probably a good idea. But I think it's pretty orthogonal from async-await.

Therefore, presuming that we want to keep our current desugaring, I think that we should probably consider this as non-blocking -- but it's definitely something to add to the list of "surprises that may trip up regular users".

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jul 5, 2019

It's actually sort of mysterious to me that this example builds at all. For example, in trying to convert this recursive async fn to something that builds:

#![feature(async_await)]

async fn foo(n: usize) {
    if n > 0 {
        Box::new(foo(n - 1)).await;
    }
}

fn is_send<T: Send>(t: T) { drop(t); }

fn main() {
    is_send(foo());
}

The only thing I could find which works is to use a Box<dyn Future> return type:

#![feature(async_await)]

use std::future::Future;
use std::pin::Pin;

fn foo(n: usize) -> Pin<Box<dyn Future<Output = ()> + Send>> {
    Box::pin(async move {
        if n > 0 {
            foo(n - 1).await;
        }
    })
}

fn is_send<T: Send>(t: T) { drop(t); }

fn main() {
    is_send(foo(22));
}

Any other setup (including, say, this one that uses Box::pin) all fail because the future we are awaiting (foo) winds up as part of the type of foo.

This is a consequence, as far as I can tell, of our desugaring to generators, which in turn is currently modeled after our approach to closures, and closures cannot capture themselves. This is needed to make "upvar inference" manageable. It occurs to me, though, that this restriction is not needed for move closures, which do not need to do upvar inference -- the same presumably applies to async move blocks. It'd be a bit of a complex refactoring, but we could restructure their types to lift the restriction.

@nikomatsakis
Copy link
Contributor

I guess that the reason the original example sidesteps these problems is because foo never awaits the return type of bar(), so it never appears in the generator type.

@cramertj
Copy link
Member Author

cramertj commented Jul 8, 2019

@nikomatsakis Right-- the point here is that there shouldn't be a cycle at all, because the value that appears inside the generator is not the future type itself, but the type Pin<Box<dyn Future<...> + Send>>.

@nikomatsakis
Copy link
Contributor

Discussed in meeting today. Filed #62539 to try and improve diagnostics for the naive case. I am marking as "unclear" because it might be good enough for stabilization purposes to document the correct workarounds (rust-lang/async-book#22) and improve error messages (#62539).

@nikomatsakis nikomatsakis added AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. and removed AsyncAwait-Polish Async-await issues that are part of the "polish" area AsyncAwait-Unclear labels Jul 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-async-await Area: Async & Await AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

4 participants