Skip to content

Commit 5608c7f

Browse files
committed
Auto merge of rust-lang#121652 - estebank:move-in-loop-break-condition, r=Nadrieril
Detect when move of !Copy value occurs within loop and should likely not be cloned When encountering a move error on a value within a loop of any kind, identify if the moved value belongs to a call expression that should not be cloned and avoid the semantically incorrect suggestion. Also try to suggest moving the call expression outside of the loop instead. ``` error[E0382]: use of moved value: `vec` --> $DIR/recreating-value-in-loop-condition.rs:6:33 | LL | let vec = vec!["one", "two", "three"]; | --- move occurs because `vec` has type `Vec<&str>`, which does not implement the `Copy` trait LL | while let Some(item) = iter(vec).next() { | ----------------------------^^^-------- | | | | | value moved here, in previous iteration of loop | inside of this loop | note: consider changing this parameter type in function `iter` to borrow instead if owning the value isn't necessary --> $DIR/recreating-value-in-loop-condition.rs:1:17 | LL | fn iter<T>(vec: Vec<T>) -> impl Iterator<Item = T> { | ---- ^^^^^^ this parameter takes ownership of the value | | | in this function help: consider moving the expression out of the loop so it is only moved once | LL ~ let mut value = iter(vec); LL ~ while let Some(item) = value.next() { | ``` We use the presence of a `break` in the loop that would be affected by the moved value as a heuristic for "shouldn't be cloned". Fix rust-lang#121466. --- *Point at continue and break that might be in the wrong place* Sometimes move errors are because of a misplaced `continue`, but we didn't surface that anywhere. Now when there are more than one set of nested loops we show them out and point at the `continue` and `break` expressions within that might need to go elsewhere. ``` error[E0382]: use of moved value: `foo` --> $DIR/nested-loop-moved-value-wrong-continue.rs:46:18 | LL | for foo in foos { | --- | | | this reinitialization might get skipped | move occurs because `foo` has type `String`, which does not implement the `Copy` trait ... LL | for bar in &bars { | ---------------- inside of this loop ... LL | baz.push(foo); | --- value moved here, in previous iteration of loop ... LL | qux.push(foo); | ^^^ value used here after move | note: verify that your loop breaking logic is correct --> $DIR/nested-loop-moved-value-wrong-continue.rs:41:17 | LL | for foo in foos { | --------------- ... LL | for bar in &bars { | ---------------- ... LL | continue; | ^^^^^^^^ this `continue` advances the loop at line 33 help: consider moving the expression out of the loop so it is only moved once | LL ~ let mut value = baz.push(foo); LL ~ for bar in &bars { LL | ... LL | if foo == *bar { LL ~ value; | help: consider cloning the value if the performance cost is acceptable | LL | baz.push(foo.clone()); | ++++++++ ``` Fix rust-lang#92531.
2 parents 62f98b4 + 3b237d7 commit 5608c7f

9 files changed

+620
-43
lines changed

compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs

+256-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use rustc_data_structures::fx::FxIndexSet;
99
use rustc_errors::{codes::*, struct_span_code_err, Applicability, Diag, MultiSpan};
1010
use rustc_hir as hir;
1111
use rustc_hir::def::{DefKind, Res};
12-
use rustc_hir::intravisit::{walk_block, walk_expr, Visitor};
12+
use rustc_hir::intravisit::{walk_block, walk_expr, Map, Visitor};
1313
use rustc_hir::{CoroutineDesugaring, PatField};
1414
use rustc_hir::{CoroutineKind, CoroutineSource, LangItem};
1515
use rustc_middle::hir::nested_filter::OnlyBodies;
@@ -447,8 +447,8 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
447447
err.span_note(
448448
span,
449449
format!(
450-
"consider changing this parameter type in {descr} `{ident}` to \
451-
borrow instead if owning the value isn't necessary",
450+
"consider changing this parameter type in {descr} `{ident}` to borrow \
451+
instead if owning the value isn't necessary",
452452
),
453453
);
454454
}
@@ -463,7 +463,9 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
463463
} else if let UseSpans::FnSelfUse { kind: CallKind::Normal { .. }, .. } = move_spans
464464
{
465465
// We already suggest cloning for these cases in `explain_captures`.
466-
} else {
466+
} else if self.suggest_hoisting_call_outside_loop(err, expr) {
467+
// The place where the the type moves would be misleading to suggest clone.
468+
// #121466
467469
self.suggest_cloning(err, ty, expr, move_span);
468470
}
469471
}
@@ -747,6 +749,234 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
747749
true
748750
}
749751

