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