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().as_tuple().is_some()
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().as_tuple().is_some()
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.ty.as_tuple().unwrap()[0].clone();
596            // Fixup the place type.
597            let new_result_place = locals.place_for_var(tuple_local_id);
598            // Replace uses of `r.0` with `r`.
599            for stmt in rest.iter_mut() {
600                stmt.dyn_visit_in_body_mut(|p: &mut Place| {
601                    if let Some((sub, ProjectionElem::Field(_, fid))) = p.as_projection()
602                        && sub.ty().as_tuple().is_some()
603                        && sub == tuple
604                    {
605                        assert_eq!(fid.index(), 0);
606                        *p = new_result_place.clone()
607                    }
608                });
609            }
610            *tuple = new_result_place;
611            return;
612        }
613
614        _ => return,
615    };
616
617    // Remove the statements we're not keeping.
618    let keep_len = statements_to_keep.len();
619    let removed_len = statements.len() - keep_len;
620    for i in 0..removed_len {
621        // If the statement we're removing assigns to a local that
622        // is used elsewhere (in the leftover statements or in another block),
623        // we don't remove it.
624        if let StatementKind::Assign(place, _) = &statements[i].kind
625            && let Some(local) = place.as_local()
626            && let mut statements_to_keep = statements[removed_len..].as_ref().iter()
627            && (used_outside_block(local) || statements_to_keep.any(|st| uses_local(st, local)))
628        {
629            continue;
630        };
631        statements[i].kind = StatementKind::Nop;
632    }
633}
634
635/// Resolve a [PendingShiftCheck] whose shift landed in a different block. We remove the check and
636/// compute the shift early, promoted to panic-on-overflow, into a fresh local placed where the
637/// check used to be; the fresh local then carries the result to the original destination.
638///
639/// This is correct because rustc has already evaluated both shift operands by the time control
640/// reaches the check: the only effects of the shift are that evaluation and the panic-on-overflow,
641/// and the overflow check already performs both at that point. Storing the result in a fresh local
642/// lets us keep the write to the (not-yet-computed) destination place at the original shift site.
643fn resolve_pending_shift_check(body: &mut ExprBody, check: PendingShiftCheck) {
644    let PendingShiftCheck {
645        block,
646        amount,
647        cond_local,
648        cast_local,
649    } = check;
650
651    // Locate the assert we will repurpose to hold the early, promoted shift. Pairing it with the
652    // shift below (via `zip`) means we bail before mutating anything if either is unexpectedly
653    // absent, so the shift is never rewired to a fresh local we never assigned.
654    let assert_pos = body.body[block].statements.iter().position(|stmt| {
655        matches!(
656            &stmt.kind,
657            StatementKind::Assert {
658                assert: Assert {
659                    cond: Operand::Move(cond),
660                    ..
661                },
662                ..
663            } if cond.as_local() == Some(cond_local)
664        )
665    });
666
667    // rustc only emits these asserts for its own shifts, so we don't follow the control flow: we
668    // look for the (still-wrapping) shift by its amount anywhere in the body.
669    let Some(((shift_loc, dest, lhs, rhs, panic_binop, ty), assert_pos)) = body
670        .body
671        .iter_enumerated()
672        .find_map(|(sb, block_data)| {
673            block_data
674                .statements
675                .iter()
676                .enumerate()
677                .find_map(|(i, stmt)| {
678                    if let StatementKind::Assign(dest, Rvalue::BinaryOp(binop, lhs, rhs)) =
679                        &stmt.kind
680                        && matches!(
681                            binop,
682                            BinOp::Shl(OverflowMode::Wrap) | BinOp::Shr(OverflowMode::Wrap)
683                        )
684                        && equiv_op(rhs, &amount)
685                    {
686                        Some((
687                            StmtLoc::new(sb, i),
688                            dest.clone(),
689                            lhs.clone(),
690                            rhs.clone(),
691                            binop.with_overflow(OverflowMode::Panic),
692                            dest.ty.clone(),
693                        ))
694                    } else {
695                        None
696                    }
697                })
698        })
699        .zip(assert_pos)
700    else {
701        return;
702    };
703
704    // Every mutation below now happens together, so the fresh local is always assigned and used.
705    let fresh = body.locals.new_var(None, ty);
706
707    // Drop the check's comparison and optional cast; they are dead once the assert is gone.
708    body.body[block].statements.iter_mut().for_each(|stmt| {
709        let is_check_temp = if let StatementKind::Assign(place, _) = &stmt.kind {
710            let local = place.as_local();
711            local == Some(cond_local) || cast_local.is_some_and(|c| local == Some(c))
712        } else {
713            false
714        };
715        if is_check_temp {
716            stmt.kind = StatementKind::Nop;
717        }
718    });
719
720    // Compute the shift early, promoted to panic-on-overflow, where the check used to be.
721    body[StmtLoc::new(block, assert_pos)].kind =
722        StatementKind::Assign(fresh.clone(), Rvalue::BinaryOp(panic_binop, lhs, rhs));
723
724    // Forward the result at the original shift site.
725    body[shift_loc].kind =
726        StatementKind::Assign(dest, Rvalue::Use(Operand::Move(fresh), WithRetag::No));
727}
728
729pub struct Transform;
730impl UllbcPass for Transform {
731    fn should_run(&self, options: &crate::options::TranslateOptions) -> bool {
732        options.reconstruct_fallible_operations
733    }
734
735    fn transform_body(&self, ctx: &mut TransformCtx, b: &mut ExprBody) {
736        let local_uses: LocalUses = compute_uses(b);
737        let mut pending_shift_checks = Vec::new();
738        b.transform_sequences_fwd(|id, locals, seq| {
739            remove_dynamic_checks(ctx, &local_uses, id, locals, seq, &mut pending_shift_checks);
740            Vec::new()
741        });
742        // Resolve the checks whose shift lives in a later block (see [PendingShiftCheck]).
743        pending_shift_checks
744            .into_iter()
745            .for_each(|check| resolve_pending_shift_check(b, check));
746    }
747}