rustc_hir_typeck/
loops.rs

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