Skip to content

Point at the Fn() or FnMut() bound that coerced a closure, which caused a move error #144558

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

Merged
merged 3 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion compiler/rustc_borrowck/src/diagnostics/move_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use rustc_middle::bug;
use rustc_middle::mir::*;
use rustc_middle::ty::{self, Ty, TyCtxt};
use rustc_mir_dataflow::move_paths::{LookupResult, MovePathIndex};
use rustc_span::{BytePos, ExpnKind, MacroKind, Span};
use rustc_span::def_id::DefId;
use rustc_span::{BytePos, DUMMY_SP, ExpnKind, MacroKind, Span};
use rustc_trait_selection::error_reporting::traits::FindExprBySpan;
use rustc_trait_selection::infer::InferCtxtExt;
use tracing::debug;
Expand Down Expand Up @@ -507,12 +508,18 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
);

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 `{closure_kind}` closure"),
)
.with_span_help(
self.get_closure_bound_clause_span(*def_id),
"`Fn` and `FnMut` closures require captured values to be able to be \
consumed multiple times, but an `FnOnce` consume them only once",
)
}
_ => {
let source = self.borrowed_content_source(deref_base);
Expand Down Expand Up @@ -561,6 +568,47 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
err
}

fn get_closure_bound_clause_span(&self, def_id: DefId) -> 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,
// get the instantiated where-bounds of that call.
let closure_hir_id = tcx.local_def_id_to_hir_id(def_id.expect_local());
let hir::Node::Expr(parent) = tcx.parent_hir_node(closure_hir_id) else { return DUMMY_SP };

let predicates = match parent.kind {
hir::ExprKind::Call(callee, _) => {
let Some(ty) = typeck_result.node_type_opt(callee.hir_id) else { return DUMMY_SP };
let ty::FnDef(fn_def_id, args) = ty.kind() else { return DUMMY_SP };
tcx.predicates_of(fn_def_id).instantiate(tcx, args)
}
hir::ExprKind::MethodCall(..) => {
let Some((_, method)) = typeck_result.type_dependent_def(parent.hir_id) else {
return DUMMY_SP;
};
let args = typeck_result.node_args(parent.hir_id);
tcx.predicates_of(method).instantiate(tcx, args)
}
_ => return DUMMY_SP,
};

// Check whether one of the where-bounds requires the closure to impl `Fn[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 `<TyOfCapturingClosure as FnMut>`
// 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;
}
}
DUMMY_SP
}

