Skip to main content

charon_lib/transform/resugar/
reconstruct_fallible_operations.rs

1//! # Micro-pass: remove the dynamic checks for array/slice bounds, overflow, and division by zero.
2//! Note that from a semantic point of view, an out-of-bound access or a division by zero
3//! must lead to a panic in Rust (which is why those checks are always present, even when
4//! compiling for release). In our case, we take this into account in the semantics of our
5//! array/slice manipulation and arithmetic functions, on the verification side.
6
7use std::collections::HashSet;
8
9use crate::transform::TransformCtx;
10use crate::transform::ctx::UllbcPass;
11use crate::ullbc_ast::*;
12
13type LocalUses = IndexVec<BlockId, HashSet<LocalId>>;
14
15/// Compute for each block the locals that are assumed to have been initialized/defined before entering it.
16fn compute_uses(body: &ExprBody) -> LocalUses {
17    #[derive(Visitor)]
18    struct UsedLocalsVisitor<'a>(&'a mut HashSet<LocalId>);
19
20    impl VisitBody for UsedLocalsVisitor<'_> {
21        fn visit_place(&mut self, x: &Place) -> ::std::ops::ControlFlow<Self::Break> {
22            if let Some(local_id) = x.as_local() {
23                self.0.insert(local_id);
24            }
25            self.visit_inner(x)
26        }
27    }
28
29    body.body.map_ref(|block| {
30        let mut uses = HashSet::new();
31        let mut visitor = UsedLocalsVisitor(&mut uses);
32
33        // do a simple live variable analysis by walking the block backwards
34        for statement in block.statements.iter().rev() {
35            match &statement.kind {
36                StatementKind::Assign(place, rval) => {
37                    // We clear the assigned place, but it may be re-added
38                    // if it's used in rval
39                    if let Some(local_id) = place.as_local() {
40                        visitor.0.remove(&local_id);
41                    }
42                    let _ = rval.drive_body(&mut visitor);
43                }
44                StatementKind::StorageLive(local) | StatementKind::StorageDead(local) => {
45                    // A `StorageLive` re-sets the local to be uninitialised,
46                    // so any usage after this point doesn't matter
47                    // Similarly, a `StorageDead` means the local is de-initialised,
48                    // so we can ignore any usage after this point
49                    visitor.0.remove(local);
50                }
51                _ => {
52                    let _ = statement.drive_body(&mut visitor);
53                }
54            }
55        }
56
57        uses
58    })
59}
60
61/// Whether the value uses the given local in a place.
62fn uses_local<T: BodyVisitable>(x: &T, local: LocalId) -> bool {
63    struct FoundIt;
64    struct UsesLocalVisitor(LocalId);
65
66    impl Visitor for UsesLocalVisitor {
67        type Break = FoundIt;
68    }
69    impl VisitBody for UsesLocalVisitor {
70        fn visit_place(&mut self, x: &Place) -> ::std::ops::ControlFlow<Self::Break> {
71            if let Some(local_id) = x.as_local()
72                && local_id == self.0
73            {
74                return ControlFlow::Break(FoundIt);
75            }
76            self.visit_inner(x)
77        }
78
79        fn visit_ullbc_statement(
80            &mut self,
81            x: &ullbc_ast::Statement,
82        ) -> ::std::ops::ControlFlow<Self::Break> {
83            match x.kind {
84                StatementKind::StorageDead(_) | StatementKind::StorageLive(_) => {
85                    ControlFlow::Continue(())
86                }
87                _ => self.visit_inner(x),
88            }
89        }
90    }
91
92    x.drive_body(&mut UsesLocalVisitor(local)).is_break()
93}
94
95fn make_binop_overflow_panic<T: BodyVisitable>(
96    x: &mut [T],
97    matches: impl Fn(&BinOp, &Operand, &Operand) -> bool,
98) -> bool {
99    let mut found = false;
100    for y in x.iter_mut() {
101        y.dyn_visit_in_body_mut(|rv: &mut Rvalue| {
102            if let Rvalue::BinaryOp(binop, op_l, op_r) = rv
103                && matches(binop, op_l, op_r)
104            {
105                *binop = binop.with_overflow(OverflowMode::Panic);
106                found = true;
107            }
108        });
109    }
110    found
111}
112
113fn make_unop_overflow_panic<T: BodyVisitable>(
114    x: &mut [T],
115    matches: impl Fn(&UnOp, &Operand) -> bool,
116) -> bool {
117    let mut found = false;
118    for y in x.iter_mut() {
119        y.dyn_visit_in_body_mut(|rv: &mut Rvalue| {
120            if let Rvalue::UnaryOp(unop, op) = rv
121                && matches(unop, op)
122            {
123                *unop = unop.with_overflow(OverflowMode::Panic);
124                found = true;
125            }
126        });
127    }
128    found
129}
130
131/// Check if the two operands are equivalent: either they're the same constant, or they represent
132/// the same place (regardless of whether the operand is a move or a copy)
133fn equiv_op(op_l: &Operand, op_r: &Operand) -> bool {
134    match (op_l, op_r) {
135        (Operand::Copy(l) | Operand::Move(l), Operand::Copy(r) | Operand::Move(r)) => l == r,
136        (Operand::Const(l), Operand::Const(r)) => l == r,
137        _ => false,
138    }
139}
140
141/// A shift overflow check whose shift operation is not in the same block: rustc evaluates the
142/// shift operands (which is when it emits the overflow check) before computing the destination
143/// place, so when that computation involves a function call (e.g. `out[i] = x >> y` through an
144/// overloaded `IndexMut`), the check ends up separated from the shift by the call terminator.
145/// See issue #1041. These asserts are only ever emitted by the compiler, so instead of following
146/// the control flow we record the check and find the matching shift over the whole body in
147/// [resolve_pending_shift_check].
148struct PendingShiftCheck {
149    /// The block containing the check.
150    block: BlockId,
151    /// The checked shift amount; used to find the shift.
152    amount: Operand,
153    /// The comparison result asserted by the check. Identifies the check's statements to remove.
154    cond_local: LocalId,
155    /// The `y as uN` cast temporary, when the amount is cast before the comparison; also removed.
156    cast_local: Option<LocalId>,
157}
158
159/// Rustc inserts dynamic checks during MIR lowering. They all end in an `Assert` statement (and
160/// this is the only use of this statement).
161fn remove_dynamic_checks(
162    ctx: &mut TransformCtx,
163    uses: &LocalUses,
164    block_id: BlockId,
165    locals: &mut Locals,
166    statements: &mut [Statement],
167    pending_shift_checks: &mut Vec<PendingShiftCheck>,
168) {
169    // Whether this local was used in another block
170    let used_outside_block = |local: LocalId| {
171        uses.iter_enumerated()
172            .any(|(bid, used)| bid != block_id && used.contains(&local))
173    };
174
175    // We return the statements we want to keep, which must be a prefix of `block.statements`.
176    let statements_to_keep = match statements {
177        // Bounds checks for slices. They look like:
178        //   l := use(copy a.metadata)
179        //   b := copy x < copy l
180        //   assert(move b == true)
181        [
182            Statement {
183                kind: StatementKind::Assign(len, Rvalue::Use(Operand::Copy(len_op), _)),
184                ..
185            },
186            Statement {
187                kind:
188                    StatementKind::Assign(
189                        is_in_bounds,
190                        Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
191                    ),
192                ..
193            },
194            Statement {
195                kind:
196                    StatementKind::Assert {
197                        assert:
198                            Assert {
199                                cond: Operand::Move(cond),
200                                expected: true,
201                                ..
202                            },
203                        ..
204                    },
205                ..
206            },
207            rest @ ..,
208        ] if lt_op2 == len
209            && cond == is_in_bounds
210            && let Some((_, ProjectionElem::PtrMetadata)) = len_op.as_projection() =>
211        {
212            rest
213        }
214        // Sometimes that instead looks like:
215        //   a := &raw const *z
216        //   l := use(copy a.metadata)
217        //   b := copy x < copy l
218        //   assert(move b == true)
219        [
220            Statement {
221                kind:
222                    StatementKind::Assign(
223                        reborrow,
224                        Rvalue::RawPtr {
225                            kind: RefKind::Shared,
226                            ..
227                        },
228                    ),
229                ..
230            },
231            Statement {
232                kind: StatementKind::Assign(len, Rvalue::Use(Operand::Copy(len_op), _)),
233                ..
234            },
235            Statement {
236                kind:
237                    StatementKind::Assign(
238                        is_in_bounds,
239                        Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
240                    ),
241                ..
242            },
243            Statement {
244                kind:
245                    StatementKind::Assert {
246                        assert:
247                            Assert {
248                                cond: Operand::Move(cond),
249                                expected: true,
250                                check_kind: Some(BuiltinAssertKind::BoundsCheck { .. }),
251                            },
252                        ..
253                    },
254                ..
255            },
256            rest @ ..,
257        ] if lt_op2 == len
258            && cond == is_in_bounds
259            && let Some((slice_place, ProjectionElem::PtrMetadata)) = len_op.as_projection()
260            && reborrow == slice_place =>
261        {
262            rest
263        }
264
265        // Zero checks for division and remainder. They look like:
266        //   b := copy y == const 0
267        //   assert(move b == false)
268        //   ...
269        //   res := x {/,%} move y;
270        //   ... or ...
271        //   b := const y == const 0
272        //   assert(move b == false)
273        //   ...
274        //   res := x {/,%} const y;
275        //
276        // This also overlaps with overflow checks for negation, which looks like:
277        //   is_min := x == INT::min
278        //   assert(move is_min == false)
279        //   ...
280        //   res := -x;
281        [
282            Statement {
283                kind:
284                    StatementKind::Assign(
285                        is_zero,
286                        Rvalue::BinaryOp(BinOp::Eq, y_op, Operand::Const(_zero)),
287                    ),
288                ..
289            },
290            Statement {
291                kind:
292                    StatementKind::Assert {
293                        assert:
294                            Assert {
295                                cond: Operand::Move(cond),
296                                expected: false,
297                                check_kind:
298                                    Some(
299                                        BuiltinAssertKind::DivisionByZero(_)
300                                        | BuiltinAssertKind::RemainderByZero(_)
301                                        | BuiltinAssertKind::OverflowNeg(_),
302                                    ),
303                            },
304                        ..
305                    },
306                ..
307            },
308            rest @ ..,
309        ] if cond == is_zero => {
310            let found = make_binop_overflow_panic(rest, |bop, _, r| {
311                matches!(bop, BinOp::Div(_) | BinOp::Rem(_)) && equiv_op(r, y_op)
312            }) || make_unop_overflow_panic(rest, |unop, o| {
313                matches!(unop, UnOp::Neg(_)) && equiv_op(o, y_op)
314            });
315            if found {
316                rest
317            } else {
318                return;
319            }
320        }
321
322        // Overflow checks for signed division and remainder. They look like:
323        //   is_neg_1 := y == (-1)
324        //   is_min := x == INT::min
325        //   has_overflow := move (is_neg_1) & move (is_min)
326        //   assert(move has_overflow == false)
327        // Note here we don't need to update the operand to panic, as this was already done
328        // by the previous pass for division by zero.
329        [
330            Statement {
331                kind: StatementKind::Assign(is_neg_1, Rvalue::BinaryOp(BinOp::Eq, _y_op, _minus_1)),
332                ..
333            },
334            Statement {
335                kind: StatementKind::Assign(is_min, Rvalue::BinaryOp(BinOp::Eq, _x_op, _int_min)),
336                ..
337            },
338            Statement {
339                kind:
340                    StatementKind::Assign(
341                        has_overflow,
342                        Rvalue::BinaryOp(
343                            BinOp::BitAnd,
344                            Operand::Move(and_op1),
345                            Operand::Move(and_op2),
346                        ),
347                    ),
348                ..
349            },
350            Statement {
351                kind:
352                    StatementKind::Assert {
353                        assert:
354                            Assert {
355                                cond: Operand::Move(cond),
356                                expected: false,
357                                check_kind: Some(BuiltinAssertKind::Overflow(..)),
358                            },
359                        ..
360                    },
361                ..
362            },
363            rest @ ..,
364        ] if and_op1 == is_neg_1 && and_op2 == is_min && cond == has_overflow => rest,
365
366        // Overflow checks for right/left shift. They can look like:
367        //   a := y as u32; // or another type
368        //   b := move a < const 32; // or another constant
369        //   assert(move b == true);
370        //   ...
371        //   res := x {<<,>>} y;
372        [
373            Statement {
374                kind: StatementKind::Assign(cast, Rvalue::UnaryOp(UnOp::Cast(_), y_op)),
375                ..
376            },
377            Statement {
378                kind:
379                    StatementKind::Assign(
380                        has_overflow,
381                        Rvalue::BinaryOp(BinOp::Lt, Operand::Move(lhs), Operand::Const(..)),
382                    ),
383                ..
384            },
385            Statement {
386                kind:
387                    StatementKind::Assert {
388                        assert:
389                            Assert {
390                                cond: Operand::Move(cond),
391                                expected: true,
392                                check_kind: Some(BuiltinAssertKind::Overflow(..)),
393                            },
394                        ..
395                    },
396                ..
397            },
398            rest @ ..,
399        ] if cond == has_overflow
400            && lhs == cast
401            && let Some(cast_local) = cast.as_local()
402            && !rest.iter().any(|st| uses_local(st, cast_local)) =>
403        {
404            let found = make_binop_overflow_panic(rest, |bop, _, r| {
405                matches!(bop, BinOp::Shl(_) | BinOp::Shr(_)) && equiv_op(r, y_op)
406            });
407            if found {
408                rest
409            } else {
410                // The shift is not in this block; it may live in a later one (see
411                // [PendingShiftCheck]). Record it and resolve it over the whole body afterwards.
412                if let Some(cond_local) = has_overflow.as_local() {
413                    pending_shift_checks.push(PendingShiftCheck {
414                        block: block_id,
415                        amount: y_op.clone(),
416                        cond_local,
417                        cast_local: Some(cast_local),
418                    });
419                }
420                return;
421            }
422        }
423        // or like:
424        //   b := y < const 32; // or another constant
425        //   assert(move b == true);
426        //   ...
427        //   res := x {<<,>>} y;
428        //
429        // this also overlaps with out of bounds checks for arrays, so we check for either;
430        // these look like:
431        //   b := copy y < const _
432        //   assert(move b == true)
433        //   ...
434        //   res := a[y];
435        [
436            Statement {
437                kind:
438                    StatementKind::Assign(
439                        has_overflow,
440                        Rvalue::BinaryOp(BinOp::Lt, y_op, Operand::Const(..)),
441                    ),
442                ..
443            },
444            Statement {
445                kind:
446                    StatementKind::Assert {
447                        assert:
448                            Assert {
449                                cond: Operand::Move(cond),
450                                expected: true,
451                                check_kind:
452                                    check_kind @ Some(
453                                        BuiltinAssertKind::Overflow(..)
454                                        | BuiltinAssertKind::BoundsCheck { .. },
455                                    ),
456                            },
457                        ..
458                    },
459                ..
460            },
461            rest @ ..,
462        ] if cond == has_overflow => {
463            // look for a shift operation
464            let mut found = make_binop_overflow_panic(rest, |bop, _, r| {
465                matches!(bop, BinOp::Shl(_) | BinOp::Shr(_)) && equiv_op(r, y_op)
466            });
467            if !found {
468                // otherwise, look for an array access
469                for stmt in rest.iter_mut() {
470                    stmt.dyn_visit_in_body(|p: &Place| {
471                        if let Some((_, ProjectionElem::Index { offset, .. })) = p.as_projection()
472                            && equiv_op(offset, y_op)
473                        {
474                            found = true;
475                        }
476                    });
477                }
478            }
479
480            if found {
481                rest
482            } else {
483                // The shift may be in a later block (see [PendingShiftCheck]). This only applies
484                // to overflow checks: a bounds check never guards a shift.
485                if matches!(check_kind, Some(BuiltinAssertKind::Overflow(..)))
486                    && let Some(cond_local) = has_overflow.as_local()
487                {
488                    pending_shift_checks.push(PendingShiftCheck {
489                        block: block_id,
490                        amount: y_op.clone(),
491                        cond_local,
492                        cast_local: None,
493                    });
494                }
495                return;
496            }
497        }
498
499        // Overflow checks for addition/subtraction/multiplication. They look like:
500        // ```text
501        //   r := x checked.+ y;
502        //   assert(move r.1 == false);
503        //   ...
504        //   z := move r.0;
505        // ```
506        // We replace that with:
507        // ```text
508        // z := x panic.+ y;
509        // ```
510        //
511        // But sometimes, because of constant promotion, we end up with a lone checked operation
512        // without assert. In that case we replace it with its wrapping equivalent.
513        [
514            Statement {
515                kind:
516                    StatementKind::Assign(
517                        tuple,
518                        Rvalue::BinaryOp(
519                            binop @ (BinOp::AddChecked | BinOp::SubChecked | BinOp::MulChecked),
520                            _,
521                            _,
522                        ),
523                    ),
524                ..
525            },
526            rest @ ..,
527        ] if let Some(tuple_local_id) = tuple.as_local()
528            && !used_outside_block(tuple_local_id) =>
529        {
530            // Check if the result boolean is used in any other way than just getting the integer
531            // result.
532            let mut uses_of_tuple = 0;
533            let mut uses_of_integer = 0;
534            if *tuple == locals.return_place() {
535                uses_of_tuple += 1; // The return place counts as a use.
536            }
537            for stmt in rest.iter_mut() {
538                stmt.dyn_visit_in_body(|p: &Place| {
539                    if p == tuple {
540                        uses_of_tuple += 1;
541                    }
542                    if let Some((sub, ProjectionElem::Field(_, fid))) = p.as_projection()
543                        && sub.ty().is_tuple()
544                        && fid.index() == 0
545                        && sub == tuple
546                    {
547                        uses_of_integer += 1;
548                    }
549                });
550            }
551            // Check if the operation is followed by an assert.
552            let followed_by_assert = if let [
553                Statement {
554                    kind:
555                        StatementKind::Assert {
556                            assert:
557                                Assert {
558                                    cond: Operand::Move(assert_cond),
559                                    expected: false,
560                                    check_kind: Some(BuiltinAssertKind::Overflow(..)),
561                                },
562                            ..
563                        },
564                    ..
565                },
566                ..,
567            ] = rest
568                && let Some((sub, ProjectionElem::Field(_, fid))) = assert_cond.as_projection()
569                && sub.ty().is_tuple()
570                && fid.index() == 1
571                && sub == tuple
572            {
573                true
574            } else {
575                false
576            };
577            if uses_of_tuple != uses_of_integer && !followed_by_assert {
578                // The tuple is used either directly or for the overflow check; we change nothing.
579                return;
580            }
581
582            if followed_by_assert {
583                // We have a compiler-emitted assert. We replace the operation with one that has
584                // panic-on-overflow semantics.
585                *binop = binop.with_overflow(OverflowMode::Panic);
586                // The failure behavior is part of the binop now, so we remove the assert.
587                rest[0].kind = StatementKind::Nop;
588            } else {
589                // The tuple is used exclusively to access the integer result, so we replace the
590                // operation with wrapping semantics.
591                *binop = binop.with_overflow(OverflowMode::Wrap);
592            }
593            // Fixup the local type.
594            let result_local = &mut locals.locals[tuple_local_id];
595            result_local.ty = result_local
596                .ty
597                .as_tuple_fields(&ctx.translated)
598                .swap_remove(0);
599            // Fixup the place type.
600            let new_result_place = locals.place_for_var(tuple_local_id);
601            // Replace uses of `r.0` with `r`.
602            for stmt in rest.iter_mut() {
603                stmt.dyn_visit_in_body_mut(|p: &mut Place| {
604                    if let Some((sub, ProjectionElem::Field(_, fid))) = p.as_projection()
605                        && sub.ty().is_tuple()
606                        && sub == tuple
607                    {
608                        assert_eq!(fid.index(), 0);
609                        *p = new_result_place.clone()
610                    }
611                });
612            }
613            *tuple = new_result_place;
614            return;
615        }
616
617        _ => return,
618    };
619
620    // Remove the statements we're not keeping.
621    let keep_len = statements_to_keep.len();
622    let removed_len = statements.len() - keep_len;
623    for i in 0..removed_len {
624        // If the statement we're removing assigns to a local that
625        // is used elsewhere (in the leftover statements or in another block),
626        // we don't remove it.
627        if let StatementKind::Assign(place, _) = &statements[i].kind
628            && let Some(local) = place.as_local()
629            && let mut statements_to_keep = statements[removed_len..].as_ref().iter()
630            && (used_outside_block(local) || statements_to_keep.any(|st| uses_local(st, local)))
631        {
632            continue;
633        };
634        statements[i].kind = StatementKind::Nop;
635    }
636}
637
638/// Resolve a [PendingShiftCheck] whose shift landed in a different block. We remove the check and
639/// compute the shift early, promoted to panic-on-overflow, into a fresh local placed where the
640/// check used to be; the fresh local then carries the result to the original destination.
641///
642/// This is correct because rustc has already evaluated both shift operands by the time control
643/// reaches the check: the only effects of the shift are that evaluation and the panic-on-overflow,
644/// and the overflow check already performs both at that point. Storing the result in a fresh local
645/// lets us keep the write to the (not-yet-computed) destination place at the original shift site.
646fn resolve_pending_shift_check(body: &mut ExprBody, check: PendingShiftCheck) {
647    let PendingShiftCheck {
648        block,
649        amount,
650        cond_local,
651        cast_local,
652    } = check;
653
654    // Locate the assert we will repurpose to hold the early, promoted shift. Pairing it with the
655    // shift below (via `zip`) means we bail before mutating anything if either is unexpectedly
656    // absent, so the shift is never rewired to a fresh local we never assigned.
657    let assert_pos = body.body[block].statements.iter().position(|stmt| {
658        matches!(
659            &stmt.kind,
660            StatementKind::Assert {
661                assert: Assert {
662                    cond: Operand::Move(cond),
663                    ..
664                },
665                ..
666            } if cond.as_local() == Some(cond_local)
667        )
668    });
669
670    // rustc only emits these asserts for its own shifts, so we don't follow the control flow: we
671    // look for the (still-wrapping) shift by its amount anywhere in the body.
672    let Some(((shift_loc, dest, lhs, rhs, panic_binop, ty), assert_pos)) = body
673        .body
674        .iter_enumerated()
675        .find_map(|(sb, block_data)| {
676            block_data
677                .statements
678                .iter()
679                .enumerate()
680                .find_map(|(i, stmt)| {
681                    if let StatementKind::Assign(dest, Rvalue::BinaryOp(binop, lhs, rhs)) =
682                        &stmt.kind
683                        && matches!(
684                            binop,
685                            BinOp::Shl(OverflowMode::Wrap) | BinOp::Shr(OverflowMode::Wrap)
686                        )
687                        && equiv_op(rhs, &amount)
688                    {
689                        Some((
690                            StmtLoc::new(sb, i),
691                            dest.clone(),
692                            lhs.clone(),
693                            rhs.clone(),
694                            binop.with_overflow(OverflowMode::Panic),
695                            dest.ty.clone(),
696                        ))
697                    } else {
698                        None
699                    }
700                })
701        })
702        .zip(assert_pos)
703    else {
704        return;
705    };
706
707    // Every mutation below now happens together, so the fresh local is always assigned and used.
708    let fresh = body.locals.new_var(None, ty);
709
710    // Drop the check's comparison and optional cast; they are dead once the assert is gone.
711    body.body[block].statements.iter_mut().for_each(|stmt| {
712        let is_check_temp = if let StatementKind::Assign(place, _) = &stmt.kind {
713            let local = place.as_local();
714            local == Some(cond_local) || cast_local.is_some_and(|c| local == Some(c))
715        } else {
716            false
717        };
718        if is_check_temp {
719            stmt.kind = StatementKind::Nop;
720        }
721    });
722
723    // Compute the shift early, promoted to panic-on-overflow, where the check used to be.
724    body[StmtLoc::new(block, assert_pos)].kind =
725        StatementKind::Assign(fresh.clone(), Rvalue::BinaryOp(panic_binop, lhs, rhs));
726
727    // Forward the result at the original shift site.
728    body[shift_loc].kind =
729        StatementKind::Assign(dest, Rvalue::Use(Operand::Move(fresh), WithRetag::No));
730}
731
732pub struct Transform;
733impl UllbcPass for Transform {
734    fn should_run(&self, options: &crate::options::TranslateOptions) -> bool {
735        options.reconstruct_fallible_operations
736    }
737
738    fn transform_body(&self, ctx: &mut TransformCtx, b: &mut ExprBody) {
739        let local_uses: LocalUses = compute_uses(b);
740        let mut pending_shift_checks = Vec::new();
741        b.transform_sequences_fwd(|id, locals, seq| {
742            remove_dynamic_checks(ctx, &local_uses, id, locals, seq, &mut pending_shift_checks);
743            Vec::new()
744        });
745        // Resolve the checks whose shift lives in a later block (see [PendingShiftCheck]).
746        pending_shift_checks
747            .into_iter()
748            .for_each(|check| resolve_pending_shift_check(b, check));
749    }
750}