rustc_hir_typeck/
loops.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4use Context::*;
5use rustc_hir as hir;
6use rustc_hir::attrs::AttributeKind;
7use rustc_hir::def::DefKind;
8use rustc_hir::def_id::LocalDefId;
9use rustc_hir::intravisit::{self, Visitor};
10use rustc_hir::{Destination, Node, find_attr};
11use rustc_middle::hir::nested_filter;
12use rustc_middle::span_bug;
13use rustc_middle::ty::TyCtxt;
14use rustc_span::hygiene::DesugaringKind;
15use rustc_span::{BytePos, Span};
16
17use crate::errors::{
18    BreakInsideClosure, BreakInsideCoroutine, BreakNonLoop, ConstContinueBadLabel,
19    ContinueLabeledBlock, OutsideLoop, OutsideLoopSuggestion, UnlabeledCfInWhileCondition,
20    UnlabeledInLabeledBlock,
21};
22
23/// The context in which a block is encountered.
24#[derive(Clone, Copy, Debug, PartialEq)]
25enum Context {
26    Normal,
27    Fn,
28    Loop(hir::LoopSource),
29    Closure(Span),
30    Coroutine {
31        coroutine_span: Span,
32        kind: hir::CoroutineDesugaring,
33        source: hir::CoroutineSource,
34    },
35    UnlabeledBlock(Span),
36    UnlabeledIfBlock(Span),
37    LabeledBlock,
38    /// E.g. The labeled block inside `['_'; 'block: { break 'block 1 + 2; }]`.
39    AnonConst,
40    /// E.g. `const { ... }`.
41    ConstBlock,
42    /// E.g. `#[loop_match] loop { state = 'label: { /* ... */ } }`.
43    LoopMatch {
44        /// The destination pointing to the labeled block (not to the loop itself).
45        labeled_block: Destination,
46    },
47}
48
49#[derive(Clone)]
50struct BlockInfo {
51    name: String,
52    spans: Vec<Span>,
53    suggs: Vec<Span>,
54}
55
56#[derive(PartialEq)]
57enum BreakContextKind {
58    Break,
59    Continue,
60}
61
62impl fmt::Display for BreakContextKind {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            BreakContextKind::Break => "break",
66            BreakContextKind::Continue => "continue",
67        }
68        .fmt(f)
69    }
70}
71
72#[derive(Clone)]
73struct CheckLoopVisitor<'tcx> {
74    tcx: TyCtxt<'tcx>,
75    // Keep track of a stack of contexts, so that suggestions
76    // are not made for contexts where it would be incorrect,
77    // such as adding a label for an `if`.
78    // e.g. `if 'foo: {}` would be incorrect.
79    cx_stack: Vec<Context>,
80    block_breaks: BTreeMap<Span, BlockInfo>,
81}
82
83pub(crate) fn check<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId, body: &'tcx hir::Body<'tcx>) {
84    let mut check =
85        CheckLoopVisitor { tcx, cx_stack: vec![Normal], block_breaks: Default::default() };
86    let cx = match tcx.def_kind(def_id) {
87        DefKind::AnonConst => AnonConst,
88        _ => Fn,
89    };
90    check.with_context(cx, |v| v.visit_body(body));
91    check.report_outside_loop_error();
92}
93
94impl<'hir> Visitor<'hir> for CheckLoopVisitor<'hir> {
95    type NestedFilter = nested_filter::OnlyBodies;
96
97    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
98        self.tcx
99    }
100
101    fn visit_anon_const(&mut self, _: &'hir hir::AnonConst) {
102        // Typecked on its own.
103    }
104
105    fn visit_inline_const(&mut self, c: &'hir hir::ConstBlock) {
106        self.with_context(ConstBlock, |v| intravisit::walk_inline_const(v, c));
107    }
108
109    fn visit_expr(&mut self, e: &'hir hir::Expr<'hir>) {
110        match e.kind {
111            hir::ExprKind::If(cond, then, else_opt) => {
112                self.visit_expr(cond);
113
114                let get_block = |ck_loop: &CheckLoopVisitor<'hir>,
115                                 expr: &hir::Expr<'hir>|
116                 -> Option<&hir::Block<'hir>> {
117                    if let hir::ExprKind::Block(b, None) = expr.kind
118                        && matches!(
119                            ck_loop.cx_stack.last(),
120                            Some(&Normal)
121                                | Some(&AnonConst)
122                                | Some(&UnlabeledBlock(_))
123                                | Some(&UnlabeledIfBlock(_))
124                        )
125                    {
126                        Some(b)
127                    } else {
128                        None
129                    }
130                };
131
132                if let Some(b) = get_block(self, then) {
133                    self.with_context(UnlabeledIfBlock(b.span.shrink_to_lo()), |v| {
134                        v.visit_block(b)
135                    });
136                } else {
137                    self.visit_expr(then);
138                }
139
140                if let Some(else_expr) = else_opt {
141                    if let Some(b) = get_block(self, else_expr) {
142                        self.with_context(UnlabeledIfBlock(b.span.shrink_to_lo()), |v| {
143                            v.visit_block(b)
144                        });
145                    } else {
146                        self.visit_expr(else_expr);
147                    }
148                }
149            }
150            hir::ExprKind::Loop(ref b, _, source, _) => {
151                let cx = match self.is_loop_match(e, b) {
152                    Some(labeled_block) => LoopMatch { labeled_block },
153                    None => Loop(source),
154                };
155
156                self.with_context(cx, |v| v.visit_block(b));
157            }
158            hir::ExprKind::Closure(&hir::Closure {
159                ref fn_decl, body, fn_decl_span, kind, ..
160            }) => {
161                let cx = match kind {
162                    hir::ClosureKind::Coroutine(hir::CoroutineKind::Desugared(kind, source)) => {
163                        Coroutine { coroutine_span: fn_decl_span, kind, source }
164                    }
165                    _ => Closure(fn_decl_span),
166                };
167                self.visit_fn_decl(fn_decl);
168                self.with_context(cx, |v| v.visit_nested_body(body));
169            }
170            hir::ExprKind::Block(ref b, Some(_label)) => {
171                self.with_context(LabeledBlock, |v| v.visit_block(b));
172            }
173            hir::ExprKind::Block(ref b, None)
174                if matches!(self.cx_stack.last(), Some(&Fn) | Some(&ConstBlock)) =>
175            {
176                self.with_context(Normal, |v| v.visit_block(b));
177            }
178            hir::ExprKind::Block(
179                ref b @ hir::Block { rules: hir::BlockCheckMode::DefaultBlock, .. },
180                None,
181            ) if matches!(
182                self.cx_stack.last(),
183                Some(&Normal) | Some(&AnonConst) | Some(&UnlabeledBlock(_))
184            ) =>
185            {
186                self.with_context(UnlabeledBlock(b.span.shrink_to_lo()), |v| v.visit_block(b));
187            }
188            hir::ExprKind::Break(break_destination, ref opt_expr) => {
189                if let Some(e) = opt_expr {
190                    self.visit_expr(e);
191                }
192
193                if self.require_label_in_labeled_block(e.span, &break_destination, "break") {
194                    // If we emitted an error about an unlabeled break in a labeled
195                    // block, we don't need any further checking for this break any more
196                    return;
197                }
198
199                let loop_id = match break_destination.target_id {
200                    Ok(loop_id) => Some(loop_id),
201                    Err(hir::LoopIdError::OutsideLoopScope) => None,
202                    Err(hir::LoopIdError::UnlabeledCfInWhileCondition) => {
203                        self.tcx.dcx().emit_err(UnlabeledCfInWhileCondition {
204                            span: e.span,
205                            cf_type: "break",
206                        });
207                        None
208                    }
209                    Err(hir::LoopIdError::UnresolvedLabel) => None,
210                };
211
212                // A `#[const_continue]` must break to a block in a `#[loop_match]`.
213                if find_attr!(self.tcx.hir_attrs(e.hir_id), AttributeKind::ConstContinue(_)) {
214                    let Some(label) = break_destination.label else {
215                        let span = e.span;
216                        self.tcx.dcx().emit_fatal(ConstContinueBadLabel { span });
217                    };
218
219                    let is_target_label = |cx: &Context| match cx {
220                        Context::LoopMatch { labeled_block } => {
221                            // NOTE: with macro expansion, the label's span might be different here
222                            // even though it does still refer to the same HIR node. A block
223                            // can't have two labels, so the hir_id is a unique identifier.
224                            assert!(labeled_block.target_id.is_ok()); // see `is_loop_match`.
225                            break_destination.target_id == labeled_block.target_id
226                        }
227                        _ => false,
228                    };
229
230                    if !self.cx_stack.iter().rev().any(is_target_label) {
231                        let span = label.ident.span;
232                        self.tcx.dcx().emit_fatal(ConstContinueBadLabel { span });
233                    }
234                }
235
236                if let Some(Node::Block(_)) = loop_id.map(|id| self.tcx.hir_node(id)) {
237                    return;
238                }
239
240                if let Some(break_expr) = opt_expr {
241                    let (head, loop_label, loop_kind) = if let Some(loop_id) = loop_id {
242                        match self.tcx.hir_expect_expr(loop_id).kind {
243                            hir::ExprKind::Loop(_, label, source, sp) => {
244                                (Some(sp), label, Some(source))
245                            }
246                            ref r => {
247                                span_bug!(e.span, "break label resolved to a non-loop: {:?}", r)
248                            }
249                        }
250                    } else {
251                        (None, None, None)
252                    };
253                    match loop_kind {
254                        None | Some(hir::LoopSource::Loop) => (),
255                        Some(kind) => {
256                            let suggestion = format!(
257                                "break{}",
258                                break_destination
259                                    .label
260                                    .map_or_else(String::new, |l| format!(" {}", l.ident))
261                            );
262                            self.tcx.dcx().emit_err(BreakNonLoop {
263                                span: e.span,
264                                head,
265                                kind: kind.name(),
266                                suggestion,
267                                loop_label,
268                                break_label: break_destination.label,
269                                break_expr_kind: &break_expr.kind,
270                                break_expr_span: break_expr.span,
271                            });
272                        }
273                    }
274                }
275
276                let sp_lo = e.span.with_lo(e.span.lo() + BytePos("break".len() as u32));
277                let label_sp = match break_destination.label {
278                    Some(label) => sp_lo.with_hi(label.ident.span.hi()),
279                    None => sp_lo.shrink_to_lo(),
280                };
281                self.require_break_cx(
282                    BreakContextKind::Break,
283                    e.span,
284                    label_sp,
285                    self.cx_stack.len() - 1,
286                );
287            }
288            hir::ExprKind::Continue(destination) => {
289                self.require_label_in_labeled_block(e.span, &destination, "continue");
290
291                match destination.target_id {
292                    Ok(loop_id) => {
293                        if let Node::Block(block) = self.tcx.hir_node(loop_id) {
294                            self.tcx.dcx().emit_err(ContinueLabeledBlock {
295                                span: e.span,
296                                block_span: block.span,
297                            });
298                        }
299                    }
300                    Err(hir::LoopIdError::UnlabeledCfInWhileCondition) => {
301                        self.tcx.dcx().emit_err(UnlabeledCfInWhileCondition {
302                            span: e.span,
303                            cf_type: "continue",
304                        });
305                    }
306                    Err(_) => {}
307                }
308                self.require_break_cx(
309                    BreakContextKind::Continue,
310                    e.span,
311                    e.span,
312                    self.cx_stack.len() - 1,
313                )
314            }
315            _ => intravisit::walk_expr(self, e),
316        }
317    }
318}
319
320impl<'hir> CheckLoopVisitor<'hir> {
321    fn with_context<F>(&mut self, cx: Context, f: F)
322    where
323        F: FnOnce(&mut CheckLoopVisitor<'hir>),
324    {
325        self.cx_stack.push(cx);
326        f(self);
327        self.cx_stack.pop();
328    }
329
330    fn require_break_cx(
331        &mut self,
332        br_cx_kind: BreakContextKind,
333        span: Span,
334        break_span: Span,
335        cx_pos: usize,
336    ) {
337        match self.cx_stack[cx_pos] {
338            LabeledBlock | Loop(_) | LoopMatch { .. } => {}
339            Closure(closure_span) => {
340                self.tcx.dcx().emit_err(BreakInsideClosure {
341                    span,
342                    closure_span,
343                    name: &br_cx_kind.to_string(),
344                });
345            }
346            Coroutine { coroutine_span, kind, source } => {
347                let kind = match kind {
348                    hir::CoroutineDesugaring::Async => "async",
349                    hir::CoroutineDesugaring::Gen => "gen",
350                    hir::CoroutineDesugaring::AsyncGen => "async gen",
351                };
352                let source = match source {
353                    hir::CoroutineSource::Block => "block",
354                    hir::CoroutineSource::Closure => "closure",
355                    hir::CoroutineSource::Fn => "function",
356                };
357                self.tcx.dcx().emit_err(BreakInsideCoroutine {
358                    span,
359                    coroutine_span,
360                    name: &br_cx_kind.to_string(),
361                    kind,
362                    source,
363                });
364            }
365            UnlabeledBlock(block_span)
366                if br_cx_kind == BreakContextKind::Break && block_span.eq_ctxt(break_span) =>
367            {
368                let block = self.block_breaks.entry(block_span).or_insert_with(|| BlockInfo {
369                    name: br_cx_kind.to_string(),
370                    spans: vec![],
371                    suggs: vec![],
372                });
373                block.spans.push(span);
374                block.suggs.push(break_span);
375            }
376            UnlabeledIfBlock(_) if br_cx_kind == BreakContextKind::Break => {
377                self.require_break_cx(br_cx_kind, span, break_span, cx_pos - 1);
378            }
379            Normal | AnonConst | Fn | UnlabeledBlock(_) | UnlabeledIfBlock(_) | ConstBlock => {
380                self.tcx.dcx().emit_err(OutsideLoop {
381                    spans: vec![span],
382                    name: &br_cx_kind.to_string(),
383                    is_break: br_cx_kind == BreakContextKind::Break,
384                    suggestion: None,
385                });
386            }
387        }
388    }
389
390    fn require_label_in_labeled_block(
391        &self,
392        span: Span,
393        label: &Destination,
394        cf_type: &str,
395    ) -> bool {
396        if !span.is_desugaring(DesugaringKind::QuestionMark)
397            && self.cx_stack.last() == Some(&LabeledBlock)
398            && label.label.is_none()
399        {
400            self.tcx.dcx().emit_err(UnlabeledInLabeledBlock { span, cf_type });
401            return true;
402        }
403        false
404    }
405
406    fn report_outside_loop_error(&self) {
407        for (s, block) in &self.block_breaks {
408            self.tcx.dcx().emit_err(OutsideLoop {
409                spans: block.spans.clone(),
410                name: &block.name,
411                is_break: true,
412                suggestion: Some(OutsideLoopSuggestion {
413                    block_span: *s,
414                    break_spans: block.suggs.clone(),
415                }),
416            });
417        }
418    }
419
420    /// Is this a loop annotated with `#[loop_match]` that looks syntactically sound?
421    fn is_loop_match(
422        &self,
423        e: &'hir hir::Expr<'hir>,
424        body: &'hir hir::Block<'hir>,
425    ) -> Option<Destination> {
426        if !find_attr!(self.tcx.hir_attrs(e.hir_id), AttributeKind::LoopMatch(_)) {
427            return None;
428        }
429
430        // NOTE: Diagnostics are emitted during MIR construction.
431
432        // Accept either `state = expr` or `state = expr;`.
433        let loop_body_expr = match body.stmts {
434            [] => match body.expr {
435                Some(expr) => expr,
436                None => return None,
437            },
438            [single] if body.expr.is_none() => match single.kind {
439                hir::StmtKind::Expr(expr) | hir::StmtKind::Semi(expr) => expr,
440                _ => return None,
441            },
442            [..] => return None,
443        };
444
445        let hir::ExprKind::Assign(_, rhs_expr, _) = loop_body_expr.kind else { return None };
446
447        let hir::ExprKind::Block(block, label) = rhs_expr.kind else { return None };
448
449        Some(Destination { label, target_id: Ok(block.hir_id) })
450    }
451}