Skip to content

Commit d2f0df7

Browse files
authored
Rollup merge of #125306 - compiler-errors:closure-incongruency, r=oli-obk
Force the inner coroutine of an async closure to `move` if the outer closure is `move` and `FnOnce` See the detailed comment in `upvar.rs`. Fixes #124867. Fixes #124487. r? oli-obk
2 parents ab9e0a7 + 6b30582 commit d2f0df7

File tree

3 files changed

+109
-31
lines changed

3 files changed

+109
-31
lines changed

compiler/rustc_hir_typeck/src/upvar.rs

+58-31
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,60 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
204204
fake_reads: Default::default(),
205205
};
206206

207+
let _ = euv::ExprUseVisitor::new(
208+
&FnCtxt::new(self, self.tcx.param_env(closure_def_id), closure_def_id),
209+
&mut delegate,
210+
)
211+
.consume_body(body);
212+
213+
// There are several curious situations with coroutine-closures where
214+
// analysis is too aggressive with borrows when the coroutine-closure is
215+
// marked `move`. Specifically:
216+
//
217+
// 1. If the coroutine-closure was inferred to be `FnOnce` during signature
218+
// inference, then it's still possible that we try to borrow upvars from
219+
// the coroutine-closure because they are not used by the coroutine body
220+
// in a way that forces a move. See the test:
221+
// `async-await/async-closures/force-move-due-to-inferred-kind.rs`.
222+
//
223+
// 2. If the coroutine-closure is forced to be `FnOnce` due to the way it
224+
// uses its upvars, but not *all* upvars would force the closure to `FnOnce`.
225+
// See the test: `async-await/async-closures/force-move-due-to-actually-fnonce.rs`.
226+
//
227+
// This would lead to an impossible to satisfy situation, since `AsyncFnOnce`
228+
// coroutine bodies can't borrow from their parent closure. To fix this,
229+
// we force the inner coroutine to also be `move`. This only matters for
230+
// coroutine-closures that are `move` since otherwise they themselves will
231+
// be borrowing from the outer environment, so there's no self-borrows occuring.
232+
//
233+
// One *important* note is that we do a call to `process_collected_capture_information`
234+
// to eagerly test whether the coroutine would end up `FnOnce`, but we do this
235+
// *before* capturing all the closure args by-value below, since that would always
236+
// cause the analysis to return `FnOnce`.
237+
if let UpvarArgs::Coroutine(..) = args
238+
&& let hir::CoroutineKind::Desugared(_, hir::CoroutineSource::Closure) =
239+
self.tcx.coroutine_kind(closure_def_id).expect("coroutine should have kind")
240+
&& let parent_hir_id =
241+
self.tcx.local_def_id_to_hir_id(self.tcx.local_parent(closure_def_id))
242+
&& let parent_ty = self.node_ty(parent_hir_id)
243+
&& let hir::CaptureBy::Value { move_kw } =
244+
self.tcx.hir_node(parent_hir_id).expect_closure().capture_clause
245+
{
246+
// (1.) Closure signature inference forced this closure to `FnOnce`.
247+
if let Some(ty::ClosureKind::FnOnce) = self.closure_kind(parent_ty) {
248+
capture_clause = hir::CaptureBy::Value { move_kw };
249+
}
250+
// (2.) The way that the closure uses its upvars means it's `FnOnce`.
251+
else if let (_, ty::ClosureKind::FnOnce, _) = self
252+
.process_collected_capture_information(
253+
capture_clause,
254+
&delegate.capture_information,
255+
)
256+
{
257+
capture_clause = hir::CaptureBy::Value { move_kw };
258+
}
259+
}
260+
207261
// As noted in `lower_coroutine_body_with_moved_arguments`, we default the capture mode
208262
// to `ByRef` for the `async {}` block internal to async fns/closure. This means
209263
// that we would *not* be moving all of the parameters into the async block by default.
@@ -253,34 +307,6 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
253307
}
254308
}
255309

