Skip to content

Commit 7ad4d1c

Browse files
authored
Enable an #[safety_constraint(...)] attribute helper for the Arbitrary and Invariant macros (rust-lang#3283)
This PR enables an `#[safety_constraint(...)]` attribute helper for the `#[derive(Arbitrary)]` and `#[derive(Invariant)]` macro. For the `Invariant` derive macro, this allows users to derive more sophisticated invariants for their data types by annotating individual named fields with the `#[safety_constraint(<cond>)]` attribute, where `<cond>` represents the predicate to be evaluated for the corresponding field. In addition, the implementation always checks `#field.is_safe()` for each field. For example, let's say we are working with the `Point` type from rust-lang#3250 ```rs #[derive(kani::Invariant)] struct Point<X, Y> { x: X, y: Y, } ``` and we need to extend it to only allow positive values for both `x` and `y`. With the `[safety_constraint(...)]` attribute, we can achieve this without explicitly writing the `impl Invariant for ...` as follows: ```rs #[derive(kani::Invariant)] struct PositivePoint { #[safety_constraint(*x >= 0)] x: i32, #[safety_constraint(*y >= 0)] y: i32, } ``` For the `Arbitrary` derive macro, this allows users to derive more sophisticated `kani::any()` generators that respect the specified invariants. In other words, the `kani::any()` will assume any invariants specified through the `#[safety_constraint(...)]` attribute helper. Going back to the `PositivePoint` example, we'd expect this harness to be successful: ```rs #[kani::proof] fn check_invariant_helper_ok() { let pos_point: PositivePoint = kani::any(); assert!(pos_point.x >= 0); assert!(pos_point.y >= 0); } ``` The PR includes tests to ensure it's working as expected, in addition to UI tests checking for cases where the arguments provided to the macro are incorrect. Happy to add any other cases that you feel are missing. Related rust-lang#3095
1 parent 5d6bf69 commit 7ad4d1c

File tree

24 files changed

+668
-20
lines changed

24 files changed

+668
-20
lines changed

library/kani_macros/src/derive.rs

+186-16
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,30 @@ pub fn expand_derive_arbitrary(item: proc_macro::TokenStream) -> proc_macro::Tok
2828
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
2929

3030
let body = fn_any_body(&item_name, &derive_item.data);
31-
let expanded = quote! {
32-
// The generated implementation.
33-
impl #impl_generics kani::Arbitrary for #item_name #ty_generics #where_clause {
34-
fn any() -> Self {
35-
#body
31+
32+
// Get the safety constraints (if any) to produce type-safe values
33+
let safety_conds_opt = safety_conds(&item_name, &derive_item.data);
34+
35+
let expanded = if let Some(safety_cond) = safety_conds_opt {
36+
let field_refs = field_refs(&item_name, &derive_item.data);
37+
quote! {
38+
// The generated implementation.
39+
impl #impl_generics kani::Arbitrary for #item_name #ty_generics #where_clause {
40+
fn any() -> Self {
41+
let obj = #body;
42+
#field_refs
43+
kani::assume(#safety_cond);
44+
obj
45+
}
46+
}
47+
}
48+
} else {
49+
quote! {
50+
// The generated implementation.
51+
impl #impl_generics kani::Arbitrary for #item_name #ty_generics #where_clause {
52+
fn any() -> Self {
53+
#body
54+
}
3655
}
3756
}
3857
};
@@ -75,6 +94,103 @@ fn fn_any_body(ident: &Ident, data: &Data) -> TokenStream {
7594
}
7695
}
7796

