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}