From 9ef09ef479b6b78c762788fce168c245c1da3dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Thu, 1 Jan 2026 17:25:25 -0500 Subject: [PATCH] Improve move error diagnostic for `AsyncFn` closures When an async closure captures a variable by move but is constrained to `AsyncFn` or `AsyncFnMut`, the error message now explains that the closure kind is the issue and points to the trait bound, similar to the existing diagnostic for `Fn`/`FnMut` closures. --- .../src/diagnostics/move_errors.rs | 96 ++++++++++++++++--- .../move-from-async-fn-bound.rs | 13 +++ .../move-from-async-fn-bound.stderr | 30 ++++++ 3 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 tests/ui/async-await/async-closures/move-from-async-fn-bound.rs create mode 100644 tests/ui/async-await/async-closures/move-from-async-fn-bound.stderr diff --git a/compiler/rustc_borrowck/src/diagnostics/move_errors.rs b/compiler/rustc_borrowck/src/diagnostics/move_errors.rs index 3322c590a6cee..011ba26d0a9fb 100644 --- a/compiler/rustc_borrowck/src/diagnostics/move_errors.rs +++ b/compiler/rustc_borrowck/src/diagnostics/move_errors.rs @@ -514,11 +514,62 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> { format!("captured by this `{closure_kind}` closure"), ) .with_span_help( - self.get_closure_bound_clause_span(*def_id), + self.get_closure_bound_clause_span(*def_id, false), "`Fn` and `FnMut` closures require captured values to be able to be \ consumed multiple times, but `FnOnce` closures may consume them only once", ) } + ty::CoroutineClosure(def_id, closure_args) + if def_id.as_local() == Some(self.mir_def_id()) + && let Some(upvar_field) = upvar_field + && let bound_span = self.get_closure_bound_clause_span(*def_id, true) + && bound_span != DUMMY_SP => + { + let closure_kind_ty = closure_args.as_coroutine_closure().kind_ty(); + let closure_kind = match closure_kind_ty.to_opt_closure_kind() { + Some(kind @ (ty::ClosureKind::Fn | ty::ClosureKind::FnMut)) => kind, + Some(ty::ClosureKind::FnOnce) => { + bug!("coroutine closure kind does not match first argument type") + } + None => bug!("coroutine closure kind not inferred by borrowck"), + }; + let capture_description = + format!("captured variable in an `Async{closure_kind}` closure"); + + let upvar = &self.upvars[upvar_field.index()]; + let upvar_hir_id = upvar.get_root_variable(); + let upvar_name = upvar.to_string(tcx); + let upvar_span = tcx.hir_span(upvar_hir_id); + + let place_name = self.describe_any_place(move_place.as_ref()); + + let place_description = + if self.is_upvar_field_projection(move_place.as_ref()).is_some() { + format!("{place_name}, a {capture_description}") + } else { + format!("{place_name}, as `{upvar_name}` is a {capture_description}") + }; + + debug!( + "report: closure_kind_ty={:?} closure_kind={:?} place_description={:?}", + closure_kind_ty, closure_kind, place_description, + ); + + let closure_span = tcx.def_span(def_id); + + self.cannot_move_out_of(span, &place_description) + .with_span_label(upvar_span, "captured outer variable") + .with_span_label( + closure_span, + format!("captured by this `Async{closure_kind}` closure"), + ) + .with_span_help( + bound_span, + "`AsyncFn` and `AsyncFnMut` closures require captured values to be able \ + to be consumed multiple times, but `AsyncFnOnce` closures may consume \ + them only once", + ) + } _ => { let source = self.borrowed_content_source(deref_base); let move_place_ref = move_place.as_ref(); @@ -566,7 +617,7 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> { err } - fn get_closure_bound_clause_span(&self, def_id: DefId) -> Span { + fn get_closure_bound_clause_span(&self, def_id: DefId, is_async: bool) -> Span { let tcx = self.infcx.tcx; let typeck_result = tcx.typeck(self.mir_def_id()); // Check whether the closure is an argument to a call, if so, @@ -590,18 +641,37 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> { _ => return DUMMY_SP, }; - // Check whether one of the where-bounds requires the closure to impl `Fn[Mut]`. + // Check whether one of the where-bounds requires the closure to impl `Fn[Mut]` + // or `AsyncFn[Mut]`. for (pred, span) in predicates.predicates.iter().zip(predicates.spans.iter()) { - if let Some(clause) = pred.as_trait_clause() - && let ty::Closure(clause_closure_def_id, _) = clause.self_ty().skip_binder().kind() - && *clause_closure_def_id == def_id - && (tcx.lang_items().fn_mut_trait() == Some(clause.def_id()) - || tcx.lang_items().fn_trait() == Some(clause.def_id())) - { - // Found `` - // We point at the `Fn()` or `FnMut()` bound that coerced the closure, which - // could be changed to `FnOnce()` to avoid the move error. - return *span; + if let Some(clause) = pred.as_trait_clause() { + let dominated_by_fn_trait = if is_async { + matches!( + tcx.async_fn_trait_kind_from_def_id(clause.def_id()), + Some(ty::ClosureKind::Fn | ty::ClosureKind::FnMut) + ) + } else { + matches!( + tcx.fn_trait_kind_from_def_id(clause.def_id()), + Some(ty::ClosureKind::Fn | ty::ClosureKind::FnMut) + ) + }; + let clause_closure_def_id = match clause.self_ty().skip_binder().kind() { + ty::Closure(closure_def_id, _) | ty::CoroutineClosure(closure_def_id, _) => { + Some(closure_def_id) + } + _ => None, + }; + if let Some(clause_closure_def_id) = clause_closure_def_id + && *clause_closure_def_id == def_id + && dominated_by_fn_trait + { + // Found `` or + // ``. + // We point at the bound that coerced the closure, which could be changed + // to `FnOnce()` or `AsyncFnOnce()` to avoid the move error. + return *span; + } } } DUMMY_SP diff --git a/tests/ui/async-await/async-closures/move-from-async-fn-bound.rs b/tests/ui/async-await/async-closures/move-from-async-fn-bound.rs new file mode 100644 index 0000000000000..fbd8aac2515b9 --- /dev/null +++ b/tests/ui/async-await/async-closures/move-from-async-fn-bound.rs @@ -0,0 +1,13 @@ +//@ edition:2021 +// Test that a by-ref `AsyncFn` closure gets an error when it tries to +// consume a value, with a helpful diagnostic pointing to the bound. + +fn call(_: F) where F: AsyncFn() {} + +fn main() { + let y = vec![format!("World")]; + call(async || { + //~^ ERROR cannot move out of `y`, a captured variable in an `AsyncFn` closure + y.into_iter(); + }); +} diff --git a/tests/ui/async-await/async-closures/move-from-async-fn-bound.stderr b/tests/ui/async-await/async-closures/move-from-async-fn-bound.stderr new file mode 100644 index 0000000000000..1a881db2a37d5 --- /dev/null +++ b/tests/ui/async-await/async-closures/move-from-async-fn-bound.stderr @@ -0,0 +1,30 @@ +error[E0507]: cannot move out of `y`, a captured variable in an `AsyncFn` closure + --> $DIR/move-from-async-fn-bound.rs:9:10 + | +LL | let y = vec![format!("World")]; + | - captured outer variable +LL | call(async || { + | ^^^^^^^^ + | | + | captured by this `AsyncFn` closure + | `y` is moved here +LL | +LL | y.into_iter(); + | - + | | + | variable moved due to use in coroutine + | move occurs because `y` has type `Vec`, which does not implement the `Copy` trait + | +help: `AsyncFn` and `AsyncFnMut` closures require captured values to be able to be consumed multiple times, but `AsyncFnOnce` closures may consume them only once + --> $DIR/move-from-async-fn-bound.rs:5:27 + | +LL | fn call(_: F) where F: AsyncFn() {} + | ^^^^^^^^^ +help: consider cloning the value if the performance cost is acceptable + | +LL | y.clone().into_iter(); + | ++++++++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0507`.