Skip to main content

rustc_hir_typeck/
loops.rs

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