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 crate::ast::*;
8use crate::transform::TransformCtx;
9use crate::ullbc_ast::{ExprBody, RawStatement, Statement};
10
11use super::ctx::UllbcPass;
12
13/// Rustc inserts dybnamic checks during MIR lowering. They all end in an `Assert` statement (and
14/// this is the only use of this statement).
15fn remove_dynamic_checks(_ctx: &mut TransformCtx, statements: &mut [Statement]) {
16    // We return the statements we want to keep, which must be a prefix of `block.statements`.
17    let statements_to_keep = match statements {
18        // Bounds checks for slices. They look like:
19        //   l := ptr_metadata(copy a)
20        //   b := copy x < copy l
21        //   assert(move b == true)
22        [Statement {
23            content:
24                RawStatement::Assign(len, Rvalue::UnaryOp(UnOp::PtrMetadata, Operand::Copy(len_op))),
25            ..
26        }, Statement {
27            content:
28                RawStatement::Assign(
29                    is_in_bounds,
30                    Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
31                ),
32            ..
33        }, Statement {
34            content:
35                RawStatement::Assert(Assert {
36                    cond: Operand::Move(cond),
37                    expected: true,
38                    ..
39                }),
40            ..
41        }, rest @ ..]
42            if lt_op2 == len && cond == is_in_bounds && len_op.ty().is_ref() =>
43        {
44            rest
45        }
46        // Sometimes that instead looks like:
47        //   a := &raw const *z
48        //   l := ptr_metadata(move a)
49        //   b := copy x < copy l
50        //   assert(move b == true)
51        [Statement {
52            content: RawStatement::Assign(reborrow, Rvalue::RawPtr(_, RefKind::Shared)),
53            ..
54        }, Statement {
55            content:
56                RawStatement::Assign(len, Rvalue::UnaryOp(UnOp::PtrMetadata, Operand::Move(len_op))),
57            ..
58        }, Statement {
59            content:
60                RawStatement::Assign(
61                    is_in_bounds,
62                    Rvalue::BinaryOp(BinOp::Lt, _, Operand::Copy(lt_op2)),
63                ),
64            ..
65        }, Statement {
66            content:
67                RawStatement::Assert(Assert {
68                    cond: Operand::Move(cond),
69                    expected: true,
70                    ..
71                }),
72            ..
73        }, rest @ ..]
74            if reborrow == len_op && lt_op2 == len && cond == is_in_bounds =>
75        {
76            rest
77        }
78
79        // Bounds checks for arrays. They look like:
80        //   b := copy x < const _
81        //   assert(move b == true)
82        [Statement {
83            content:
84                RawStatement::Assign(is_in_bounds, Rvalue::BinaryOp(BinOp::Lt, _, Operand::Const(_))),
85            ..
86        }, Statement {
87            content:
88                RawStatement::Assert(Assert {
89                    cond: Operand::Move(cond),
90                    expected: true,
91                    ..
92                }),
93            ..
94        }, rest @ ..]
95            if cond == is_in_bounds =>
96        {
97            rest
98        }
99
100        // Zero checks for division and remainder. They look like:
101        //   b := copy x == const 0
102        //   assert(move b == false)
103        [Statement {
104            content:
105                RawStatement::Assign(is_zero, Rvalue::BinaryOp(BinOp::Eq, _, Operand::Const(_zero))),
106            ..
107        }, Statement {
108            content:
109                RawStatement::Assert(Assert {
110                    cond: Operand::Move(cond),
111                    expected,
112                    ..
113                }),
114            ..
115        }, rest @ ..]
116            if cond == is_zero && *expected == false =>
117        {
118            rest
119        }
120
121        // Overflow checks for signed division and remainder. They look like:
122        //   is_neg_1 := y == (-1)
123        //   is_min := x == INT::min
124        //   has_overflow := move (is_neg_1) & move (is_min)
125        //   assert(move has_overflow == false)
126        [Statement {
127            content: RawStatement::Assign(is_neg_1, Rvalue::BinaryOp(BinOp::Eq, _y_op, _minus_1)),
128            ..
129        }, Statement {
130            content: RawStatement::Assign(is_min, Rvalue::BinaryOp(BinOp::Eq, _x_op, _int_min)),
131            ..
132        }, Statement {
133            content:
134                RawStatement::Assign(
135                    has_overflow,
136                    Rvalue::BinaryOp(BinOp::BitAnd, Operand::Move(and_op1), Operand::Move(and_op2)),
137                ),
138            ..
139        }, Statement {
140            content:
141                RawStatement::Assert(Assert {
142                    cond: Operand::Move(cond),
143                    expected,
144                    ..
145                }),
146            ..
147        }, rest @ ..]
148            if and_op1 == is_neg_1
149                && and_op2 == is_min
150                && cond == has_overflow
151                && *expected == false =>
152        {
153            rest
154        }
155
156        // Overflow checks for right/left shift. They can look like:
157        //   x := ...;
158        //   b := move x < const 32; // or another constant
159        //   assert(move b == true);
160        [Statement {
161            content: RawStatement::Assign(x, _),
162            ..
163        }, Statement {
164            content:
165                RawStatement::Assign(
166                    has_overflow,
167                    Rvalue::BinaryOp(BinOp::Lt, Operand::Move(lt_op2), Operand::Const(..)),
168                ),
169            ..
170        }, Statement {
171            content:
172                RawStatement::Assert(Assert {
173                    cond: Operand::Move(cond),
174                    expected,
175                    ..
176                }),
177            ..
178        }, rest @ ..]
179            if lt_op2 == x && cond == has_overflow && *expected == true =>
180        {
181            rest
182        }
183        // They can also look like:
184        //   b := const c < const 32; // or another constant
185        //   assert(move b == true);
186        [Statement {
187            content:
188                RawStatement::Assign(
189                    has_overflow,
190                    Rvalue::BinaryOp(BinOp::Lt, Operand::Const(..), Operand::Const(..)),
191                ),
192            ..
193        }, Statement {
194            content:
195                RawStatement::Assert(Assert {
196                    cond: Operand::Move(cond),
197                    expected,
198                    ..
199                }),
200            ..
201        }, rest @ ..]
202            if cond == has_overflow && *expected == true =>
203        {
204            rest
205        }
206
207        // Overflow checks for addition/subtraction/multiplication. They look like:
208        //   r := x checked.+ y;
209        //   assert(move r.1 == false);
210        // They only happen in constants because we compile with `-C opt-level=3`. They span two
211        // blocks so we remove them in a later pass.
212        [Statement {
213            content:
214                RawStatement::Assign(
215                    result,
216                    Rvalue::BinaryOp(BinOp::CheckedAdd | BinOp::CheckedSub | BinOp::CheckedMul, ..),
217                ),
218            ..
219        }, Statement {
220            content:
221                RawStatement::Assert(Assert {
222                    cond: Operand::Move(cond),
223                    expected,
224                    ..
225                }),
226            ..
227        }, ..]
228            if result.is_local()
229                && let Some((sub, ProjectionElem::Field(FieldProjKind::Tuple(2), p_id))) =
230                    cond.as_projection()
231                && sub.is_local()
232                && cond.local_id() == result.local_id()
233                && p_id.index() == 1
234                && *expected == false =>
235        {
236            // We leave this assert intact; it will be simplified in
237            // [`remove_arithmetic_overflow_checks`].
238            return;
239        }
240
241        _ => return,
242    };
243
244    // Remove the statements.
245    let keep_len = statements_to_keep.len();
246    for i in 0..statements.len() - keep_len {
247        statements[i].content = RawStatement::Nop;
248    }
249}
250
251pub struct Transform;
252impl UllbcPass for Transform {
253    fn transform_body(&self, ctx: &mut TransformCtx, b: &mut ExprBody) {
254        for block in b.body.iter_mut() {
255            block.transform_sequences(|seq| {
256                remove_dynamic_checks(ctx, seq);
257                Vec::new()
258            });
259        }
260    }
261}