fn add_move_hints(&self, error: GroupedMoveError<'tcx>, err: &mut Diag<'_>, span: Span) {
match error {
GroupedMoveError::MovesFromPlace { mut binds_to, move_from, .. } => {
Expand Down
7 changes: 4 additions & 3 deletions compiler/rustc_errors/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -847,17 +847,18 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
self
}

with_fn! { with_span_help,
/// Prints the span with some help above it.
/// This is like [`Diag::help()`], but it gets its own span.
#[rustc_lint_diagnostics]
pub fn span_help<S: Into<MultiSpan>>(
pub fn span_help(
&mut self,
sp: S,
sp: impl Into<MultiSpan>,
msg: impl Into<SubdiagMessage>,
) -> &mut Self {
self.sub(Level::Help, msg, sp.into());
self
}
} }

/// Disallow attaching suggestions to this diagnostic.
/// Any suggestions attached e.g. with the `span_suggestion_*` methods
Expand Down
1 change: 1 addition & 0 deletions tests/ui/borrowck/borrowck-in-static.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ LL | Box::new(|| x)
| |
| captured by this `Fn` closure
|
= help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
help: consider cloning the value if the performance cost is acceptable
|
LL | Box::new(|| x.clone())
Expand Down
5 changes: 5 additions & 0 deletions tests/ui/borrowck/borrowck-move-by-capture.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ LL | let _h = to_fn_once(move || -> isize { *bar });
| |
| `bar` is moved here
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
--> $DIR/borrowck-move-by-capture.rs:3:37
|
LL | fn to_fn_mut<A:std::marker::Tuple,F:FnMut<A>>(f: F) -> F { f }
| ^^^^^^^^
help: consider cloning the value before moving it into the closure
|
LL ~ let value = bar.clone();
Expand Down
5 changes: 5 additions & 0 deletions tests/ui/borrowck/issue-103624.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ LL |
LL | self.b;
| ^^^^^^ `self.b` is moved here
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
--> $DIR/issue-103624.rs:7:36
|
LL | async fn spawn_blocking<T>(f: impl (Fn() -> T) + Send + Sync + 'static) -> T {
| ^^^^^^^^^^^
note: if `StructB` implemented `Clone`, you could clone the value
--> $DIR/issue-103624.rs:23:1
|
Expand Down
5 changes: 5 additions & 0 deletions tests/ui/borrowck/issue-87456-point-to-closure.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ LL |
LL | let _foo: String = val;
| ^^^ move occurs because `val` has type `String`, which does not implement the `Copy` trait
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
--> $DIR/issue-87456-point-to-closure.rs:3:24
|
LL | fn take_mut(_val: impl FnMut()) {}
| ^^^^^^^
help: consider borrowing here
|
LL | let _foo: String = &val;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ LL | y.into_iter();
| |
| move occurs because `y` has type `Vec<String>`, which does not implement the `Copy` trait
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
--> $DIR/unboxed-closures-move-upvar-from-non-once-ref-closure.rs:5:28
|
LL | fn call<F>(f: F) where F : Fn() {
| ^^^^
note: `into_iter` takes ownership of the receiver `self`, which moves `y`
--> $SRC_DIR/core/src/iter/traits/collect.rs:LL:COL
help: you can `clone` the value and consume it, but this might not be your desired behavior
Expand Down
1 change: 1 addition & 0 deletions tests/ui/issues/issue-4335.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ LL | id(Box::new(|| *v))
| |
| captured by this `FnMut` closure
|
= help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
help: if `T` implemented `Clone`, you could clone the value
--> $DIR/issue-4335.rs:5:10
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ LL | let _f = to_fn(|| test(i));
| |
| captured by this `Fn` closure
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
--> $DIR/moves-based-on-type-move-out-of-closure-env-issue-1965.rs:3:33
|
LL | fn to_fn<A:std::marker::Tuple,F:Fn<A>>(f: F) -> F { f }
| ^^^^^
help: consider cloning the value if the performance cost is acceptable
|
LL | let _f = to_fn(|| test(i.clone()));
Expand Down
5 changes: 5 additions & 0 deletions tests/ui/nll/issue-52663-span-decl-captured-variable.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ LL | expect_fn(|| drop(x.0));
| |
| captured by this `Fn` closure
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
--> $DIR/issue-52663-span-decl-captured-variable.rs:1:33
|
LL | fn expect_fn<F>(f: F) where F : Fn() {
| ^^^^
help: consider cloning the value if the performance cost is acceptable
|
LL | expect_fn(|| drop(x.0.clone()));
Expand Down
1 change: 1 addition & 0 deletions tests/ui/span/borrowck-call-is-borrow-issue-12224.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ LL |
LL | foo(f);
| ^ move occurs because `f` has type `{closure@$DIR/borrowck-call-is-borrow-issue-12224.rs:52:17: 52:58}`, which does not implement the `Copy` trait
|
= help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
help: consider cloning the value if the performance cost is acceptable
|
LL | foo(f.clone());
Expand Down
161 changes: 161 additions & 0 deletions tests/ui/suggestions/dont-suggest-ref/move-into-closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,55 @@ struct X(Y);
struct Y;

fn consume_fn<F: Fn()>(_f: F) { }
//~^ HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures

fn consume_fnmut<F: FnMut()>(_f: F) { }
//~^ HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures

trait T {
fn consume_fn<F: Fn()>(_f: F) { }
//~^ HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
fn method_consume_fn<F: Fn()>(&self, _f: F) { }
//~^ HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
//~| HELP `Fn` and `FnMut` closures
}
impl T for () {}

pub fn main() { }

Expand Down Expand Up @@ -73,6 +120,120 @@ fn move_into_fn() {
});
}

fn move_into_assoc_fn() {
let e = Either::One(X(Y));
let mut em = Either::One(X(Y));

let x = X(Y);

// move into Fn

<() as T>::consume_fn(|| {
let X(_t) = x;
//~^ ERROR cannot move
//~| HELP consider borrowing here
if let Either::One(_t) = e { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
while let Either::One(_t) = e { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
match e {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(_t)
| Either::Two(_t) => (),
}
match e {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(_t) => (),
Either::Two(ref _t) => (),
// FIXME: should suggest removing `ref` too
}

let X(mut _t) = x;
//~^ ERROR cannot move
//~| HELP consider borrowing here
if let Either::One(mut _t) = em { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
while let Either::One(mut _t) = em { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
match em {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(mut _t)
| Either::Two(mut _t) => (),
}
match em {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(mut _t) => (),
Either::Two(ref _t) => (),
// FIXME: should suggest removing `ref` too
}
});
}

fn move_into_method() {
let e = Either::One(X(Y));
let mut em = Either::One(X(Y));

let x = X(Y);

// move into Fn

().method_consume_fn(|| {
let X(_t) = x;
//~^ ERROR cannot move
//~| HELP consider borrowing here
if let Either::One(_t) = e { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
while let Either::One(_t) = e { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
match e {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(_t)
| Either::Two(_t) => (),
}
match e {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(_t) => (),
Either::Two(ref _t) => (),
// FIXME: should suggest removing `ref` too
}

let X(mut _t) = x;
//~^ ERROR cannot move
//~| HELP consider borrowing here
if let Either::One(mut _t) = em { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
while let Either::One(mut _t) = em { }
//~^ ERROR cannot move
//~| HELP consider borrowing here
match em {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(mut _t)
| Either::Two(mut _t) => (),
}
match em {
//~^ ERROR cannot move
//~| HELP consider borrowing here
Either::One(mut _t) => (),
Either::Two(ref _t) => (),
// FIXME: should suggest removing `ref` too
}
});
}

fn move_into_fnmut() {
let e = Either::One(X(Y));
let mut em = Either::One(X(Y));
Expand Down
Loading
Loading