Skip to content

Commit a9e14fe

Browse files
vonakaFedor Ryabinincarolynzech
authored
Ensure that contract closures are FnOnce (#4151)
Rust believes that Kani's contract closures are `FnMut`. This prevents us from writing contracts for functions that return mutable references to their input arguments (#3764). To ensure Rust correctly infers these closures as `FnOnce`, they need to be wrapped in a dummy function that explicitly requires an `FnOnce`. This wrapping must be done at the point of closure definition, as doing it later, when calling the function, doesn't seem to have any effect. Resolves #3764 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Co-authored-by: Fedor Ryabinin <rfedor@amazon.com> Co-authored-by: Carolyn Zech <cmzech@amazon.com>
1 parent 96f7e59 commit a9e14fe

File tree

12 files changed

+413
-222
lines changed

12 files changed

+413
-222
lines changed

library/kani_macros/src/sysroot/contracts/assert.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl<'a> ContractConditionsHandler<'a> {
2929
quote!(
3030
#[kanitool::is_contract_generated(assert)]
3131
#[allow(dead_code, unused_variables, unused_mut)]
32-
let mut #assert_ident = || #output #body;
32+
let mut #assert_ident = kani_force_fn_once(|| #output #body);
3333
)
3434
}
3535

@@ -49,9 +49,9 @@ impl<'a> ContractConditionsHandler<'a> {
4949
let result = Ident::new(INTERNAL_RESULT_IDENT, Span::call_site());
5050

5151
parse_quote!(
52-
let mut body_wrapper = || #output {
52+
let mut body_wrapper = kani_force_fn_once(|| #output {
5353
#(#stmts)*
54-
};
54+
});
5555
let #result : #return_type = #body_wrapper_ident();
5656
#result
5757
)

library/kani_macros/src/sysroot/contracts/bootstrap.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ impl<'a> ContractConditionsHandler<'a> {
4747
#[kanitool::asserted_with = #assert_name]
4848
#[kanitool::modifies_wrapper = #modifies_name]
4949
#vis #sig {
50+
// Dummy functions used to force the compiler to annotate Kani's
51+
// closures as `FnOnce`. Without this, Rust infers the generated closures as `FnMut`,
52+
// preventing us from writing contracts for functions returning mutable references.
53+
// See https://github.com/model-checking/kani/issues/3764
54+
#[inline(never)]
55+
#[kanitool::fn_marker = "kani_force_fn_once"]
56+
const fn kani_force_fn_once<T, F: FnOnce() -> T>(f: F) -> F {
57+
f
58+
}
59+
#[inline(never)]
60+
#[kanitool::fn_marker = "kani_force_fn_once_with_args"]
61+
const fn kani_force_fn_once_with_args<A, T, F: FnOnce(A) -> T>(f: F) -> F {
62+
f
63+
}
5064
// Dummy function used to force the compiler to capture the environment.
5165
// We cannot call closures inside constant functions.
5266
// This function gets replaced by `kani::internal::call_closure`.
@@ -125,7 +139,7 @@ impl<'a> ContractConditionsHandler<'a> {
125139
quote!(
126140
#[kanitool::is_contract_generated(recursion_check)]
127141
#[allow(dead_code, unused_variables, unused_mut)]
128-
let mut #recursion_ident = || #output
142+
let mut #recursion_ident = kani_force_fn_once(|| #output
129143
{
130144
#[kanitool::recursion_tracker]
131145
static mut REENTRY: bool = false;
@@ -139,7 +153,7 @@ impl<'a> ContractConditionsHandler<'a> {
139153
unsafe { REENTRY = false };
140154
#result
141155
}
142-
};
156+
});
143157
)
144158
}
145159

library/kani_macros/src/sysroot/contracts/check.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ impl<'a> ContractConditionsHandler<'a> {
114114
quote!(
115115
#[kanitool::is_contract_generated(check)]
116116
#[allow(dead_code, unused_variables, unused_mut)]
117-
let mut #check_ident = || #output #body;
117+
let mut #check_ident = kani_force_fn_once(|| #output #body);
118118
)
119119
}
120120

@@ -144,10 +144,10 @@ impl<'a> ContractConditionsHandler<'a> {
144144
quote!(
145145
#[kanitool::is_contract_generated(wrapper)]
146146
#[allow(dead_code, unused_variables, unused_mut)]
147-
let mut #modifies_ident = |#wrapper_ident: _| #output {
147+
let mut #modifies_ident = kani_force_fn_once_with_args(|#wrapper_ident: _| #output {
148148
#redefs
149149
#(#stmts)*
150-
};
150+
});
151151
)
152152
}
153153

@@ -157,7 +157,13 @@ impl<'a> ContractConditionsHandler<'a> {
157157
let Stmt::Local(Local { init: Some(LocalInit { expr, .. }), .. }) = closure_stmt else {
158158
unreachable!()
159159
};
160-
let Expr::Closure(closure) = expr.as_ref() else { unreachable!() };
160+
// The closure is wrapped into `kani_force_fn_once_with_args`
161+
let Expr::Call(call) = expr.as_ref() else { unreachable!() };
162+
if call.args.len() != 1 {
163+
unreachable!()
164+
}
165+
let expr = call.args.first().unwrap();
166+
let Expr::Closure(closure) = expr else { unreachable!() };
161167
let Expr::Block(body) = closure.body.as_ref() else { unreachable!() };
162168
let stream = self.modifies_closure(&closure.output, &body.block, TokenStream2::new());
163169
*closure_stmt = syn::parse2(stream).unwrap();

library/kani_macros/src/sysroot/contracts/helpers.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,25 @@ pub fn closure_body(closure: &mut Stmt) -> &mut ExprBlock {
123123
let Stmt::Local(Local { init: Some(LocalInit { expr, .. }), .. }) = closure else {
124124
unreachable!()
125125
};
126-
let Expr::Closure(closure) = expr.as_mut() else { unreachable!() };
127-
let Expr::Block(body) = closure.body.as_mut() else { unreachable!() };
128-
body
126+
match expr.as_mut() {
127+
// The case of closures wrapped in `kani_force_fn_once`
128+
Expr::Call(call) if call.args.len() == 1 => {
129+
let arg = call.args.first_mut().unwrap();
130+
match arg {
131+
Expr::Closure(closure) => {
132+
let Expr::Block(body) = closure.body.as_mut() else { unreachable!() };
133+
body
134+
}
135+
_ => unreachable!(),
136+
}
137+
}
138+
139+
Expr::Closure(closure) => {
140+
let Expr::Block(body) = closure.body.as_mut() else { unreachable!() };
141+
body
142+
}
143+
_ => unreachable!(),
144+
}
129145
}
130146

131147
/// Does the provided path have the same chain of identifiers as `mtch` (match)

0 commit comments

Comments
 (0)