requires and ensure
Most of what you will be writing with creusot will be requires
and ensures
clauses. That is: preconditions and postconditions.
Together, those form the contract of a function. This is in essence the "logical signature" of your function: when analyzing a function, only the contract of other functions is visible.
Preconditions with requires
A precondition is an assertion that must hold when entering the function. For example, the following function accesses a slice at index 0
:
#![allow(unused)] fn main() { fn head(v: &[i32]) -> i32 { v[0] } }
So we must require that the slice has at least one element:
#![allow(unused)] fn main() { #[requires(v@.len() >= 1)] fn head(v: &[i32]) -> i32 { v[0] } }
Note the view (@
) operator: it is needed to convert the Rust type &[i32]
to a logical type Seq<i32>
. Else, we could not call [T]::len
, which is a program function (and not a logical one).
To learn more, see the chapter on Pearlite and Views.
Postconditions with ensures
A postcondition is an assertions that is proven true at the end of the function. The return value of the function can be accessed via the result
keyword.
In the case of the example above, we want to assert that the returned integer is the first of the slice:
#![allow(unused)] fn main() { #[requires(v@.len() >= 1)] #[ensures(v@[0] == result)] fn head(v: &[i32]) -> i32 { v[0] } }
Note that we:
- use the
@
operator on the slice to get aSeq<i32>
- we can then index this
Seq<i32>
to get ai32
.