charon_lib/transform/
remove_dynamic_checks.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 derive_generic_visitor::*;
8
9use crate::ast::*;
10use crate::transform::TransformCtx;
11use crate::ullbc_ast::{ExprBody, RawStatement, Statement};
12
13use super::ctx::UllbcPass;
14
15/// Whether the value uses the given local in a place.
16fn uses_local<T: BodyVisitable>(x: &T, local: LocalId) -> bool {
17    struct FoundIt;
18    struct UsesLocalVisitor(LocalId);
19
20    impl Visitor for UsesLocalVisitor {
21        type Break = FoundIt;
22    }
23    impl VisitBody for UsesLocalVisitor {
24        fn visit_place(&mut self, x: &Place) -> ::std::ops::ControlFlow<Self::Break> {
25            if let Some(local_id) = x.as_local() {
26                if local_id == self.0 {
27                    return ControlFlow::Break(FoundIt);
28                }
29            }
30            self.visit_inner(x)
31        }
32    }
33
34    x.drive_body(&mut UsesLocalVisitor(local)).is_break()
35}
36
37fn make_binop_overflow_panic<T: BodyVisitable>(
38    x: &mut [T],
39    matches: impl Fn(&BinOp, &Operand, &Operand) -> bool,
40) -> bool {
41    let mut found = false;
42    for y in x.iter_mut() {
43        y.dyn_visit_in_body_mut(|rv: &mut Rvalue| {
44            if let Rvalue::BinaryOp(binop, op_l, op_r) = rv
45                && matches(binop, op_l, op_r)
46            {
47                *binop = binop.with_overflow(OverflowMode::Panic);
48                found = true;
49            }
50        });
51    }
52    found
53}
54
55fn make_unop_overflow_panic<T: BodyVisitable>(
56    x: &mut [T],
57    matches: impl Fn(&UnOp, &Operand) -> bool,
58) -> bool {
59    let mut found = false;
60    for y in x.iter_mut() {
61        y.dyn_visit_in_body_mut(|rv: &mut Rvalue| {
62            if let Rvalue::UnaryOp(unop, op) = rv
63                && matches(unop, op)
64            {
65                *unop = unop.with_overflow(OverflowMode::Panic);
66                found = true;
67            }
68        });
69    }
70    found
71}
72
73/// Check if the two operands are equivalent: either they're the same constant, or they represent
74/// the same place (regardless of whether the operand is a move or a copy)
75fn equiv_op(op_l: &Operand, op_r: &Operand) -> bool {
76    match (op_l, op_r) {
77        (Operand::Copy(l) | Operand::Move(l), Operand::Copy(r) | Operand::Move(r)) => l == r,
78        (Operand::Const(l), Operand::Const(r)) => l == r,
79        _ => false,
80    }
81}
82
83/// Rustc inserts dynamic checks during MIR lowering. They all end in an `Assert` statement (and
84/// this is the only use of this statement).
85fn remove_dynamic_checks(
86    _ctx: &mut TransformCtx,
87    locals: &mut Locals,
88    statements: &mut [Statement],
89) {
90    // We return the statements we want to keep, which must be a prefix of `block.statements`.
91    let statements_to_keep = match statements {
92        // Bounds checks for slices. They look like:
93        //   l := ptr_metadata(copy a)
94        //   b := copy x < copy l
95        //   assert(move b == true)
96        [Statement {
97            content:
98                RawStatement::Assign(len, Rvalue::UnaryOp(UnOp::PtrMetadata, Operand::Copy(len_op))),
99            ..
100        }, Statement {
101            content:
102                RawStatement::Assign(
103                    is_in_bounds,
104                    Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
105                ),
106            ..
107        }, Statement {
108            content:
109                RawStatement::Assert(Assert {
110                    cond: Operand::Move(cond),
111                    expected: true,
112                    ..
113                }),
114            ..
115        }, rest @ ..]
116            if lt_op2 == len && cond == is_in_bounds && len_op.ty().is_ref() =>
117        {
118            rest
119        }
120        // Sometimes that instead looks like:
121        //   a := &raw const *z
122        //   l := ptr_metadata(move a)
123        //   b := copy x < copy l
124        //   assert(move b == true)
125        [Statement {
126            content: RawStatement::Assign(reborrow, Rvalue::RawPtr(_, RefKind::Shared)),
127            ..
128        }, Statement {
129            content:
130                RawStatement::Assign(len, Rvalue::UnaryOp(UnOp::PtrMetadata, Operand::Move(len_op))),
131            ..
132        }, Statement {
133            content:
134                RawStatement::Assign(
135                    is_in_bounds,
136                    Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
137                ),
138            ..
139        }, Statement {
140            content:
141                RawStatement::Assert(Assert {
142                    cond: Operand::Move(cond),
143                    expected: true,
144                    ..
145                }),
146            ..
147        }, rest @ ..]
148            if reborrow == len_op && lt_op2 == len && cond == is_in_bounds =>
149        {
150            rest
151        }
152
153        // Zero checks for division and remainder. They look like:
154        //   b := copy y == const 0
155        //   assert(move b == false)
156        //   ...
157        //   res := x {/,%} move y;
158        //   ... or ...
159        //   b := const y == const 0
160        //   assert(move b == false)
161        //   ...
162        //   res := x {/,%} const y;
163        //
164        // This also overlaps with overflow checks for negation, which looks like:
165        //   is_min := x == INT::min
166        //   assert(move is_min == false)
167        //   ...
168        //   res := -x;
169        [Statement {
170            content:
171                RawStatement::Assign(is_zero, Rvalue::BinaryOp(BinOp::Eq, y_op, Operand::Const(_zero))),
172            ..
173        }, Statement {
174            content:
175                RawStatement::Assert(Assert {
176                    cond: Operand::Move(cond),
177                    expected: false,
178                    ..
179                }),
180            ..
181        }, rest @ ..]
182            if cond == is_zero =>
183        {
184            let found = make_binop_overflow_panic(rest, |bop, _, r| {
185                matches!(bop, BinOp::Div(_) | BinOp::Rem(_)) && equiv_op(r, y_op)
186            }) || make_unop_overflow_panic(rest, |unop, o| {
187                matches!(unop, UnOp::Neg(_)) && equiv_op(o, y_op)
188            });
189            if found {
190                rest
191            } else {
192                return;
193            }
194        }
195
196        // Overflow checks for signed division and remainder. They look like:
197        //   is_neg_1 := y == (-1)
198        //   is_min := x == INT::min
199        //   has_overflow := move (is_neg_1) & move (is_min)
200        //   assert(move has_overflow == false)
201        // Note here we don't need to update the operand to panic, as this was already done
202        // by the previous pass for division by zero.
203        [Statement {
204            content: RawStatement::Assign(is_neg_1, Rvalue::BinaryOp(BinOp::Eq, _y_op, _minus_1)),
205            ..
206        }, Statement {
207            content: RawStatement::Assign(is_min, Rvalue::BinaryOp(BinOp::Eq, _x_op, _int_min)),
208            ..
209        }, Statement {
210            content:
211                RawStatement::Assign(
212                    has_overflow,
213                    Rvalue::BinaryOp(BinOp::BitAnd, Operand::Move(and_op1), Operand::Move(and_op2)),
214                ),
215            ..
216        }, Statement {
217            content:
218                RawStatement::Assert(Assert {
219                    cond: Operand::Move(cond),
220                    expected: false,
221                    ..
222                }),
223            ..
224        }, rest @ ..]
225            if and_op1 == is_neg_1 && and_op2 == is_min && cond == has_overflow =>
226        {
227            rest
228        }
229
230        // Overflow checks for right/left shift. They can look like:
231        //   a := y as u32; // or another type
232        //   b := move a < const 32; // or another constant
233        //   assert(move b == true);
234        //   ...
235        //   res := x {<<,>>} y;
236        [Statement {
237            content: RawStatement::Assign(cast, Rvalue::UnaryOp(UnOp::Cast(_), y_op)),
238            ..
239        }, Statement {
240            content:
241                RawStatement::Assign(
242                    has_overflow,
243                    Rvalue::BinaryOp(BinOp::Lt, Operand::Move(lhs), Operand::Const(..)),
244                ),
245            ..
246        }, Statement {
247            content:
248                RawStatement::Assert(Assert {
249                    cond: Operand::Move(cond),
250                    expected: true,
251                    ..
252                }),
253            ..
254        }, rest @ ..]
255            if cond == has_overflow
256                && lhs == cast
257                && let Some(cast_local) = cast.as_local()
258                && !rest.iter().any(|st| uses_local(st, cast_local)) =>
259        {
260            let found = make_binop_overflow_panic(rest, |bop, _, r| {
261                matches!(bop, BinOp::Shl(_) | BinOp::Shr(_)) && equiv_op(r, y_op)
262            });
263            if found {
264                rest
265            } else {
266                return;
267            }
268        }
269        // or like:
270        //   b := y < const 32; // or another constant
271        //   assert(move b == true);
272        //   ...
273        //   res := x {<<,>>} y;
274        //
275        // this also overlaps with out of bounds checks for arrays, so we check for either;
276        // these look like:
277        //   b := copy y < const _
278        //   assert(move b == true)
279        //   ...
280        //   res := a[y];
281        [Statement {
282            content:
283                RawStatement::Assign(
284                    has_overflow,
285                    Rvalue::BinaryOp(BinOp::Lt, y_op, Operand::Const(..)),
286                ),
287            ..
288        }, Statement {
289            content:
290                RawStatement::Assert(Assert {
291                    cond: Operand::Move(cond),
292                    expected: true,
293                    ..
294                }),
295            ..
296        }, rest @ ..]
297            if cond == has_overflow =>
298        {
299            // look for a shift operation
300            let mut found = make_binop_overflow_panic(rest, |bop, _, r| {
301                matches!(bop, BinOp::Shl(_) | BinOp::Shr(_)) && equiv_op(r, y_op)
302            });
303            if !found {
304                // otherwise, look for an array access
305                for stmt in rest.iter_mut() {
306                    stmt.dyn_visit_in_body(|p: &Place| {
307                        if let Some((_, ProjectionElem::Index { offset, .. })) = p.as_projection()
308                            && equiv_op(offset, y_op)
309                        {
310                            found = true;
311                        }
312                    });
313                }
314            }
315
316            if found {
317                rest
318            } else {
319                return;
320            }
321        }
322
323        // Overflow checks for addition/subtraction/multiplication. They look like:
324        // ```text
325        //   r := x checked.+ y;
326        //   assert(move r.1 == false);
327        //   ...
328        //   z := move r.0;
329        // ```
330        // We replace that with:
331        // ```text
332        // z := x + y;
333        // ```
334        //
335        // But sometimes, because of constant promotion, we end up with a lone checked operation
336        // without assert. In that case we replace it with its wrapping equivalent.
337        [Statement {
338            content:
339                RawStatement::Assign(
340                    result,
341                    Rvalue::BinaryOp(
342                        binop @ (BinOp::AddChecked | BinOp::SubChecked | BinOp::MulChecked),
343                        _,
344                        _,
345                    ),
346                ),
347            ..
348        }, rest @ ..]
349            if let Some(result_local_id) = result.as_local() =>
350        {
351            // Look for uses of the overflow boolean.
352            let mut overflow_is_used = false;
353            for stmt in rest.iter_mut() {
354                stmt.dyn_visit_in_body(|p: &Place| {
355                    if let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(..), fid))) =
356                        p.as_projection()
357                        && fid.index() == 1
358                        && sub == result
359                    {
360                        overflow_is_used = true;
361                    }
362                });
363            }
364            // Check if the operation is followed by an assert.
365            let followed_by_assert = if let [Statement {
366                content:
367                    RawStatement::Assert(Assert {
368                        cond: Operand::Move(assert_cond),
369                        expected: false,
370                        ..
371                    }),
372                ..
373            }, ..] = rest
374                && let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(..), fid))) =
375                    assert_cond.as_projection()
376                && fid.index() == 1
377                && sub == result
378            {
379                true
380            } else {
381                false
382            };
383            if overflow_is_used && !followed_by_assert {
384                // The overflow boolean is used in a way that isn't a builtin overflow check; we
385                // change nothing.
386                return;
387            }
388
389            if followed_by_assert {
390                // We have a compiler-emitted assert. We replace the operation with one that has
391                // panic-on-overflow semantics.
392                *binop = binop.with_overflow(OverflowMode::Panic);
393                // The failure behavior is part of the binop now, so we remove the assert.
394                rest[0].content = RawStatement::Nop;
395            } else {
396                // The overflow boolean is not used, we replace the operations with wrapping
397                // semantics.
398                *binop = binop.with_overflow(OverflowMode::Wrap);
399            }
400            // Fixup the local type.
401            let result_local = &mut locals.locals[result_local_id];
402            result_local.ty = result_local.ty.as_tuple().unwrap()[0].clone();
403            // Fixup the place type.
404            let new_result_place = locals.place_for_var(result_local_id);
405            // Replace uses of `r.0` with `r`.
406            for stmt in rest.iter_mut() {
407                stmt.dyn_visit_in_body_mut(|p: &mut Place| {
408                    if let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(..), fid))) =
409                        p.as_projection()
410                        && sub == result
411                    {
412                        assert_eq!(fid.index(), 0);
413                        *p = new_result_place.clone()
414                    }
415                });
416            }
417            *result = new_result_place;
418            return;
419        }
420
421        _ => return,
422    };
423
424    // Remove the statements we're not keeping.
425    let keep_len = statements_to_keep.len();
426    for i in 0..statements.len() - keep_len {
427        statements[i].content = RawStatement::Nop;
428    }
429}
430
431pub struct Transform;
432impl UllbcPass for Transform {
433    fn transform_body(&self, ctx: &mut TransformCtx, b: &mut ExprBody) {
434        b.transform_sequences_fwd(|locals, seq| {
435            remove_dynamic_checks(ctx, locals, seq);
436            Vec::new()
437        });
438    }
439}