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}