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

Cannot await in scope that contains call to format! #64960

Open
jonhoo opened this issue Oct 1, 2019 · 5 comments

Comments

@jonhoo
Copy link
Contributor

commented Oct 1, 2019

As part of #64477, it was identified that the following code no longer compiles (#64477 (comment)):

async fn foo(_: String) {
}

fn bar() -> impl Send {
    async move {
        foo(format!("{}:{}", 1, 2)).await;
    }
}

This is as a result of this issue, and @nikomatsakis goes into some more detail as to why the borrow checker rejects the code in #64477 (comment). Basically, the code ends up creating temporaries that refer to the arguments of format!, and those temporaries are dropped only at the end of the current scope, which is after .await. And so, those temporaries must live across a yield point, which in turn prevents the resulting generator from being Send with the error:

error[E0277]: `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
 --> src/lib.rs:4:13
  |
4 | fn bar() -> impl Send {
  |             ^^^^^^^^^ `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
  |

This pattern (I have found at least) is quite common, and so having the code rejected is unfortunate given that in these cases ew only need the returned String before the yield point.

@Nemo157 proposed a fix in #64477 (comment), which is implemented in #64856, but it changes the drop order of the argument to format!, which may have unintended consequences. In #64856 (comment), @nikomatsakis suggested that we may be able to solve this with changes to the analysis, which prompted the opening of this issue.

@Blub

This comment has been minimized.

Copy link

commented Oct 4, 2019

Since assigning the formatted string to a variable first makes this work, I wonder if it is the same issue as with std::slice::from_raw_parts{,mut}(), like the two cases of which one works the other fails in this playground: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=319431dd1b0f8a05c351ccad03b19d0b

EDIT:
Simplified playground link: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=207b3206c6a6c403445f8eca02749592

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Oct 8, 2019

I'm assigning to myself only to dig into the output and add a few more facts about what's going on and what possible routes we might use to fix this -- not to actually fix it.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Oct 9, 2019

I did a little investigation. To start, I created a standalone test that doesn't rely on unstable details from the standard library (gist). This allows me to experiment a bit -- for example, I removed the _oibit_remover field from Void, which makes the format temporaries be considered Send, which then allowed me to observe the MIR that is ultimately created. You can view the graphviz for that MIR here.

Some observations:

  • The array _17 is the relevant temporary. You can see that it is created in block BB9 and then has a StorageDead invoked at the block BB37 (assuming no unwinding occurs). In between, in block BB29, the yield occurs (look for the suspend terminator).
  • The type of _17 is [ArgumentV1<'a>; 2].
  • This type is not "needs drop" -- the ArgumentV1 struct consists of a borrowed reference (of type &Void) and a formatter (of type fn). The compiler knows that these things never need to be dropped, and hence there is no DROP terminator in the MIR. NLL already makes this observable in terms of the set of programs that are accepted.
  • Therefore, if we had a more precise accounting of what is live, this value could arguably be excluded. However, it would not trivially be excluded, in that the stack slot is technically still live and so one might imagine that there are raw pointers pointing to it. This is something we've discussed on and off in terms of allocating generator space (cc @tmandry on that point) but I hadn't considered it from this angle before.

My conclusion then is that the correct route to fixing this problem begins by trying to address #57107 (get more precision) and is then blocked probably on us resolving some of the rules around when we need to preserve stack slots. This latter case is related to #61849 but is distinct, since in this case _17 is not moved afaik, it is borrowed (and that borrow is dead).

This is sort of a "special case" of the idea of us being able to move destructors -- basically, we might conceivably shorten the lifetime of temporaries where the type does not 'need drop' (i.e., where there is no destructor).

@nikomatsakis nikomatsakis removed their assignment Oct 9, 2019
@asafigan

This comment has been minimized.

Copy link

commented Oct 20, 2019

To clarify the work around for anyone who comes across this issue, assigning the formatted string to a variable first allows the program to compile.

Work around for the code provided by @jonhoo:

change

foo(format!("{}:{}", 1, 2)).await;

to

let formatted_string = format!("{}:{}", 1, 2);
foo(formatted_string).await;
@pimeys

This comment has been minimized.

Copy link

commented Oct 21, 2019

Spent some hours trying to find out why my code is not compiling and in the end it was due to the same issue described here. What makes it even worse is the huge amount of type information spamming the whole terminal in my case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.