256-
let _ = euv::ExprUseVisitor::new(
257-
&FnCtxt::new(self, self.tcx.param_env(closure_def_id), closure_def_id),
258-
&mut delegate,
259-
)
260-
.consume_body(body);
261-
262-
// If a coroutine is comes from a coroutine-closure that is `move`, but
263-
// the coroutine-closure was inferred to be `FnOnce` during signature
264-
// inference, then it's still possible that we try to borrow upvars from
265-
// the coroutine-closure because they are not used by the coroutine body
266-
// in a way that forces a move.
267-
//
268-
// This would lead to an impossible to satisfy situation, since `AsyncFnOnce`
269-
// coroutine bodies can't borrow from their parent closure. To fix this,
270-
// we force the inner coroutine to also be `move`. This only matters for
271-
// coroutine-closures that are `move` since otherwise they themselves will
272-
// be borrowing from the outer environment, so there's no self-borrows occuring.
273-
if let UpvarArgs::Coroutine(..) = args
274-
&& let hir::CoroutineKind::Desugared(_, hir::CoroutineSource::Closure) =
275-
self.tcx.coroutine_kind(closure_def_id).expect("coroutine should have kind")
276-
&& let parent_hir_id =
277-
self.tcx.local_def_id_to_hir_id(self.tcx.local_parent(closure_def_id))
278-
&& let parent_ty = self.node_ty(parent_hir_id)
279-
&& let Some(ty::ClosureKind::FnOnce) = self.closure_kind(parent_ty)
280-
{
281-
capture_clause = self.tcx.hir_node(parent_hir_id).expect_closure().capture_clause;
282-
}
283-
284310
debug!(
285311
"For closure={:?}, capture_information={:#?}",
286312
closure_def_id, delegate.capture_information
@@ -289,7 +315,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
289315
self.log_capture_analysis_first_pass(closure_def_id, &delegate.capture_information, span);
290316

291317
let (capture_information, closure_kind, origin) = self
292-
.process_collected_capture_information(capture_clause, delegate.capture_information);
318+
.process_collected_capture_information(capture_clause, &delegate.capture_information);
293319

294320
self.compute_min_captures(closure_def_id, capture_information, span);
295321

@@ -545,13 +571,14 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
545571
fn process_collected_capture_information(
546572
&self,
547573
capture_clause: hir::CaptureBy,
548-
capture_information: InferredCaptureInformation<'tcx>,
574+
capture_information: &InferredCaptureInformation<'tcx>,
549575
) -> (InferredCaptureInformation<'tcx>, ty::ClosureKind, Option<(Span, Place<'tcx>)>) {
550576
let mut closure_kind = ty::ClosureKind::LATTICE_BOTTOM;
551577
let mut origin: Option<(Span, Place<'tcx>)> = None;
552578

553579
let processed = capture_information
554-
.into_iter()
580+
.iter()
581+
.cloned()
555582
.map(|(place, mut capture_info)| {
556583
// Apply rules for safety before inferring closure kind
557584
let (place, capture_kind) =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//@ aux-build:block-on.rs
2+
//@ edition:2021
3+
//@ check-pass
4+
5+
#![feature(async_closure)]
6+
7+
extern crate block_on;
8+
9+
fn consume(_: String) {}
10+
11+
fn main() {
12+
block_on::block_on(async {
13+
let x = 1i32;
14+
let s = String::new();
15+
// `consume(s)` pulls the closure's kind down to `FnOnce`,
16+
// which means that we don't treat the borrow of `x` as a
17+
// self-borrow (with `'env` lifetime). This leads to a lifetime
18+
// error which is solved by forcing the inner coroutine to
19+
// be `move` as well, so that it moves `x`.
20+
let c = async move || {
21+
println!("{x}");
22+
// This makes the closure FnOnce...
23+
consume(s);
24+
};
25+
c().await;
26+
});
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//@ aux-build:block-on.rs
2+
//@ edition:2021
3+
//@ check-pass
4+
5+
#![feature(async_closure)]
6+
7+
extern crate block_on;
8+
9+
fn force_fnonce<T: async FnOnce()>(t: T) -> T { t }
10+
11+
fn main() {
12+
block_on::block_on(async {
13+
let x = 1i32;
14+
// `force_fnonce` pulls the closure's kind down to `FnOnce`,
15+
// which means that we don't treat the borrow of `x` as a
16+
// self-borrow (with `'env` lifetime). This leads to a lifetime
17+
// error which is solved by forcing the inner coroutine to
18+
// be `move` as well, so that it moves `x`.
19+
let c = force_fnonce(async move || {
20+
println!("{x}");
21+
});
22+
c().await;
23+
});
24+
}

0 commit comments

Comments
 (0)