Skip to content

Commit f0c9df3

Browse files
authored
Unrolled build for #144438
Rollup merge of #144438 - dawidl022:contracts/guarded-lowering, r=oli-obk Guard HIR lowered contracts with `contract_checks` Refactor contract HIR lowering to ensure no contract code is executed when contract-checks are disabled. The call to `contract_checks` is moved to inside the lowered fn body, and contract closures are built conditionally, ensuring no side-effects present in contracts occur when those are disabled. This partially addresses #139548, i.e. the bad behavior no longer happens with contract checks disabled (`-Zcontract-checks=no`). The change is made in preparation for adding contract variable declarations - variables declared before the `requires` assertion, and accessible from both `requires` and `ensures`, but not in the function body (PR #144444). As those declarations may also have side-effects, it's good to guard them with `contract_checks` - the new lowering approach allows for this to be done easily. Contracts tracking issue: #128044 **Known limiatations**: - It is still possible to early return from the *function* from within a contract, e.g. ```rust #[ensures({if x > 0 { return 0 }; |_| true})] fn foo(x: u32) -> i32 { 42 } ``` When `foo` is called with an argument greater than 0, instead of `42`, `0` will be returned. As this is not a regression, it is not addressed in this PR. However, it may be worth revisiting later down the line, as users may expect a form of early return from *contract specifications*, and so returning from the entire *function* could cause confusion. - ~Contracts are still not optimised out when disabled. Currently, even when contracts are disabled, the code generated causes existing optimisations to fail, meaning even disabled contracts could impact runtime performance. This issue is blocking #136578, and has not been addressed in this PR, i.e. the `mir-opt` and `codegen` tests that fail in #136578 still fail with these new HIR lowering changes.~ Contracts should now be optimised out when disabled, however some regressions tests still need to be added to be sure that is indeed the case.
2 parents 779e19d + 4d23c41 commit f0c9df3

File tree

24 files changed

+596
-209
lines changed

24 files changed

