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::*;
12use crate::ids::IndexVec;
13use crate::transform::TransformCtx;
14use crate::ullbc_ast::{BlockId, ExprBody, Statement, StatementKind};
15
16use crate::transform::ctx::UllbcPass;
17
18type LocalUses = IndexVec<BlockId, HashSet<LocalId>>;
19
20/// Compute for each block the locals that are assumed to have been initialized/defined before entering it.
21fn compute_uses(body: &ExprBody) -> LocalUses {
22 #[derive(Visitor)]
23 struct UsedLocalsVisitor<'a>(&'a mut HashSet<LocalId>);
24
25 impl VisitBody for UsedLocalsVisitor<'_> {
26 fn visit_place(&mut self, x: &Place) -> ::std::ops::ControlFlow<Self::Break> {
27 if let Some(local_id) = x.as_local() {
28 self.0.insert(local_id);
29 }
30 self.visit_inner(x)
31 }
32 }
33
34 body.body.map_ref(|block| {
35 let mut uses = HashSet::new();
36 let mut visitor = UsedLocalsVisitor(&mut uses);
37
38 // do a simple live variable analysis by walking the block backwards
39 for statement in block.statements.iter().rev() {
40 match &statement.kind {
41 StatementKind::Assign(place, rval) => {
42 // We clear the assigned place, but it may be re-added
43 // if it's used in rval
44 if let Some(local_id) = place.as_local() {
45 visitor.0.remove(&local_id);
46 }
47 let _ = rval.drive_body(&mut visitor);
48 }
49 StatementKind::StorageLive(local) | StatementKind::StorageDead(local) => {
50 // A `StorageLive` re-sets the local to be uninitialised,
51 // so any usage after this point doesn't matter
52 // Similarly, a `StorageDead` means the local is de-initialised,
53 // so we can ignore any usage after this point
54 visitor.0.remove(local);
55 }
56 _ => {
57 let _ = statement.drive_body(&mut visitor);
58 }
59 }
60 }
61
62 uses
63 })
64}
65
66/// Whether the value uses the given local in a place.
67fn uses_local<T: BodyVisitable>(x: &T, local: LocalId) -> bool {
68 struct FoundIt;
69 struct UsesLocalVisitor(LocalId);
70
71 impl Visitor for UsesLocalVisitor {
72 type Break = FoundIt;
73 }
74 impl VisitBody for UsesLocalVisitor {
75 fn visit_place(&mut self, x: &Place) -> ::std::ops::ControlFlow<Self::Break> {
76 if let Some(local_id) = x.as_local()
77 && local_id == self.0
78 {
79 return ControlFlow::Break(FoundIt);
80 }
81 self.visit_inner(x)
82 }
83
84 fn visit_ullbc_statement(
85 &mut self,
86 x: &ullbc_ast::Statement,
87 ) -> ::std::ops::ControlFlow<Self::Break> {
88 match x.kind {
89 StatementKind::StorageDead(_) | StatementKind::StorageLive(_) => {
90 ControlFlow::Continue(())
91 }
92 _ => self.visit_inner(x),
93 }
94 }
95 }
96
97 x.drive_body(&mut UsesLocalVisitor(local)).is_break()
98}
99
100fn make_binop_overflow_panic<T: BodyVisitable>(
101 x: &mut [T],
102 matches: impl Fn(&BinOp, &Operand, &Operand) -> bool,
103) -> bool {
104 let mut found = false;
105 for y in x.iter_mut() {
106 y.dyn_visit_in_body_mut(|rv: &mut Rvalue| {
107 if let Rvalue::BinaryOp(binop, op_l, op_r) = rv
108 && matches(binop, op_l, op_r)
109 {
110 *binop = binop.with_overflow(OverflowMode::Panic);
111 found = true;
112 }
113 });
114 }
115 found
116}
117
118fn make_unop_overflow_panic<T: BodyVisitable>(
119 x: &mut [T],
120 matches: impl Fn(&UnOp, &Operand) -> bool,
121) -> bool {
122 let mut found = false;
123 for y in x.iter_mut() {
124 y.dyn_visit_in_body_mut(|rv: &mut Rvalue| {
125 if let Rvalue::UnaryOp(unop, op) = rv
126 && matches(unop, op)
127 {
128 *unop = unop.with_overflow(OverflowMode::Panic);
129 found = true;
130 }
131 });
132 }
133 found
134}
135
136/// Check if the two operands are equivalent: either they're the same constant, or they represent
137/// the same place (regardless of whether the operand is a move or a copy)
138fn equiv_op(op_l: &Operand, op_r: &Operand) -> bool {
139 match (op_l, op_r) {
140 (Operand::Copy(l) | Operand::Move(l), Operand::Copy(r) | Operand::Move(r)) => l == r,
141 (Operand::Const(l), Operand::Const(r)) => l == r,
142 _ => false,
143 }
144}
145
146/// Rustc inserts dynamic checks during MIR lowering. They all end in an `Assert` statement (and
147/// this is the only use of this statement).
148fn remove_dynamic_checks(
149 _ctx: &mut TransformCtx,
150 uses: &LocalUses,
151 block_id: BlockId,
152 locals: &mut Locals,
153 statements: &mut [Statement],
154) {
155 // Whether this local was used in another block
156 let used_outside_block = |local: LocalId| {
157 uses.iter_enumerated()
158 .any(|(bid, used)| bid != block_id && used.contains(&local))
159 };
160
161 // We return the statements we want to keep, which must be a prefix of `block.statements`.
162 let statements_to_keep = match statements {
163 // Bounds checks for slices. They look like:
164 // l := use(copy a.metadata)
165 // b := copy x < copy l
166 // assert(move b == true)
167 [
168 Statement {
169 kind: StatementKind::Assign(len, Rvalue::Use(Operand::Copy(len_op), _)),
170 ..
171 },
172 Statement {
173 kind:
174 StatementKind::Assign(
175 is_in_bounds,
176 Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
177 ),
178 ..
179 },
180 Statement {
181 kind:
182 StatementKind::Assert {
183 assert:
184 Assert {
185 cond: Operand::Move(cond),
186 expected: true,
187 ..
188 },
189 ..
190 },
191 ..
192 },
193 rest @ ..,
194 ] if lt_op2 == len
195 && cond == is_in_bounds
196 && let Some((_, ProjectionElem::PtrMetadata)) = len_op.as_projection() =>
197 {
198 rest
199 }
200 // Sometimes that instead looks like:
201 // a := &raw const *z
202 // l := use(copy a.metadata)
203 // b := copy x < copy l
204 // assert(move b == true)
205 [
206 Statement {
207 kind:
208 StatementKind::Assign(
209 reborrow,
210 Rvalue::RawPtr {
211 kind: RefKind::Shared,
212 ..
213 },
214 ),
215 ..
216 },
217 Statement {
218 kind: StatementKind::Assign(len, Rvalue::Use(Operand::Copy(len_op), _)),
219 ..
220 },
221 Statement {
222 kind:
223 StatementKind::Assign(
224 is_in_bounds,
225 Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
226 ),
227 ..
228 },
229 Statement {
230 kind:
231 StatementKind::Assert {
232 assert:
233 Assert {
234 cond: Operand::Move(cond),
235 expected: true,
236 check_kind: Some(BuiltinAssertKind::BoundsCheck { .. }),
237 },
238 ..
239 },
240 ..
241 },
242 rest @ ..,
243 ] if lt_op2 == len
244 && cond == is_in_bounds
245 && let Some((slice_place, ProjectionElem::PtrMetadata)) = len_op.as_projection()
246 && reborrow == slice_place =>
247 {
248 rest
249 }
250
251 // Zero checks for division and remainder. They look like:
252 // b := copy y == const 0
253 // assert(move b == false)
254 // ...
255 // res := x {/,%} move y;
256 // ... or ...
257 // b := const y == const 0
258 // assert(move b == false)
259 // ...
260 // res := x {/,%} const y;
261 //
262 // This also overlaps with overflow checks for negation, which looks like:
263 // is_min := x == INT::min
264 // assert(move is_min == false)
265 // ...
266 // res := -x;
267 [
268 Statement {
269 kind:
270 StatementKind::Assign(
271 is_zero,
272 Rvalue::BinaryOp(BinOp::Eq, y_op, Operand::Const(_zero)),
273 ),
274 ..
275 },
276 Statement {
277 kind:
278 StatementKind::Assert {
279 assert:
280 Assert {
281 cond: Operand::Move(cond),
282 expected: false,
283 check_kind:
284 Some(
285 BuiltinAssertKind::DivisionByZero(_)
286 | BuiltinAssertKind::RemainderByZero(_)
287 | BuiltinAssertKind::OverflowNeg(_),
288 ),
289 },
290 ..
291 },
292 ..
293 },
294 rest @ ..,
295 ] if cond == is_zero => {
296 let found = make_binop_overflow_panic(rest, |bop, _, r| {
297 matches!(bop, BinOp::Div(_) | BinOp::Rem(_)) && equiv_op(r, y_op)
298 }) || make_unop_overflow_panic(rest, |unop, o| {
299 matches!(unop, UnOp::Neg(_)) && equiv_op(o, y_op)
300 });
301 if found {
302 rest
303 } else {
304 return;
305 }
306 }
307
308 // Overflow checks for signed division and remainder. They look like:
309 // is_neg_1 := y == (-1)
310 // is_min := x == INT::min
311 // has_overflow := move (is_neg_1) & move (is_min)
312 // assert(move has_overflow == false)
313 // Note here we don't need to update the operand to panic, as this was already done
314 // by the previous pass for division by zero.
315 [
316 Statement {
317 kind: StatementKind::Assign(is_neg_1, Rvalue::BinaryOp(BinOp::Eq, _y_op, _minus_1)),
318 ..
319 },
320 Statement {
321 kind: StatementKind::Assign(is_min, Rvalue::BinaryOp(BinOp::Eq, _x_op, _int_min)),
322 ..
323 },
324 Statement {
325 kind:
326 StatementKind::Assign(
327 has_overflow,
328 Rvalue::BinaryOp(
329 BinOp::BitAnd,
330 Operand::Move(and_op1),
331 Operand::Move(and_op2),
332 ),
333 ),
334 ..
335 },
336 Statement {
337 kind:
338 StatementKind::Assert {
339 assert:
340 Assert {
341 cond: Operand::Move(cond),
342 expected: false,
343 check_kind: Some(BuiltinAssertKind::Overflow(..)),
344 },
345 ..
346 },
347 ..
348 },
349 rest @ ..,
350 ] if and_op1 == is_neg_1 && and_op2 == is_min && cond == has_overflow => rest,
351
352 // Overflow checks for right/left shift. They can look like:
353 // a := y as u32; // or another type
354 // b := move a < const 32; // or another constant
355 // assert(move b == true);
356 // ...
357 // res := x {<<,>>} y;
358 [
359 Statement {
360 kind: StatementKind::Assign(cast, Rvalue::UnaryOp(UnOp::Cast(_), y_op)),
361 ..
362 },
363 Statement {
364 kind:
365 StatementKind::Assign(
366 has_overflow,
367 Rvalue::BinaryOp(BinOp::Lt, Operand::Move(lhs), Operand::Const(..)),
368 ),
369 ..
370 },
371 Statement {
372 kind:
373 StatementKind::Assert {
374 assert:
375 Assert {
376 cond: Operand::Move(cond),
377 expected: true,
378 check_kind: Some(BuiltinAssertKind::Overflow(..)),
379 },
380 ..
381 },
382 ..
383 },
384 rest @ ..,
385 ] if cond == has_overflow
386 && lhs == cast
387 && let Some(cast_local) = cast.as_local()
388 && !rest.iter().any(|st| uses_local(st, cast_local)) =>
389 {
390 let found = make_binop_overflow_panic(rest, |bop, _, r| {
391 matches!(bop, BinOp::Shl(_) | BinOp::Shr(_)) && equiv_op(r, y_op)
392 });
393 if found {
394 rest
395 } else {
396 return;
397 }
398 }
399 // or like:
400 // b := y < const 32; // or another constant
401 // assert(move b == true);
402 // ...
403 // res := x {<<,>>} y;
404 //
405 // this also overlaps with out of bounds checks for arrays, so we check for either;
406 // these look like:
407 // b := copy y < const _
408 // assert(move b == true)
409 // ...
410 // res := a[y];
411 [
412 Statement {
413 kind:
414 StatementKind::Assign(
415 has_overflow,
416 Rvalue::BinaryOp(BinOp::Lt, y_op, Operand::Const(..)),
417 ),
418 ..
419 },
420 Statement {
421 kind:
422 StatementKind::Assert {
423 assert:
424 Assert {
425 cond: Operand::Move(cond),
426 expected: true,
427 check_kind:
428 Some(
429 BuiltinAssertKind::Overflow(..)
430 | BuiltinAssertKind::BoundsCheck { .. },
431 ),
432 },
433 ..
434 },
435 ..
436 },
437 rest @ ..,
438 ] if cond == has_overflow => {
439 // look for a shift operation
440 let mut found = make_binop_overflow_panic(rest, |bop, _, r| {
441 matches!(bop, BinOp::Shl(_) | BinOp::Shr(_)) && equiv_op(r, y_op)
442 });
443 if !found {
444 // otherwise, look for an array access
445 for stmt in rest.iter_mut() {
446 stmt.dyn_visit_in_body(|p: &Place| {
447 if let Some((_, ProjectionElem::Index { offset, .. })) = p.as_projection()
448 && equiv_op(offset, y_op)
449 {
450 found = true;
451 }
452 });
453 }
454 }
455
456 if found {
457 rest
458 } else {
459 return;
460 }
461 }
462
463 // Overflow checks for addition/subtraction/multiplication. They look like:
464 // ```text
465 // r := x checked.+ y;
466 // assert(move r.1 == false);
467 // ...
468 // z := move r.0;
469 // ```
470 // We replace that with:
471 // ```text
472 // z := x panic.+ y;
473 // ```
474 //
475 // But sometimes, because of constant promotion, we end up with a lone checked operation
476 // without assert. In that case we replace it with its wrapping equivalent.
477 [
478 Statement {
479 kind:
480 StatementKind::Assign(
481 tuple,
482 Rvalue::BinaryOp(
483 binop @ (BinOp::AddChecked | BinOp::SubChecked | BinOp::MulChecked),
484 _,
485 _,
486 ),
487 ),
488 ..
489 },
490 rest @ ..,
491 ] if let Some(tuple_local_id) = tuple.as_local()
492 && !used_outside_block(tuple_local_id) =>
493 {
494 // Check if the result boolean is used in any other way than just getting the integer
495 // result.
496 let mut uses_of_tuple = 0;
497 let mut uses_of_integer = 0;
498 if *tuple == locals.return_place() {
499 uses_of_tuple += 1; // The return place counts as a use.
500 }
501 for stmt in rest.iter_mut() {
502 stmt.dyn_visit_in_body(|p: &Place| {
503 if p == tuple {
504 uses_of_tuple += 1;
505 }
506 if let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(..), fid))) =
507 p.as_projection()
508 && fid.index() == 0
509 && sub == tuple
510 {
511 uses_of_integer += 1;
512 }
513 });
514 }
515 // Check if the operation is followed by an assert.
516 let followed_by_assert = if let [
517 Statement {
518 kind:
519 StatementKind::Assert {
520 assert:
521 Assert {
522 cond: Operand::Move(assert_cond),
523 expected: false,
524 check_kind: Some(BuiltinAssertKind::Overflow(..)),
525 },
526 ..
527 },
528 ..
529 },
530 ..,
531 ] = rest
532 && let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(..), fid))) =
533 assert_cond.as_projection()
534 && fid.index() == 1
535 && sub == tuple
536 {
537 true
538 } else {
539 false
540 };
541 if uses_of_tuple != uses_of_integer && !followed_by_assert {
542 // The tuple is used either directly or for the overflow check; we change nothing.
543 return;
544 }
545
546 if followed_by_assert {
547 // We have a compiler-emitted assert. We replace the operation with one that has
548 // panic-on-overflow semantics.
549 *binop = binop.with_overflow(OverflowMode::Panic);
550 // The failure behavior is part of the binop now, so we remove the assert.
551 rest[0].kind = StatementKind::Nop;
552 } else {
553 // The tuple is used exclusively to access the integer result, so we replace the
554 // operation with wrapping semantics.
555 *binop = binop.with_overflow(OverflowMode::Wrap);
556 }
557 // Fixup the local type.
558 let result_local = &mut locals.locals[tuple_local_id];
559 result_local.ty = result_local.ty.as_tuple().unwrap()[0].clone();
560 // Fixup the place type.
561 let new_result_place = locals.place_for_var(tuple_local_id);
562 // Replace uses of `r.0` with `r`.
563 for stmt in rest.iter_mut() {
564 stmt.dyn_visit_in_body_mut(|p: &mut Place| {
565 if let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(..), fid))) =
566 p.as_projection()
567 && sub == tuple
568 {
569 assert_eq!(fid.index(), 0);
570 *p = new_result_place.clone()
571 }
572 });
573 }
574 *tuple = new_result_place;
575 return;
576 }
577
578 _ => return,
579 };
580
581 // Remove the statements we're not keeping.
582 let keep_len = statements_to_keep.len();
583 let removed_len = statements.len() - keep_len;
584 for i in 0..removed_len {
585 // If the statement we're removing assigns to a local that
586 // is used elsewhere (in the leftover statements or in another block),
587 // we don't remove it.
588 if let StatementKind::Assign(place, _) = &statements[i].kind
589 && let Some(local) = place.as_local()
590 && let mut statements_to_keep = statements[removed_len..].as_ref().iter()
591 && (used_outside_block(local) || statements_to_keep.any(|st| uses_local(st, local)))
592 {
593 continue;
594 };
595 statements[i].kind = StatementKind::Nop;
596 }
597}
598
599pub struct Transform;
600impl UllbcPass for Transform {
601 fn should_run(&self, options: &crate::options::TranslateOptions) -> bool {
602 options.reconstruct_fallible_operations
603 }
604
605 fn transform_body(&self, ctx: &mut TransformCtx, b: &mut ExprBody) {
606 let local_uses: LocalUses = compute_uses(b);
607 b.transform_sequences_fwd(|id, locals, seq| {
608 remove_dynamic_checks(ctx, &local_uses, id, locals, seq);
609 Vec::new()
610 });
611 }
612}