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 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}