+596
-209
lines changed
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
use thin_vec::thin_vec;
2+
3+
use crate::LoweringContext;
4+
5+
impl<'a, 'hir> LoweringContext<'a, 'hir> {
6+
/// Lowered contracts are guarded with the `contract_checks` compiler flag,
7+
/// i.e. the flag turns into a boolean guard in the lowered HIR. The reason
8+
/// for not eliminating the contract code entirely when the `contract_checks`
9+
/// flag is disabled is so that contracts can be type checked, even when
10+
/// they are disabled, which avoids them becoming stale (i.e. out of sync
11+
/// with the codebase) over time.
12+
///
13+
/// The optimiser should be able to eliminate all contract code guarded
14+
/// by `if false`, leaving the original body intact when runtime contract
15+
/// checks are disabled.
16+
pub(super) fn lower_contract(
17+
&mut self,
18+
body: impl FnOnce(&mut Self) -> rustc_hir::Expr<'hir>,
19+
contract: &rustc_ast::FnContract,
20+
) -> rustc_hir::Expr<'hir> {
21+
match (&contract.requires, &contract.ensures) {
22+
(Some(req), Some(ens)) => {
23+
// Lower the fn contract, which turns:
24+
//
25+
// { body }
26+
//
27+
// into:
28+
//
29+
// let __postcond = if contract_checks {
30+
// contract_check_requires(PRECOND);
31+
// Some(|ret_val| POSTCOND)
32+
// } else {
33+
// None
34+
// };
35+
// {
36+
// let ret = { body };
37+
//
38+
// if contract_checks {
39+
// contract_check_ensures(__postcond, ret)
40+
// } else {
41+
// ret
42+
// }
43+
// }
44+
45+
let precond = self.lower_precond(req);
46+
let postcond_checker = self.lower_postcond_checker(ens);
47+
48+
let contract_check =
49+
self.lower_contract_check_with_postcond(Some(precond), postcond_checker);
50+
51+
let wrapped_body =
52+
self.wrap_body_with_contract_check(body, contract_check, postcond_checker.span);
53+
self.expr_block(wrapped_body)
54+
}
55+
(None, Some(ens)) => {
56+
// Lower the fn contract, which turns:
57+
//
58+
// { body }
59+
//
60+
// into:
61+
//
62+
// let __postcond = if contract_checks {
63+
// Some(|ret_val| POSTCOND)
64+
// } else {
65+
// None
66+
// };
67+
// {
68+
// let ret = { body };
69+
//
70+
// if contract_checks {
71+
// contract_check_ensures(__postcond, ret)
72+
// } else {
73+
// ret
74+
// }
75+
// }
76+
77+
let postcond_checker = self.lower_postcond_checker(ens);
78+
let contract_check =
79+
self.lower_contract_check_with_postcond(None, postcond_checker);
80+
81+
let wrapped_body =
82+
self.wrap_body_with_contract_check(body, contract_check, postcond_checker.span);
83+
self.expr_block(wrapped_body)
84+
}
85+
(Some(req), None) => {
86+
// Lower the fn contract, which turns:
87+
//
88+
// { body }
89+
//
90+
// into:
91+
//
92+
// {
93+
// if contracts_checks {
94+
// contract_requires(PRECOND);
95+
// }
96+
// body
97+
// }
98+
let precond = self.lower_precond(req);
99+
let precond_check = self.lower_contract_check_just_precond(precond);
100+
101+
let body = self.arena.alloc(body(self));
102+
103+
// Flatten the body into precond check, then body.
104+
let wrapped_body = self.block_all(
105+
body.span,
106+
self.arena.alloc_from_iter([precond_check].into_iter()),
107+
Some(body),
108+
);
109+
self.expr_block(wrapped_body)
110+
}
111+
(None, None) => body(self),
112+
}
113+
}
114+
115+
/// Lower the precondition check intrinsic.
116+
fn lower_precond(&mut self, req: &Box<rustc_ast::Expr>) -> rustc_hir::Stmt<'hir> {
117+
let lowered_req = self.lower_expr_mut(&req);
118+
let req_span = self.mark_span_with_reason(
119+
rustc_span::DesugaringKind::Contract,
120+
lowered_req.span,
121+
None,
122+
);
123+
let precond = self.expr_call_lang_item_fn_mut(
124+
req_span,
125+
rustc_hir::LangItem::ContractCheckRequires,
126+
&*arena_vec![self; lowered_req],
127+
);
128+
self.stmt_expr(req.span, precond)
129+
}
130+
131+
fn lower_postcond_checker(
132+
&mut self,
133+
ens: &Box<rustc_ast::Expr>,
134+
) -> &'hir rustc_hir::Expr<'hir> {
135+
let ens_span = self.lower_span(ens.span);
136+
let ens_span =
137+
self.mark_span_with_reason(rustc_span::DesugaringKind::Contract, ens_span, None);
138+
let lowered_ens = self.lower_expr_mut(&ens);
139+
self.expr_call_lang_item_fn(
140+
ens_span,
141+
rustc_hir::LangItem::ContractBuildCheckEnsures,
142+
&*arena_vec![self; lowered_ens],
143+
)
144+
}
145+
146+
fn lower_contract_check_just_precond(
147+
&mut self,
148+
precond: rustc_hir::Stmt<'hir>,
149+
) -> rustc_hir::Stmt<'hir> {
150+
let stmts = self.arena.alloc_from_iter([precond].into_iter());
151+
152+
let then_block_stmts = self.block_all(precond.span, stmts, None);
153+
let then_block = self.arena.alloc(self.expr_block(&then_block_stmts));
154+
155+
let precond_check = rustc_hir::ExprKind::If(
156+
self.arena.alloc(self.expr_bool_literal(precond.span, self.tcx.sess.contract_checks())),
157+
then_block,
158+
None,
159+
);
160+
161+
let precond_check = self.expr(precond.span, precond_check);
162+
self.stmt_expr(precond.span, precond_check)
163+
}
164+
165+
fn lower_contract_check_with_postcond(
166+
&mut self,
167+
precond: Option<rustc_hir::Stmt<'hir>>,
168+
postcond_checker: &'hir rustc_hir::Expr<'hir>,
169+
) -> &'hir rustc_hir::Expr<'hir> {
170+
let stmts = self.arena.alloc_from_iter(precond.into_iter());
171+
let span = match precond {
172+
Some(precond) => precond.span,
173+
None => postcond_checker.span,
174+
};
175+
176+
let postcond_checker = self.arena.alloc(self.expr_enum_variant_lang_item(
177+
postcond_checker.span,
178+
rustc_hir::lang_items::LangItem::OptionSome,
179+
&*arena_vec![self; *postcond_checker],
180+
));
181+
let then_block_stmts = self.block_all(span, stmts, Some(postcond_checker));
182+
let then_block = self.arena.alloc(self.expr_block(&then_block_stmts));
183+
184+
let none_expr = self.arena.alloc(self.expr_enum_variant_lang_item(
185+
postcond_checker.span,
186+
rustc_hir::lang_items::LangItem::OptionNone,
187+
Default::default(),
188+
));
189+
let else_block = self.block_expr(none_expr);
190+
let else_block = self.arena.alloc(self.expr_block(else_block));
191+
192+
let contract_check = rustc_hir::ExprKind::If(
193+
self.arena.alloc(self.expr_bool_literal(span, self.tcx.sess.contract_checks())),
194+
then_block,
195+
Some(else_block),
196+
);
197+
self.arena.alloc(self.expr(span, contract_check))
198+
}
199+
200+
fn wrap_body_with_contract_check(
201+
&mut self,
202+
body: impl FnOnce(&mut Self) -> rustc_hir::Expr<'hir>,
203+
contract_check: &'hir rustc_hir::Expr<'hir>,
204+
postcond_span: rustc_span::Span,
205+
) -> &'hir rustc_hir::Block<'hir> {
206+
let check_ident: rustc_span::Ident =
207+
rustc_span::Ident::from_str_and_span("__ensures_checker", postcond_span);
208+
let (check_hir_id, postcond_decl) = {
209+
// Set up the postcondition `let` statement.
210+
let (checker_pat, check_hir_id) = self.pat_ident_binding_mode_mut(
211+
postcond_span,
212+
check_ident,
213+
rustc_hir::BindingMode::NONE,
214+
);
215+
(
216+
check_hir_id,
217+
self.stmt_let_pat(
218+
None,
219+
postcond_span,
220+
Some(contract_check),
221+
self.arena.alloc(checker_pat),
222+
rustc_hir::LocalSource::Contract,
223+
),
224+
)
225+
};
226+
227+
// Install contract_ensures so we will intercept `return` statements,
228+
// then lower the body.
229+
self.contract_ensures = Some((postcond_span, check_ident, check_hir_id));
230+
let body = self.arena.alloc(body(self));
231+
232+
// Finally, inject an ensures check on the implicit return of the body.
233+
let body = self.inject_ensures_check(body, postcond_span, check_ident, check_hir_id);
234+
235+
// Flatten the body into precond, then postcond, then wrapped body.
236+
let wrapped_body = self.block_all(
237+
body.span,
238+
self.arena.alloc_from_iter([postcond_decl].into_iter()),
239+
Some(body),
240+
);
241+
wrapped_body
242+
}
243+
244+
/// Create an `ExprKind::Ret` that is optionally wrapped by a call to check
245+
/// a contract ensures clause, if it exists.
246+
pub(super) fn checked_return(
247+
&mut self,
248+
opt_expr: Option<&'hir rustc_hir::Expr<'hir>>,
249+
) -> rustc_hir::ExprKind<'hir> {
250+
let checked_ret =
251+
if let Some((check_span, check_ident, check_hir_id)) = self.contract_ensures {
252+
let expr = opt_expr.unwrap_or_else(|| self.expr_unit(check_span));
253+
Some(self.inject_ensures_check(expr, check_span, check_ident, check_hir_id))
254+
} else {
255+
opt_expr
256+
};
257+
rustc_hir::ExprKind::Ret(checked_ret)
258+
}
259+
260+
/// Wraps an expression with a call to the ensures check before it gets returned.
261+
pub(super) fn inject_ensures_check(
262+
&mut self,
263+
expr: &'hir rustc_hir::Expr<'hir>,
264+
span: rustc_span::Span,
265+
cond_ident: rustc_span::Ident,
266+
cond_hir_id: rustc_hir::HirId,
267+
) -> &'hir rustc_hir::Expr<'hir> {
268+
// {
269+
// let ret = { body };
270+
//
271+
// if contract_checks {
272+
// contract_check_ensures(__postcond, ret)
273+
// } else {
274+
// ret
275+
// }
276+
// }
277+
let ret_ident: rustc_span::Ident = rustc_span::Ident::from_str_and_span("__ret", span);
278+
279+
// Set up the return `let` statement.
280+
let (ret_pat, ret_hir_id) =
281+
self.pat_ident_binding_mode_mut(span, ret_ident, rustc_hir::BindingMode::NONE);
282+
283+
let ret_stmt = self.stmt_let_pat(
284+
None,
285+
span,
286+
Some(expr),
287+
self.arena.alloc(ret_pat),
288+
rustc_hir::LocalSource::Contract,
289+
);
290+
291+
let ret = self.expr_ident(span, ret_ident, ret_hir_id);
292+
293+
let cond_fn = self.expr_ident(span, cond_ident, cond_hir_id);
294+
let contract_check = self.expr_call_lang_item_fn_mut(
295+
span,
296+
rustc_hir::LangItem::ContractCheckEnsures,
297+
arena_vec![self; *cond_fn, *ret],
298+
);
299+
let contract_check = self.arena.alloc(contract_check);
300+
let call_expr = self.block_expr_block(contract_check);
301+
302+
// same ident can't be used in 2 places, so we create a new one for the
303+
// else branch
304+
let ret = self.expr_ident(span, ret_ident, ret_hir_id);
305+
let ret_block = self.block_expr_block(ret);
306+
307+
let contracts_enabled: rustc_hir::Expr<'_> =
308+
self.expr_bool_literal(span, self.tcx.sess.contract_checks());
309+
let contract_check = self.arena.alloc(self.expr(
310+
span,
311+
rustc_hir::ExprKind::If(
312+
self.arena.alloc(contracts_enabled),
313+
call_expr,
314+
Some(ret_block),
315+
),
316+
));
317+
318+
let attrs: rustc_ast::AttrVec = thin_vec![self.unreachable_code_attr(span)];
319+
self.lower_attrs(contract_check.hir_id, &attrs, span, rustc_hir::Target::Expression);
320+
321+
let ret_block = self.block_all(span, arena_vec![self; ret_stmt], Some(contract_check));
322+
self.arena.alloc(self.expr_block(self.arena.alloc(ret_block)))
323+
}
324+
}

0 commit comments

Comments
 (0)