Recursive async function permitted, but why?

This works (playground):

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

async fn recurse(i: usize) {
    if i == 0 {
        println!("Zero!");
        return;
    }
    
    println!("entering {}", i);
    let recursed = Box::pin(recurse(i-1))
        as Pin<Box<dyn Future<Output=()>>>;
    recursed.await;
    println!("exiting {}", i);
}

fn main() {
    futures::executor::block_on(recurse(10));
}

But if we change the interesting bit to this, we get a compiler error:

    println!("entering {}", i);
    (Box::pin(recurse(i-1)) as Pin<Box<dyn Future<Output=()>>>).await;
    println!("exiting {}", i);

In neither case is the unboxed future live across the await. What is it about the version with the explicit temporary that makes it acceptable? (Is this a bug?)

My guess would be that this is just business as usual: (Box::pin(recurse(i-1)) as Pin<Box<dyn Future<Output=()>>>) is a temporary and is dropped immediately after the expression ends. OTOH, let recursed = Box::pin(recurse(i-1)) is a name binding that keeps the value alive until the end of the innermost block's scope.

I might be wrong, though – not sure if any special interaction with async is happening here.

But wouldn't that tend to make you expect the opposite behavior? It's the let version that works, and the expression temporary that doesn't. (Well, there's an expression temporary in both cases.)

The .await takes ownership so it doesn't matter that it's a temporary.

2 Likes

The concrete type of recurse(i-1) is probably leaking into the approximation used during initial type checking because it's part of the temporaries of the .await statement. Very similar to how foo(format!("{}", bar)).await would give errors about *mut (dyn std::ops::Fn() + 'static) because format!'s temporaries were treated as live across the await.

But in this case it seems more clearly a bug since as should be consuming its operand so it should be known that it is not live across the yield.

Note that it should be allowed to just use Box::pin, without casting it into a trait object at all, and the fact that it isn't is an implementation limitation of how current rustc calculates the layout of the future type. It doesn't look like there's a bug tracking this limitation though.

Oh, indeed.

Thanks for taking a look. Filed:

Filed: