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}