97+
/// Parse the condition expressions in `#[safety_constraint(<cond>)]` attached to struct
98+
/// fields and, it at least one was found, generate a conjunction to be assumed.
99+
///
100+
/// For example, if we're deriving implementations for the struct
101+
/// ```
102+
/// #[derive(Arbitrary)]
103+
/// #[derive(Invariant)]
104+
/// struct PositivePoint {
105+
/// #[safety_constraint(*x >= 0)]
106+
/// x: i32,
107+
/// #[safety_constraint(*y >= 0)]
108+
/// y: i32,
109+
/// }
110+
/// ```
111+
/// this function will generate the `TokenStream`
112+
/// ```
113+
/// *x >= 0 && *y >= 0
114+
/// ```
115+
/// which can be passed to `kani::assume` to constrain the values generated
116+
/// through the `Arbitrary` impl so that they are type-safe by construction.
117+
fn safety_conds(ident: &Ident, data: &Data) -> Option<TokenStream> {
118+
match data {
119+
Data::Struct(struct_data) => safety_conds_inner(ident, &struct_data.fields),
120+
Data::Enum(_) => None,
121+
Data::Union(_) => None,
122+
}
123+
}
124+
125+
/// Generates an expression resulting from the conjunction of conditions
126+
/// specified as safety constraints for each field. See `safety_conds` for more details.
127+
fn safety_conds_inner(ident: &Ident, fields: &Fields) -> Option<TokenStream> {
128+
match fields {
129+
Fields::Named(ref fields) => {
130+
let conds: Vec<TokenStream> =
131+
fields.named.iter().filter_map(|field| parse_safety_expr(ident, field)).collect();
132+
if !conds.is_empty() { Some(quote! { #(#conds)&&* }) } else { None }
133+
}
134+
Fields::Unnamed(_) => None,
135+
Fields::Unit => None,
136+
}
137+
}
138+
139+
/// Generates the sequence of expressions to initialize the variables used as
140+
/// references to the struct fields.
141+
///
142+
/// For example, if we're deriving implementations for the struct
143+
/// ```
144+
/// #[derive(Arbitrary)]
145+
/// #[derive(Invariant)]
146+
/// struct PositivePoint {
147+
/// #[safety_constraint(*x >= 0)]
148+
/// x: i32,
149+
/// #[safety_constraint(*y >= 0)]
150+
/// y: i32,
151+
/// }
152+
/// ```
153+
/// this function will generate the `TokenStream`
154+
/// ```
155+
/// let x = &obj.x;
156+
/// let y = &obj.y;
157+
/// ```
158+
/// which allows us to refer to the struct fields without using `self`.
159+
/// Note that the actual stream is generated in the `field_refs_inner` function.
160+
fn field_refs(ident: &Ident, data: &Data) -> TokenStream {
161+
match data {
162+
Data::Struct(struct_data) => field_refs_inner(ident, &struct_data.fields),
163+
Data::Enum(_) => unreachable!(),
164+
Data::Union(_) => unreachable!(),
165+
}
166+
}
167+
168+
/// Generates the sequence of expressions to initialize the variables used as
169+
/// references to the struct fields. See `field_refs` for more details.
170+
fn field_refs_inner(_ident: &Ident, fields: &Fields) -> TokenStream {
171+
match fields {
172+
Fields::Named(ref fields) => {
173+
let field_refs: Vec<TokenStream> = fields
174+
.named
175+
.iter()
176+
.map(|field| {
177+
let name = &field.ident;
178+
quote_spanned! {field.span()=>
179+
let #name = &obj.#name;
180+
}
181+
})
182+
.collect();
183+
if !field_refs.is_empty() {
184+
quote! { #( #field_refs )* }
185+
} else {
186+
quote! {}
187+
}
188+
}
189+
Fields::Unnamed(_) => quote! {},
190+
Fields::Unit => quote! {},
191+
}
192+
}
193+
78194
/// Generate an item initialization where an item can be a struct or a variant.
79195
/// For named fields, this will generate: `Item { field1: kani::any(), field2: kani::any(), .. }`
80196
/// For unnamed fields, this will generate: `Item (kani::any(), kani::any(), ..)`
@@ -115,6 +231,42 @@ fn init_symbolic_item(ident: &Ident, fields: &Fields) -> TokenStream {
115231
}
116232
}
117233

234+
/// Extract, parse and return the expression `cond` (i.e., `Some(cond)`) in the
235+
/// `#[safety_constraint(<cond>)]` attribute helper associated with a given field.
236+
/// Return `None` if the attribute isn't specified.
237+
fn parse_safety_expr(ident: &Ident, field: &syn::Field) -> Option<TokenStream> {
238+
let name = &field.ident;
239+
let mut safety_helper_attr = None;
240+
241+
// Keep the helper attribute if we find it
242+
for attr in &field.attrs {
243+
if attr.path().is_ident("safety_constraint") {
244+
safety_helper_attr = Some(attr);
245+
}
246+
}
247+
248+
// Parse the arguments in the `#[safety_constraint(...)]` attribute
249+
if let Some(attr) = safety_helper_attr {
250+
let expr_args: Result<syn::Expr, syn::Error> = attr.parse_args();
251+
252+
// Check if there was an error parsing the arguments
253+
if let Err(err) = expr_args {
254+
abort!(Span::call_site(), "Cannot derive impl for `{}`", ident;
255+
note = attr.span() =>
256+
"safety constraint in field `{}` could not be parsed: {}", name.as_ref().unwrap().to_string(), err
257+
)
258+
}
259+
260+
// Return the expression associated to the safety constraint
261+
let safety_expr = expr_args.unwrap();
262+
Some(quote_spanned! {field.span()=>
263+
#safety_expr
264+
})
265+
} else {
266+
None
267+
}
268+
}
269+
118270
/// Generate the body of the function `any()` for enums. The cases are:
119271
/// 1. For zero-variants enumerations, this will encode a `panic!()` statement.
120272
/// 2. For one or more variants, the code will be something like:
@@ -176,10 +328,14 @@ pub fn expand_derive_invariant(item: proc_macro::TokenStream) -> proc_macro::Tok
176328
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
177329

178330
let body = is_safe_body(&item_name, &derive_item.data);
331+
let field_refs = field_refs(&item_name, &derive_item.data);
332+
179333
let expanded = quote! {
180334
// The generated implementation.
181335
impl #impl_generics kani::Invariant for #item_name #ty_generics #where_clause {
182336
fn is_safe(&self) -> bool {
337+
let obj = self;
338+
#field_refs
183339
#body
184340
}
185341
}
@@ -199,7 +355,7 @@ fn add_trait_bound_invariant(mut generics: Generics) -> Generics {
199355

200356
fn is_safe_body(ident: &Ident, data: &Data) -> TokenStream {
201357
match data {
202-
Data::Struct(struct_data) => struct_safe_conjunction(ident, &struct_data.fields),
358+
Data::Struct(struct_data) => struct_invariant_conjunction(ident, &struct_data.fields),
203359
Data::Enum(_) => {
204360
abort!(Span::call_site(), "Cannot derive `Invariant` for `{}` enum", ident;
205361
note = ident.span() =>
@@ -215,21 +371,35 @@ fn is_safe_body(ident: &Ident, data: &Data) -> TokenStream {
215371
}
216372
}
217373

218-
/// Generates an expression that is the conjunction of `is_safe` calls for each field in the struct.
219-
fn struct_safe_conjunction(_ident: &Ident, fields: &Fields) -> TokenStream {
374+
/// Generates an expression that is the conjunction of safety constraints for each field in the struct.
375+
fn struct_invariant_conjunction(ident: &Ident, fields: &Fields) -> TokenStream {
220376
match fields {
221377
// Expands to the expression
378+
// `true && <safety_cond1> && <safety_cond2> && ..`
379+
// where `safety_condN` is
380+
// - `self.fieldN.is_safe() && <cond>` if a condition `<cond>` was
381+
// specified through the `#[safety_constraint(<cond>)]` helper attribute, or
382+
// - `self.fieldN.is_safe()` otherwise
383+
//
384+
// Therefore, if `#[safety_constraint(<cond>)]` isn't specified for any field, this expands to
222385
// `true && self.field1.is_safe() && self.field2.is_safe() && ..`
223386
Fields::Named(ref fields) => {
224-
let safe_calls = fields.named.iter().map(|field| {
225-
let name = &field.ident;
226-
quote_spanned! {field.span()=>
227-
self.#name.is_safe()
228-
}
229-
});
387+
let safety_conds: Vec<TokenStream> = fields
388+
.named
389+
.iter()
390+
.map(|field| {
391+
let name = &field.ident;
392+
let default_expr = quote_spanned! {field.span()=>
393+
#name.is_safe()
394+
};
395+
parse_safety_expr(ident, field)
396+
.map(|expr| quote! { #expr && #default_expr})
397+
.unwrap_or(default_expr)
398+
})
399+
.collect();
230400
// An initial value is required for empty structs
231-
safe_calls.fold(quote! { true }, |acc, call| {
232-
quote! { #acc && #call }
401+
safety_conds.iter().fold(quote! { true }, |acc, cond| {
402+
quote! { #acc && #cond }
233403
})
234404
}
235405
Fields::Unnamed(ref fields) => {

library/kani_macros/src/lib.rs

+108-4
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,120 @@ pub fn unstable_feature(attr: TokenStream, item: TokenStream) -> TokenStream {
100100
attr_impl::unstable(attr, item)
101101
}
102102

103-
/// Allow users to auto generate Arbitrary implementations by using `#[derive(Arbitrary)]` macro.
103+
/// Allow users to auto generate `Arbitrary` implementations by using
104+
/// `#[derive(Arbitrary)]` macro.
105+
///
106+
/// When using `#[derive(Arbitrary)]` on a struct, the `#[safety_constraint(<cond>)]`
107+
/// attribute can be added to its fields to indicate a type safety invariant
108+
/// condition `<cond>`. Since `kani::any()` is always expected to produce
109+
/// type-safe values, **adding `#[safety_constraint(...)]` to any fields will further
110+
/// constrain the objects generated with `kani::any()`**.
111+
///
112+
/// For example, the `check_positive` harness in this code is expected to
113+
/// pass:
114+
///
115+
/// ```rs
116+
/// #[derive(kani::Arbitrary)]
117+
/// struct AlwaysPositive {
118+
/// #[safety_constraint(*inner >= 0)]
119+
/// inner: i32,
120+
/// }
121+
///
122+
/// #[kani::proof]
123+
/// fn check_positive() {
124+
/// let val: AlwaysPositive = kani::any();
125+
/// assert!(val.inner >= 0);
126+
/// }
127+
/// ```
128+
///
129+
/// Therefore, using the `#[safety_constraint(...)]` attribute can lead to vacuous
130+
/// results when the values are over-constrained. For example, in this code
131+
/// the `check_positive` harness will pass too:
132+
///
133+
/// ```rs
134+
/// #[derive(kani::Arbitrary)]
135+
/// struct AlwaysPositive {
136+
/// #[safety_constraint(*inner >= 0 && *inner < i32::MIN)]
137+
/// inner: i32,
138+
/// }
139+
///
140+
/// #[kani::proof]
141+
/// fn check_positive() {
142+
/// let val: AlwaysPositive = kani::any();
143+
/// assert!(val.inner >= 0);
144+
/// }
145+
/// ```
146+
///
147+
/// Unfortunately, we made a mistake when specifying the condition because
148+
/// `*inner >= 0 && *inner < i32::MIN` is equivalent to `false`. This results
149+
/// in the relevant assertion being unreachable:
150+
///
151+
/// ```
152+
/// Check 1: check_positive.assertion.1
153+
/// - Status: UNREACHABLE
154+
/// - Description: "assertion failed: val.inner >= 0"
155+
/// - Location: src/main.rs:22:5 in function check_positive
156+
/// ```
157+
///
158+
/// As usual, we recommend users to defend against these behaviors by using
159+
/// `kani::cover!(...)` checks and watching out for unreachable assertions in
160+
/// their project's code.
104161
#[proc_macro_error]
105-
#[proc_macro_derive(Arbitrary)]
162+
#[proc_macro_derive(Arbitrary, attributes(safety_constraint))]
106163
pub fn derive_arbitrary(item: TokenStream) -> TokenStream {
107164
derive::expand_derive_arbitrary(item)
108165
}
109166

110-
/// Allow users to auto generate Invariant implementations by using `#[derive(Invariant)]` macro.
167+
/// Allow users to auto generate `Invariant` implementations by using
168+
/// `#[derive(Invariant)]` macro.
169+
///
170+
/// When using `#[derive(Invariant)]` on a struct, the `#[safety_constraint(<cond>)]`
171+
/// attribute can be added to its fields to indicate a type safety invariant
172+
/// condition `<cond>`. This will ensure that the gets additionally checked when
173+
/// using the `is_safe()` method generated by the `#[derive(Invariant)]` macro.
174+
///
175+
/// For example, the `check_positive` harness in this code is expected to
176+
/// fail:
177+
///
178+
/// ```rs
179+
/// #[derive(kani::Invariant)]
180+
/// struct AlwaysPositive {
181+
/// #[safety_constraint(*inner >= 0)]
182+
/// inner: i32,
183+
/// }
184+
///
185+
/// #[kani::proof]
186+
/// fn check_positive() {
187+
/// let val = AlwaysPositive { inner: -1 };
188+
/// assert!(val.is_safe());
189+
/// }
190+
/// ```
191+
///
192+
/// This is not too surprising since the type safety invariant that we indicated
193+
/// is not being taken into account when we create the `AlwaysPositive` object.
194+
///
195+
/// As mentioned, the `is_safe()` methods generated by the
196+
/// `#[derive(Invariant)]` macro check the corresponding `is_safe()` method for
197+
/// each field in addition to any type safety invariants specified through the
198+
/// `#[safety_constraint(...)]` attribute.
199+
///
200+
/// For example, for the `AlwaysPositive` struct from above, we will generate
201+
/// the following implementation:
202+
///
203+
/// ```rs
204+
/// impl kani::Invariant for AlwaysPositive {
205+
/// fn is_safe(&self) -> bool {
206+
/// let obj = self;
207+
/// let inner = &obj.inner;
208+
/// true && *inner >= 0 && inner.is_safe()
209+
/// }
210+
/// }
211+
/// ```
212+
///
213+
/// Note: the assignments to `obj` and `inner` are made so that we can treat the
214+
/// fields as if they were references.
111215
#[proc_macro_error]
112-
#[proc_macro_derive(Invariant)]
216+
#[proc_macro_derive(Invariant, attributes(safety_constraint))]
113217
pub fn derive_invariant(item: TokenStream) -> TokenStream {
114218
derive::expand_derive_invariant(item)
115219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Check 1: check_invariant_helper_ok.assertion.1\
2+
- Status: SUCCESS\
3+
- Description: "assertion failed: pos_point.x >= 0"
4+
5+
Check 2: check_invariant_helper_ok.assertion.2\
6+
- Status: SUCCESS\
7+
- Description: "assertion failed: pos_point.y >= 0"
8+
9+
Check 1: check_invariant_helper_fail.assertion.1\
10+
- Status: FAILURE\
11+
- Description: "assertion failed: pos_point.x >= 0"
12+
13+
Check 2: check_invariant_helper_fail.assertion.2\
14+
- Status: FAILURE\
15+
- Description: "assertion failed: pos_point.y >= 0"
16+
17+
Complete - 2 successfully verified harnesses, 0 failures, 2 total.

0 commit comments

Comments
 (0)