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(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 { fn_decl, body, fn_decl_span, kind, .. }) => {
159                let cx = match kind {
160                    hir::ClosureKind::Coroutine(hir::CoroutineKind::Desugared(kind, source)) => {
161                        Coroutine { coroutine_span: fn_decl_span, kind, source }
162                    }
163                    _ => Closure(fn_decl_span),
164                };
165                self.visit_fn_decl(fn_decl);
166                self.with_context(cx, |v| v.visit_nested_body(body));
167            }
168            hir::ExprKind::Block(b, Some(_label)) => {
169                self.with_context(LabeledBlock, |v| v.visit_block(b));
170            }
171            hir::ExprKind::Block(b, None)
172                if matches!(self.cx_stack.last(), Some(&Fn) | Some(&ConstBlock)) =>
173            {
174                self.with_context(Normal, |v| v.visit_block(b));
175            }
176            hir::ExprKind::Block(
177                b @ hir::Block { rules: hir::BlockCheckMode::DefaultBlock, .. },
178                None,
179            ) if matches!(
180                self.cx_stack.last(),
181                Some(&Normal) | Some(&AnonConst) | Some(&UnlabeledBlock(_))
182            ) =>
183            {
184                self.with_context(UnlabeledBlock(b.span.shrink_to_lo()), |v| v.visit_block(b));
185            }
186            hir::ExprKind::Break(break_destination, ref opt_expr) => {
187                if let Some(e) = opt_expr {
188                    self.visit_expr(e);
189                }
190
191                if self.require_label_in_labeled_block(e.span, &break_destination, "break") {
192                    // If we emitted an error about an unlabeled break in a labeled
193                    // block, we don't need any further checking for this break any more
194                    return;
195                }
196
197                let loop_id = match break_destination.target_id {
198                    Ok(loop_id) => Some(loop_id),
199                    Err(hir::LoopIdError::OutsideLoopScope) => None,
200                    Err(hir::LoopIdError::UnlabeledCfInWhileCondition) => {
201                        self.tcx.dcx().emit_err(UnlabeledCfInWhileCondition {
202                            span: e.span,
203                            cf_type: "break",
204                        });
205                        None
206                    }
207                    Err(hir::LoopIdError::UnresolvedLabel) => None,
208                };
209
210                // A `#[const_continue]` must break to a block in a `#[loop_match]`.
211                if find_attr!(self.tcx.hir_attrs(e.hir_id), AttributeKind::ConstContinue(_)) {
212                    let Some(label) = break_destination.label else {
213                        let span = e.span;
214                        self.tcx.dcx().emit_fatal(ConstContinueBadLabel { span });
215                    };
216
217                    let is_target_label = |cx: &Context| match cx {
218                        Context::LoopMatch { labeled_block } => {
219                            // NOTE: with macro expansion, the label's span might be different here
220                            // even though it does still refer to the same HIR node. A block
221                            // can't have two labels, so the hir_id is a unique identifier.
222                            assert!(labeled_block.target_id.is_ok()); // see `is_loop_match`.
223                            break_destination.target_id == labeled_block.target_id
224                        }
225                        _ => false,
226                    };
227
228                    if !self.cx_stack.iter().rev().any(is_target_label) {
229                        let span = label.ident.span;
230                        self.tcx.dcx().emit_fatal(ConstContinueBadLabel { span });
231                    }
232                }
233
234                if let Some(Node::Block(_)) = loop_id.map(|id| self.tcx.hir_node(id)) {
235                    return;
236                }
237
238                if let Some(break_expr) = opt_expr {
239                    let (head, loop_label, loop_kind) = if let Some(loop_id) = loop_id {
240                        match self.tcx.hir_expect_expr(loop_id).kind {
241                            hir::ExprKind::Loop(_, label, source, sp) => {
242                                (Some(sp), label, Some(source))
243                            }
244                            ref r => {
245                                span_bug!(e.span, "break label resolved to a non-loop: {:?}", r)
246                            }
247                        }
248                    } else {
249                        (None, None, None)
250                    };
251                    match loop_kind {
252                        None | Some(hir::LoopSource::Loop) => (),
253                        Some(kind) => {
254                            let suggestion = format!(
255                                "break{}",
256                                break_destination
257                                    .label
258                                    .map_or_else(String::new, |l| format!(" {}", l.ident))
259                            );
260                            self.tcx.dcx().emit_err(BreakNonLoop {
261                                span: e.span,
262                                head,
263                                kind: kind.name(),
264                                suggestion,
265                                loop_label,
266                                break_label: break_destination.label,
267                                break_expr_kind: &break_expr.kind,
268                                break_expr_span: break_expr.span,
269                            });
270                        }
271                    }
272                }
273
274                let sp_lo = e.span.with_lo(e.span.lo() + BytePos("break".len() as u32));
275                let label_sp = match break_destination.label {
276                    Some(label) => sp_lo.with_hi(label.ident.span.hi()),
277                    None => sp_lo.shrink_to_lo(),
278                };
279                self.require_break_cx(
280                    BreakContextKind::Break,
281                    e.span,
282                    label_sp,
283                    self.cx_stack.len() - 1,
284                );
285            }
286            hir::ExprKind::Continue(destination) => {
287                self.require_label_in_labeled_block(e.span, &destination, "continue");
288
289                match destination.target_id {
290                    Ok(loop_id) => {
291                        if let Node::Block(block) = self.tcx.hir_node(loop_id) {
292                            self.tcx.dcx().emit_err(ContinueLabeledBlock {
293                                span: e.span,
294                                block_span: block.span,
295                            });
296                        }
297                    }
298                    Err(hir::LoopIdError::UnlabeledCfInWhileCondition) => {
299                        self.tcx.dcx().emit_err(UnlabeledCfInWhileCondition {
300                            span: e.span,
301                            cf_type: "continue",
302                        });
303                    }
304                    Err(_) => {}
305                }
306                self.require_break_cx(
307                    BreakContextKind::Continue,
308                    e.span,
309                    e.span,
310                    self.cx_stack.len() - 1,
311                )
312            }
313            _ => intravisit::walk_expr(self, e),
314        }
315    }
316}
317
318impl<'hir> CheckLoopVisitor<'hir> {
319    fn with_context<F>(&mut self, cx: Context, f: F)
320    where
321        F: FnOnce(&mut CheckLoopVisitor<'hir>),
322    {
323        self.cx_stack.push(cx);
324        f(self);
325        self.cx_stack.pop();
326    }
327
328    fn require_break_cx(
329        &mut self,
330        br_cx_kind: BreakContextKind,
331        span: Span,
332        break_span: Span,
333        cx_pos: usize,
334    ) {
335        match self.cx_stack[cx_pos] {
336            LabeledBlock | Loop(_) | LoopMatch { .. } => {}
337            Closure(closure_span) => {
338                self.tcx.dcx().emit_err(BreakInsideClosure {
339                    span,
340                    closure_span,
341                    name: &br_cx_kind.to_string(),
342                });
343            }
344            Coroutine { coroutine_span, kind, source } => {
345                let kind = match kind {
346                    hir::CoroutineDesugaring::Async => "async",
347                    hir::CoroutineDesugaring::Gen => "gen",
348                    hir::CoroutineDesugaring::AsyncGen => "async gen",
349                };
350                let source = match source {
351                    hir::CoroutineSource::Block => "block",
352                    hir::CoroutineSource::Closure => "closure",
353                    hir::CoroutineSource::Fn => "function",
354                };
355                self.tcx.dcx().emit_err(BreakInsideCoroutine {
356                    span,
357                    coroutine_span,
358                    name: &br_cx_kind.to_string(),
359                    kind,
360                    source,
361                });
362            }
363            UnlabeledBlock(block_span)
364                if br_cx_kind == BreakContextKind::Break && block_span.eq_ctxt(break_span) =>
365            {
366                let block = self.block_breaks.entry(block_span).or_insert_with(|| BlockInfo {
367                    name: br_cx_kind.to_string(),
368                    spans: vec![],
369                    suggs: vec![],
370                });
371                block.spans.push(span);
372                block.suggs.push(break_span);
373            }
374            UnlabeledIfBlock(_) if br_cx_kind == BreakContextKind::Break => {
375                self.require_break_cx(br_cx_kind, span, break_span, cx_pos - 1);
376            }
377            Normal | AnonConst | Fn | UnlabeledBlock(_) | UnlabeledIfBlock(_) | ConstBlock => {
378                self.tcx.dcx().emit_err(OutsideLoop {
379                    spans: vec![span],
380                    name: &br_cx_kind.to_string(),
381                    is_break: br_cx_kind == BreakContextKind::Break,
382                    suggestion: None,
383                });
384            }
385        }
386    }
387
388    fn require_label_in_labeled_block(
389        &self,
390        span: Span,
391        label: &Destination,
392        cf_type: &str,
393    ) -> bool {
394        if !span.is_desugaring(DesugaringKind::QuestionMark)
395            && self.cx_stack.last() == Some(&LabeledBlock)
396            && label.label.is_none()
397        {
398            self.tcx.dcx().emit_err(UnlabeledInLabeledBlock { span, cf_type });
399            return true;
400        }
401        false
402    }
403
404    fn report_outside_loop_error(&self) {
405        for (s, block) in &self.block_breaks {
406            self.tcx.dcx().emit_err(OutsideLoop {
407                spans: block.spans.clone(),
408                name: &block.name,
409                is_break: true,
410                suggestion: Some(OutsideLoopSuggestion {
411                    block_span: *s,
412                    break_spans: block.suggs.clone(),
413                }),
414            });
415        }
416    }
417
418    /// Is this a loop annotated with `#[loop_match]` that looks syntactically sound?
419    fn is_loop_match(
420        &self,
421        e: &'hir hir::Expr<'hir>,
422        body: &'hir hir::Block<'hir>,
423    ) -> Option<Destination> {
424        if !find_attr!(self.tcx.hir_attrs(e.hir_id), AttributeKind::LoopMatch(_)) {
425            return None;
426        }
427
428        // NOTE: Diagnostics are emitted during MIR construction.
429
430        // Accept either `state = expr` or `state = expr;`.
431        let loop_body_expr = match body.stmts {
432            [] => body.expr?,
433            [single] if body.expr.is_none() => match single.kind {
434                hir::StmtKind::Expr(expr) | hir::StmtKind::Semi(expr) => expr,
435                _ => return None,
436            },
437            [..] => return None,
438        };
439
440        let hir::ExprKind::Assign(_, rhs_expr, _) = loop_body_expr.kind else { return None };
441
442        let hir::ExprKind::Block(block, label) = rhs_expr.kind else { return None };
443
444        Some(Destination { label, target_id: Ok(block.hir_id) })
445    }
446}