-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
async fn cannot be recursive #53690
Comments
cc @cramertj @withoutboats (I'm not sure how easy the second example would be to fix) |
Wait, why are you using |
Definitely the trait object form will be fixed someday. I don't know if we can ever make a non-dynamic call with a heap allocation work. Recursion without heap allocating the new stack frame doesn't work at all as far as I know (unless its tail call recursion 😱). |
I wasn't aware that async fn foo() -> u32 {
let x = Box::new(foo());
await!(x) + 1
} you get exactly the same error. |
@upsuper You get a different error, involving The error you were getting before was about |
Ah, okay, I didn't notice that... Thanks for pointing that out. |
yeah, we can't ever allow non-tail recursion in |
Changing it to a version that doesn't actually recurse indefinitely: async fn foo(x: bool) -> u32 {
if x {
await!(foo(false)) + 1
}
else {
4
}
} doesn't compile for the same reason. Using FutureObj of PinBox of F to erase the type: async fn foo(x: bool) -> u32 {
if x {
let f: std::future::FutureObj<u32> = std::future::FutureObj::new(std::pin::PinBox::new(foo(false)));
await!(f) + 1
}
else {
4
}
} fails with
Changing the async fn to an async block still doesn't compile: fn foo(x: bool) -> impl std::future::Future<Output = u32> {
async move {
if x {
let f: std::future::FutureObj<u32> = std::future::FutureObj::new(std::pin::PinBox::new(foo(false)));
await!(f) + 1
}
else {
4
}
}
} with the same error about evaluating the Send bound. Adding the Send bound on the return type explicitly finally makes it compile: fn foo(x: bool) -> impl std::future::Future<Output = u32> + Send {
async move {
if x {
let f: std::future::FutureObj<u32> = std::future::FutureObj::new(std::pin::PinBox::new(foo(false)));
await!(f) + 1
}
else {
4
}
}
} |
I think the However, in this case: use futures::future::FutureObj;
async fn foo(x: bool) -> u32 {
if x {
let f = FutureObj::new(Box::new(foo(false)));
await!(f) + 1
}
else {
4
}
} I would expect However, that is fixed if we move the recursive call to a separate (non
async fn foo(x: bool) -> u32 {
if x {
- let f = FutureObj::new(Box::new(foo(false)));
+ fn out_of_line() -> FutureObj<'static, u32> {
+ FutureObj::new(Box::new(foo(false)))
+ }
+ let f = out_of_line();
await!(f) + 1
}
else { This seems to indicate that there is a bug (or missing optimization, in some points of view) in the implementation of generators. It looks like their memory layout keeps a "slot" for all intermediate expressions, not just those that need to be preserved across Can anyone familiar with the implementation confirm? |
cc @Zoxc |
This is a limitation of the current fn test() -> impl Sync {
test()
} |
@Zoxc I think your example is “the anonymous concrete type returned by However in #53690 (comment), because of type ereasure, I would expect the concrete generator type not to contain (a box of) itself, because that intermediate expression is been moved (into |
This is an example of what happens in #53690 (comment) with trait Trait {}
fn share<T: Send>(t: &T) {}
fn foo() -> impl Trait {
let a = foo();
share(&a);
} In your example, when type checking the body of Anyway this is all working as "intended" and there isn't really a bug here. |
The |
@SimonSapin You're manually breaking a cyclic dependency, it makes sense that it would work. The hard part is implementing the
|
Not sure how related this is, but I ran into a case where an
|
This is indeed because you are calling |
@ebkalderon Just like in #53690 (comment) you can fix this by erasing the type of the recursive future. let f: std::pin::Pin<Box<std::future::Future<Output = _>>> = Box::pin(fetched.build_dependencies());
let deps_built = await!(f); |
@Arnavion Thanks for the information! Wrapping the future in a |
Closing, as this is working as-intended. |
So, while it seems that this is working as intended (or at least "as it must"), the error message leaves much to be desired. Would be great to lift whatever friendly message is (I'd assume?) on infinitely sized structs over here. |
…Centril Explicit error message for async recursion. Attempt at clearer error message when async recusion is attempted. In response to rust-lang#62539 (and rust-lang#53690).
With the current design of async functions, it doesn't seem to be able to call them recursively.
The most straightforward way:
fails apparently, because the size of the future type is indefinite, and thus the compiler complains:
Another idea would be to put the recursive future into another heap-allocated object:
However this doesn't work either, because resolving the return type of
foo
requires the return type offoo
, which forms a cycle dependency, and compiler complains:If the type is a problem, maybe we can use trait object?
But it still doesn't work, because
Future
isn't object-safe due to thepoll
method, which is correctly reported by the compiler:So it seems that there is no way an async function can be called recursively.
I'm not sure how much problematic it would be, but it seems this limitation wasn't mentioned in the RFC nor any introduction of async, so maybe it's worth considering.
Recursion without additional allocation may be very challenging, but we should probably allow opt-in async recursion with some explicit cost.
The text was updated successfully, but these errors were encountered: