Skip to content

Commit 3467ba1

Browse files
JustusAdamzhassan-awsfeliperodri
authored
modifies Clauses for Function Contracts (rust-lang#2800)
Extends the function contract functionality with a `modifies` clause. The design is different from rust-lang#2594 but serves a similar purpose. The `modifies` clause allows the user to specify which parts of a structure a function may assign to. Essentially refining the `mut` annotation. We allow arbitrary (side-effect free) expressions in the `modifies` clause. The expressions are evaluated as part of the preconditions and passed to the function-under-verification as additional arguments. CBMC is then instructed to check that those locations are assigned. Aliasing means that this also adds the location in the original structure to the write set. Each expression must return a pointer to a value that implements `Arbitrary`. On replacement we then simply assign `*ptr = kani::any()`, relying again on aliasing to update the original structure. Additional tests for the new functionality are provided. Resolves rust-lang#2594 ## Open Questions ### API divergence from CBMC (accepted) The current design goes roughly as follows: We start with a `modifies` annotation on a function ```rs #[modifies(obj.some_expr())] fn target(obj: ...) { ... } ``` And from this we generate code to the effect of (simplified here) ```rs fn target_check(obj: ...) { // Undo the lifetime entanglements let modified_1 = std::mem::transmute::<&'a _, &'b _>(obj.some_expr()); target_wrapper(obj, modified_1); } #[cbmc::assigns(*modified_1)] fn target_wrapper(obj: ..., modified_1: &impl kani::Arbitrary) { ... } ``` Unlike CBMC we expect `obj.some_expr()` to be of a **pointer type** (`*const`, `*mut`, `&mut` or `&`) that points to the object which is target of the modification. So if we had a `t : &mut T` that was modified, CBMC would expect its assigns clause to say `*t`, but we expect `t` (no dereference). The reason is that the code we generate uses the workaround of creating an alias to whichever part of `obj` is modified and registers the alias with CBMC (thereby registering the original also). If we generated code where the right side of `let modified_1 =` is not of pointer type, then the object is moved to the stack and the aliasing destroyed. The open questions is whether we are happy with this change in API. (Yes) ### Test cases when expressions are used in the clause. With more complex expressions in the modifies clause it becomes hard to define good test cases because they reference generated code as in this case: ```rs #[kani::requires(**ptr < 100)] #[kani::modifies(ptr.as_ref())] fn modify(ptr: &mut Box<u32>) { *ptr.as_mut() += 1; } ``` This passes (as it should) and when commenting out the `modifies` clause we get this error: ``` Check 56: modify_wrapper_895c4e.assigns.2 - Status: FAILURE - Description: "Check that *var_2 is assignable" - Location: assigns_expr_pass.rs:8:5 in function modify_wrapper_895c4e ``` The information in this error is very non-specific, hard to read and brittle. How should we define robust "expected" test cases for such errors? ### Corner Cases / Future Improvements - rust-lang#2907 - rust-lang#2908 - rust-lang#2909 ## TODOs - [ ] Test Cases where the clause contains - [x] `Rc` + (`RefCell` or `unsafe`) (see rust-lang#2907) - [x] Fields - [x] Statement expressions - [x] `Vec` (see rust-lang#2909) - [ ] Fat pointers - [ ] update contracts documentation - [x] Make sure the wrapper arguments are unique. - [x] Ensure `nondet-static-exclude` always uses the correct filepath (relative or absolute) - [ ] Test case for multiple `modifies` clauses. 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: Zyad Hassan <[email protected]> Co-authored-by: Felipe R. Monteiro <[email protected]>
1 parent e09993b commit 3467ba1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2036
-335
lines changed

cprover_bindings/src/goto_program/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ pub use expr::{
2222
};
2323
pub use location::Location;
2424
pub use stmt::{Stmt, StmtBody, SwitchCase};
25-
pub use symbol::{Symbol, SymbolValues};
25+
pub use symbol::{FunctionContract, Lambda, Symbol, SymbolValues};
2626
pub use symbol_table::SymbolTable;
2727
pub use typ::{CIntType, DatatypeComponent, Parameter, Type};

cprover_bindings/src/goto_program/symbol.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct Symbol {
1313
pub location: Location,
1414
pub typ: Type,
1515
pub value: SymbolValues,
16+
/// Contracts to be enforced (only supported for functions)
17+
pub contract: Option<Box<FunctionContract>>,
1618

1719
/// Optional debugging information
1820
@@ -44,6 +46,57 @@ pub struct Symbol {
4446
pub is_weak: bool,
4547
}
4648

49+
/// The equivalent of a "mathematical function" in CBMC. Semantically this is an
50+
/// anonymous function object, similar to a closure, but without closing over an
51+
/// environment.
52+
///
53+
/// This is only valid for use as a function contract. It may not perform side
54+
/// effects, a property that is enforced on the CBMC side.
55+
///
56+
/// The precise nomenclature is that in CBMC a contract value has *type*
57+
/// `mathematical_function` and values of that type are `lambda`s. Since this
58+
/// struct represents such values it is named `Lambda`.
59+
#[derive(Debug, Clone)]
60+
pub struct Lambda {
61+
pub arguments: Vec<Parameter>,
62+
pub body: Expr,
63+
}
64+
65+
impl Lambda {
66+
pub fn as_contract_for(
67+
fn_ty: &Type,
68+
return_var_name: Option<InternedString>,
69+
body: Expr,
70+
) -> Self {
71+
let arguments = match fn_ty {
72+
Type::Code { parameters, return_type } => {
73+
[Parameter::new(None, return_var_name, (**return_type).clone())]
74+
.into_iter()
75+
.chain(parameters.iter().cloned())
76+
.collect()
77+
}
78+
_ => panic!(
79+
"Contract lambdas can only be generated for `Code` types, received {fn_ty:?}"
80+
),
81+
};
82+
Self { arguments, body }
83+
}
84+
}
85+
86+
/// The CBMC representation of a function contract. Represents
87+
/// https://diffblue.github.io/cbmc/contracts-user.html but currently only assigns clauses are
88+
/// supported.
89+
#[derive(Clone, Debug)]
90+
pub struct FunctionContract {
91+
pub(crate) assigns: Vec<Lambda>,
92+
}
93+
94+
impl FunctionContract {
95+
pub fn new(assigns: Vec<Lambda>) -> Self {
96+
Self { assigns }
97+
}
98+
}
99+
47100
/// Currently, only C is understood by CBMC.
48101
// TODO: <https://github.com/model-checking/kani/issues/1>
49102
#[derive(Clone, Debug)]
@@ -84,6 +137,7 @@ impl Symbol {
84137
base_name,
85138
pretty_name,
86139

140+
contract: None,
87141
module: None,
88142
mode: SymbolModes::C,
89143
// global properties
@@ -107,6 +161,18 @@ impl Symbol {
107161
}
108162
}
109163

164+
/// Add this contract to the symbol (symbol must be a function) or fold the
165+
/// conditions into an existing contract.
166+
pub fn attach_contract(&mut self, contract: FunctionContract) {
167+
assert!(self.typ.is_code());
168+
match self.contract {
169+
Some(ref mut prior) => {
170+
prior.assigns.extend(contract.assigns);
171+
}
172+
None => self.contract = Some(Box::new(contract)),
173+
}
174+
}
175+
110176
/// The symbol that defines the type of the struct or union.
111177
/// For a struct foo this is the symbol "tag-foo" that maps to the type struct foo.
112178
pub fn aggr_ty<T: Into<InternedString>>(t: Type, pretty_name: T) -> Symbol {
@@ -319,6 +385,12 @@ impl Symbol {
319385
self.is_auxiliary = hidden;
320386
self
321387
}
388+
389+
/// Set `is_property`.
390+
pub fn with_is_property(mut self, v: bool) -> Self {
391+
self.is_property = v;
392+
self
393+
}
322394
}
323395

324396
/// Predicates

cprover_bindings/src/goto_program/symbol_table.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33
use super::super::{env, MachineModel};
4-
use super::{BuiltinFn, Stmt, Symbol};
4+
use super::{BuiltinFn, FunctionContract, Stmt, Symbol};
55
use crate::InternedString;
66
use std::collections::BTreeMap;
77
/// This is a typesafe implementation of the CBMC symbol table, based on the CBMC code at:
@@ -79,6 +79,17 @@ impl SymbolTable {
7979
let name = name.into();
8080
self.symbol_table.get_mut(&name).unwrap().update_fn_declaration_with_definition(body);
8181
}
82+
83+
/// Attach a contract to the symbol identified by `name`. If a prior
84+
/// contract exists it is extended with additional clauses.
85+
pub fn attach_contract<T: Into<InternedString>>(
86+
&mut self,
87+
name: T,
88+
contract: FunctionContract,
89+
) {
90+
let sym = self.symbol_table.get_mut(&name.into()).unwrap();
91+
sym.attach_contract(contract);
92+
}
8293
}
8394

8495
/// Getters

cprover_bindings/src/goto_program/typ.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,17 @@ impl Parameter {
228228
}
229229
}
230230

231+
/// Constructor
232+
impl Parameter {
233+
pub fn new<S: Into<InternedString>>(
234+
base_name: Option<S>,
235+
identifier: Option<S>,
236+
typ: Type,
237+
) -> Self {
238+
Self { base_name: base_name.map(Into::into), identifier: identifier.map(Into::into), typ }
239+
}
240+
}
241+
231242
impl CIntType {
232243
pub fn sizeof_in_bits(&self, st: &SymbolTable) -> u64 {
233244
match self {

cprover_bindings/src/irep/irep.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::super::goto_program::{Location, Type};
66
use super::super::MachineModel;
77
use super::{IrepId, ToIrep};
88
use crate::cbmc_string::InternedString;
9+
use crate::linear_map;
910
use linear_map::LinearMap;
1011
use num::BigInt;
1112
use std::fmt::Debug;
@@ -141,4 +142,12 @@ impl Irep {
141142
pub fn zero() -> Irep {
142143
Irep::just_id(IrepId::Id0)
143144
}
145+
146+
pub fn tuple(sub: Vec<Irep>) -> Self {
147+
Irep {
148+
id: IrepId::Tuple,
149+
sub,
150+
named_sub: linear_map![(IrepId::Type, Irep::just_id(IrepId::Tuple))],
151+
}
152+
}
144153
}

cprover_bindings/src/irep/irep_id.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ pub enum IrepId {
593593
CSpecLoopInvariant,
594594
CSpecRequires,
595595
CSpecEnsures,
596+
CSpecAssigns,
596597
VirtualFunction,
597598
ElementType,
598599
WorkingDirectory,
@@ -1462,6 +1463,7 @@ impl ToString for IrepId {
14621463
IrepId::CSpecLoopInvariant => "#spec_loop_invariant",
14631464
IrepId::CSpecRequires => "#spec_requires",
14641465
IrepId::CSpecEnsures => "#spec_ensures",
1466+
IrepId::CSpecAssigns => "#spec_assigns",
14651467
IrepId::VirtualFunction => "virtual_function",
14661468
IrepId::ElementType => "element_type",
14671469
IrepId::WorkingDirectory => "working_directory",

cprover_bindings/src/irep/to_irep.rs

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use super::super::goto_program;
66
use super::super::MachineModel;
77
use super::{Irep, IrepId};
88
use crate::linear_map;
9+
use crate::InternedString;
910
use goto_program::{
10-
BinaryOperator, CIntType, DatatypeComponent, Expr, ExprValue, Location, Parameter,
11+
BinaryOperator, CIntType, DatatypeComponent, Expr, ExprValue, Lambda, Location, Parameter,
1112
SelfOperator, Stmt, StmtBody, SwitchCase, SymbolValues, Type, UnaryOperator,
1213
};
1314

@@ -16,10 +17,10 @@ pub trait ToIrep {
1617
}
1718

1819
/// Utility functions
19-
fn arguments_irep(arguments: &[Expr], mm: &MachineModel) -> Irep {
20+
fn arguments_irep<'a>(arguments: impl Iterator<Item = &'a Expr>, mm: &MachineModel) -> Irep {
2021
Irep {
2122
id: IrepId::Arguments,
22-
sub: arguments.iter().map(|x| x.to_irep(mm)).collect(),
23+
sub: arguments.map(|x| x.to_irep(mm)).collect(),
2324
named_sub: linear_map![],
2425
}
2526
}
@@ -169,6 +170,16 @@ impl ToIrep for Expr {
169170
}
170171
}
171172

173+
impl Irep {
174+
pub fn symbol(identifier: InternedString) -> Self {
175+
Irep {
176+
id: IrepId::Symbol,
177+
sub: vec![],
178+
named_sub: linear_map![(IrepId::Identifier, Irep::just_string_id(identifier))],
179+
}
180+
}
181+
}
182+
172183
impl ToIrep for ExprValue {
173184
fn to_irep(&self, mm: &MachineModel) -> Irep {
174185
match self {
@@ -245,7 +256,7 @@ impl ToIrep for ExprValue {
245256
}
246257
ExprValue::FunctionCall { function, arguments } => side_effect_irep(
247258
IrepId::FunctionCall,
248-
vec![function.to_irep(mm), arguments_irep(arguments, mm)],
259+
vec![function.to_irep(mm), arguments_irep(arguments.iter(), mm)],
249260
),
250261
ExprValue::If { c, t, e } => Irep {
251262
id: IrepId::If,
@@ -297,14 +308,7 @@ impl ToIrep for ExprValue {
297308
sub: values.iter().map(|x| x.to_irep(mm)).collect(),
298309
named_sub: linear_map![],
299310
},
300-
ExprValue::Symbol { identifier } => Irep {
301-
id: IrepId::Symbol,
302-
sub: vec![],
303-
named_sub: linear_map![(
304-
IrepId::Identifier,
305-
Irep::just_string_id(identifier.to_string()),
306-
)],
307-
},
311+
ExprValue::Symbol { identifier } => Irep::symbol(*identifier),
308312
ExprValue::Typecast(e) => {
309313
Irep { id: IrepId::Typecast, sub: vec![e.to_irep(mm)], named_sub: linear_map![] }
310314
}
@@ -456,7 +460,7 @@ impl ToIrep for StmtBody {
456460
vec![
457461
lhs.as_ref().map_or(Irep::nil(), |x| x.to_irep(mm)),
458462
function.to_irep(mm),
459-
arguments_irep(arguments, mm),
463+
arguments_irep(arguments.iter(), mm),
460464
],
461465
),
462466
StmtBody::Goto(dest) => code_irep(IrepId::Goto, vec![])
@@ -499,10 +503,50 @@ impl ToIrep for SwitchCase {
499503
}
500504
}
501505

506+
impl ToIrep for Lambda {
507+
/// At the moment this function assumes that this lambda is used for a
508+
/// `modifies` contract. It should work for any other lambda body, but
509+
/// the parameter names use "modifies" in their generated names.
510+
fn to_irep(&self, mm: &MachineModel) -> Irep {
511+
let (ops_ireps, types) = self
512+
.arguments
513+
.iter()
514+
.enumerate()
515+
.map(|(index, param)| {
516+
let ty_rep = param.typ().to_irep(mm);
517+
(
518+
Irep::symbol(
519+
param.identifier().unwrap_or_else(|| format!("_modifies_{index}").into()),
520+
)
521+
.with_named_sub(IrepId::Type, ty_rep.clone()),
522+
ty_rep,
523+
)
524+
})
525+
.unzip();
526+
let typ = Irep {
527+
id: IrepId::MathematicalFunction,
528+
sub: vec![Irep::just_sub(types), self.body.typ().to_irep(mm)],
529+
named_sub: Default::default(),
530+
};
531+
Irep {
532+
id: IrepId::Lambda,
533+
sub: vec![Irep::tuple(ops_ireps), self.body.to_irep(mm)],
534+
named_sub: linear_map!((IrepId::Type, typ)),
535+
}
536+
}
537+
}
538+
502539
impl goto_program::Symbol {
503540
pub fn to_irep(&self, mm: &MachineModel) -> super::Symbol {
541+
let mut typ = self.typ.to_irep(mm);
542+
if let Some(contract) = &self.contract {
543+
typ = typ.with_named_sub(
544+
IrepId::CSpecAssigns,
545+
Irep::just_sub(contract.assigns.iter().map(|req| req.to_irep(mm)).collect()),
546+
);
547+
}
504548
super::Symbol {
505-
typ: self.typ.to_irep(mm),
549+
typ,
506550
value: match &self.value {
507551
SymbolValues::Expr(e) => e.to_irep(mm),
508552
SymbolValues::Stmt(s) => s.to_irep(mm),

0 commit comments

Comments
 (0)