752+
/// In a move error that occurs on a call wihtin a loop, we try to identify cases where cloning
753+
/// the value would lead to a logic error. We infer these cases by seeing if the moved value is
754+
/// part of the logic to break the loop, either through an explicit `break` or if the expression
755+
/// is part of a `while let`.
756+
fn suggest_hoisting_call_outside_loop(&self, err: &mut Diag<'_>, expr: &hir::Expr<'_>) -> bool {
757+
let tcx = self.infcx.tcx;
758+
let mut can_suggest_clone = true;
759+
760+
// If the moved value is a locally declared binding, we'll look upwards on the expression
761+
// tree until the scope where it is defined, and no further, as suggesting to move the
762+
// expression beyond that point would be illogical.
763+
let local_hir_id = if let hir::ExprKind::Path(hir::QPath::Resolved(
764+
_,
765+
hir::Path { res: hir::def::Res::Local(local_hir_id), .. },
766+
)) = expr.kind
767+
{
768+
Some(local_hir_id)
769+
} else {
770+
// This case would be if the moved value comes from an argument binding, we'll just
771+
// look within the entire item, that's fine.
772+
None
773+
};
774+
775+
/// This will allow us to look for a specific `HirId`, in our case `local_hir_id` where the
776+
/// binding was declared, within any other expression. We'll use it to search for the
777+
/// binding declaration within every scope we inspect.
778+
struct Finder {
779+
hir_id: hir::HirId,
780+
found: bool,
781+
}
782+
impl<'hir> Visitor<'hir> for Finder {
783+
fn visit_pat(&mut self, pat: &'hir hir::Pat<'hir>) {
784+
if pat.hir_id == self.hir_id {
785+
self.found = true;
786+
}
787+
hir::intravisit::walk_pat(self, pat);
788+
}
789+
fn visit_expr(&mut self, ex: &'hir hir::Expr<'hir>) {
790+
if ex.hir_id == self.hir_id {
791+
self.found = true;
792+
}
793+
hir::intravisit::walk_expr(self, ex);
794+
}
795+
}
796+
// The immediate HIR parent of the moved expression. We'll look for it to be a call.
797+
let mut parent = None;
798+
// The top-most loop where the moved expression could be moved to a new binding.
799+
let mut outer_most_loop: Option<&hir::Expr<'_>> = None;
800+
for (_, node) in tcx.hir().parent_iter(expr.hir_id) {
801+
let e = match node {
802+
hir::Node::Expr(e) => e,
803+
hir::Node::Local(hir::Local { els: Some(els), .. }) => {
804+
let mut finder = BreakFinder { found_breaks: vec![], found_continues: vec![] };
805+
finder.visit_block(els);
806+
if !finder.found_breaks.is_empty() {
807+
// Don't suggest clone as it could be will likely end in an infinite
808+
// loop.
809+
// let Some(_) = foo(non_copy.clone()) else { break; }
810+
// --- ^^^^^^^^ -----
811+
can_suggest_clone = false;
812+
}
813+
continue;
814+
}
815+
_ => continue,
816+
};
817+
if let Some(&hir_id) = local_hir_id {
818+
let mut finder = Finder { hir_id, found: false };
819+
finder.visit_expr(e);
820+
if finder.found {
821+
// The current scope includes the declaration of the binding we're accessing, we
822+
// can't look up any further for loops.
823+
break;
824+
}
825+
}
826+
if parent.is_none() {
827+
parent = Some(e);
828+
}
829+
match e.kind {
830+
hir::ExprKind::Let(_) => {
831+
match tcx.parent_hir_node(e.hir_id) {
832+
hir::Node::Expr(hir::Expr {
833+
kind: hir::ExprKind::If(cond, ..), ..
834+
}) => {
835+
let mut finder = Finder { hir_id: expr.hir_id, found: false };
836+
finder.visit_expr(cond);
837+
if finder.found {
838+
// The expression where the move error happened is in a `while let`
839+
// condition Don't suggest clone as it will likely end in an
840+
// infinite loop.
841+
// while let Some(_) = foo(non_copy.clone()) { }
842+
// --------- ^^^^^^^^
843+
can_suggest_clone = false;
844+
}
845+
}
846+
_ => {}
847+
}
848+
}
849+
hir::ExprKind::Loop(..) => {
850+
outer_most_loop = Some(e);
851+
}
852+
_ => {}
853+
}
854+
}
855+
let loop_count: usize = tcx
856+
.hir()
857+
.parent_iter(expr.hir_id)
858+
.map(|(_, node)| match node {
859+
hir::Node::Expr(hir::Expr { kind: hir::ExprKind::Loop(..), .. }) => 1,
860+
_ => 0,
861+
})
862+
.sum();
863+
864+
let sm = tcx.sess.source_map();
865+
if let Some(in_loop) = outer_most_loop {
866+
let mut finder = BreakFinder { found_breaks: vec![], found_continues: vec![] };
867+
finder.visit_expr(in_loop);
868+
// All of the spans for `break` and `continue` expressions.
869+
let spans = finder
870+
.found_breaks
871+
.iter()
872+
.chain(finder.found_continues.iter())
873+
.map(|(_, span)| *span)
874+
.filter(|span| {
875+
!matches!(
876+
span.desugaring_kind(),
877+
Some(DesugaringKind::ForLoop | DesugaringKind::WhileLoop)
878+
)
879+
})
880+
.collect::<Vec<Span>>();
881+
// All of the spans for the loops above the expression with the move error.
882+
let loop_spans: Vec<_> = tcx
883+
.hir()
884+
.parent_iter(expr.hir_id)
885+
.filter_map(|(_, node)| match node {
886+
hir::Node::Expr(hir::Expr { span, kind: hir::ExprKind::Loop(..), .. }) => {
887+
Some(*span)
888+
}
889+
_ => None,
890+
})
891+
.collect();
892+
// It is possible that a user written `break` or `continue` is in the wrong place. We
893+
// point them out at the user for them to make a determination. (#92531)
894+
if !spans.is_empty() && loop_count > 1 {
895+
// Getting fancy: if the spans of the loops *do not* overlap, we only use the line
896+
// number when referring to them. If there *are* overlaps (multiple loops on the
897+
// same line) then we use the more verbose span output (`file.rs:col:ll`).
898+
let mut lines: Vec<_> =
899+
loop_spans.iter().map(|sp| sm.lookup_char_pos(sp.lo()).line).collect();
900+
lines.sort();
901+
lines.dedup();
902+
let fmt_span = |span: Span| {
903+
if lines.len() == loop_spans.len() {
904+
format!("line {}", sm.lookup_char_pos(span.lo()).line)
905+
} else {
906+
sm.span_to_diagnostic_string(span)
907+
}
908+
};
909+
let mut spans: MultiSpan = spans.clone().into();
910+
// Point at all the `continue`s and explicit `break`s in the relevant loops.
911+
for (desc, elements) in [
912+
("`break` exits", &finder.found_breaks),
913+
("`continue` advances", &finder.found_continues),
914+
] {
915+
for (destination, sp) in elements {
916+
if let Ok(hir_id) = destination.target_id
917+
&& let hir::Node::Expr(expr) = tcx.hir().hir_node(hir_id)
918+
&& !matches!(
919+
sp.desugaring_kind(),
920+
Some(DesugaringKind::ForLoop | DesugaringKind::WhileLoop)
921+
)
922+
{
923+
spans.push_span_label(
924+
*sp,
925+
format!("this {desc} the loop at {}", fmt_span(expr.span)),
926+
);
927+
}
928+
}
929+
}
930+
// Point at all the loops that are between this move and the parent item.
931+
for span in loop_spans {
932+
spans.push_span_label(sm.guess_head_span(span), "");
933+
}
934+
935+
// note: verify that your loop breaking logic is correct
936+
// --> $DIR/nested-loop-moved-value-wrong-continue.rs:41:17
937+
// |
938+
// 28 | for foo in foos {
939+
// | ---------------
940+
// ...
941+
// 33 | for bar in &bars {
942+
// | ----------------
943+
// ...
944+
// 41 | continue;
945+
// | ^^^^^^^^ this `continue` advances the loop at line 33
946+
err.span_note(spans, "verify that your loop breaking logic is correct");
947+
}
948+
if let Some(parent) = parent
949+
&& let hir::ExprKind::MethodCall(..) | hir::ExprKind::Call(..) = parent.kind
950+
{
951+
// FIXME: We could check that the call's *parent* takes `&mut val` to make the
952+
// suggestion more targeted to the `mk_iter(val).next()` case. Maybe do that only to
953+
// check for wheter to suggest `let value` or `let mut value`.
954+
955+
let span = in_loop.span;
956+
if !finder.found_breaks.is_empty()
957+
&& let Ok(value) = sm.span_to_snippet(parent.span)
958+
{
959+
// We know with high certainty that this move would affect the early return of a
960+
// loop, so we suggest moving the expression with the move out of the loop.
961+
let indent = if let Some(indent) = sm.indentation_before(span) {
962+
format!("\n{indent}")
963+
} else {
964+
" ".to_string()
965+
};
966+
err.multipart_suggestion(
967+
"consider moving the expression out of the loop so it is only moved once",
968+
vec![
969+
(parent.span, "value".to_string()),
970+
(span.shrink_to_lo(), format!("let mut value = {value};{indent}")),
971+
],
972+
Applicability::MaybeIncorrect,
973+
);
974+
}
975+
}
976+
}
977+
can_suggest_clone
978+
}
979+
750980
fn suggest_cloning(&self, err: &mut Diag<'_>, ty: Ty<'tcx>, expr: &hir::Expr<'_>, span: Span) {
751981
let tcx = self.infcx.tcx;
752982
// Try to find predicates on *generic params* that would allow copying `ty`
@@ -3688,6 +3918,28 @@ impl<'a, 'v> Visitor<'v> for ReferencedStatementsVisitor<'a> {
36883918
}
36893919
}
36903920

3921+
/// Look for `break` expressions within any arbitrary expressions. We'll do this to infer
3922+
/// whether this is a case where the moved value would affect the exit of a loop, making it
3923+
/// unsuitable for a `.clone()` suggestion.
3924+
struct BreakFinder {
3925+
found_breaks: Vec<(hir::Destination, Span)>,
3926+
found_continues: Vec<(hir::Destination, Span)>,
3927+
}
3928+
impl<'hir> Visitor<'hir> for BreakFinder {
3929+
fn visit_expr(&mut self, ex: &'hir hir::Expr<'hir>) {
3930+
match ex.kind {
3931+
hir::ExprKind::Break(destination, _) => {
3932+
self.found_breaks.push((destination, ex.span));
3933+
}
3934+
hir::ExprKind::Continue(destination) => {
3935+
self.found_continues.push((destination, ex.span));
3936+
}
3937+
_ => {}
3938+
}
3939+
hir::intravisit::walk_expr(self, ex);
3940+
}
3941+
}
3942+
36913943
/// Given a set of spans representing statements initializing the relevant binding, visit all the
36923944
/// function expressions looking for branching code paths that *do not* initialize the binding.
36933945
struct ConditionVisitor<'b> {

compiler/rustc_hir/src/hir.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2049,7 +2049,7 @@ impl LoopSource {
20492049
}
20502050
}
20512051

2052-
#[derive(Copy, Clone, Debug, HashStable_Generic)]
2052+
#[derive(Copy, Clone, Debug, PartialEq, HashStable_Generic)]
20532053
pub enum LoopIdError {
20542054
OutsideLoopScope,
20552055
UnlabeledCfInWhileCondition,

tests/ui/borrowck/mut-borrow-in-loop-2.fixed

-35
This file was deleted.

tests/ui/borrowck/mut-borrow-in-loop-2.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
//@ run-rustfix
21
#![allow(dead_code)]
32

43
struct Events<R>(R);

tests/ui/borrowck/mut-borrow-in-loop-2.stderr

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error[E0382]: use of moved value: `value`
2-
--> $DIR/mut-borrow-in-loop-2.rs:31:23
2+
--> $DIR/mut-borrow-in-loop-2.rs:30:23
33
|
44
LL | fn this_does_not<'a, R>(value: &'a mut Events<R>) {
55
| ----- move occurs because `value` has type `&mut Events<R>`, which does not implement the `Copy` trait
@@ -9,12 +9,18 @@ LL | Other::handle(value);
99
| ^^^^^ value moved here, in previous iteration of loop
1010
|
1111
note: consider changing this parameter type in function `handle` to borrow instead if owning the value isn't necessary
12-
--> $DIR/mut-borrow-in-loop-2.rs:9:22
12+
--> $DIR/mut-borrow-in-loop-2.rs:8:22
1313
|
1414
LL | fn handle(value: T) -> Self;
1515
| ------ ^ this parameter takes ownership of the value
1616
| |
1717
| in this function
18+
help: consider moving the expression out of the loop so it is only moved once
19+
|
20+
LL ~ let mut value = Other::handle(value);
21+
LL ~ for _ in 0..3 {
22+
LL ~ value;
23+
|
1824
help: consider creating a fresh reborrow of `value` here
1925
|
2026
LL | Other::handle(&mut *value);

0 commit comments

Comments
 (0)