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