Quickstart
Installation
Install Creusot and why3 as described in the README.
Create a project
Create a new project with this command:
cargo creusot new project-name
That command creates a directory package-name
containing the basic elements of a Rust project verified with Creusot. The file src/lib.rs
is initialized with an example function annotated with a contract:
// src/lib.rs
use creusot_contracts::*;
#[requires(x@ < i64::MAX@)]
#[ensures(result@ == x@ + 1)]
pub fn add_one(x: i64) -> i64 {
x + 1
}
Compile and prove
To verify this project, run this command:
cargo creusot prove
A successful run gives us the certainty that functions defined in this package satisfy their contracts:
for all arguments satisfying the preconditions (requires
clauses), the result of the function will
satisfy the postconditions (ensures
clauses).
Prove with Why3
Finally, launch the why3 ide:
cargo creusot why3 ide
The documentation for the why3 ide can be found here.
We also recommend section 2.3 of this thesis for a brief overview of Why3 and Creusot proofs.
We plan to improve this part of the user experience, but that will have to wait until Creusot gets more stable and complete. If you'd like to help, a prototype VSCode plugin for Why3 is in development, it should make the experience much smoother when complete.
Prove with Why3find
Configure
$ cargo creusot config
The configuration is stored in why3find.json
.
Prove
-
Run the Creusot translation.
$ cargo creusot
-
Run the Why3find prover.
$ cargo creusot prove
Run the Why3find prover on specific files:
$ cargo creusot prove verif/[COMA_FILES]
-
Launch Why3 IDE on unproved goals (this will only open one file even if there are many listed with unproved goals).
$ cargo creusot prove -i $ cargo creusot prove -i verif/[COMA_FILES]
Tutorial
This tutorial is an introduction to deductive verification with Creusot. After introducing basic concepts, we will walk through the steps to verify a function with Creusot.
No prior knowledge about formal methods is required to follow this tutorial. Basic familiarity with Rust should be enough to get started with Creusot.
Creusot
Follow the README to install Why3 and Creusot.
Setup
Create a new project with this command:
cargo creusot new creusot-tutorial
This generates a directory creusot-tutorial
containing the following files:
Cargo.toml
rust-toolchain
why3find.json
src/lib.rs
The first three files contain package-level configuration settings. You may already be familiar with
Cargo.toml
and rust-toolchain
, generic files for Rust projects. The file why3find.json
is
used by a verification tool invoked by Creusot. We will leave these files alone for this tutorial.
The last file, src/lib.rs
, is what we will be editing in this tutorial.
My first contract
Open src/lib.rs
. It contains an example function with a contract:
use creusot_contracts::*;
#[requires(x@ < i64::MAX@)]
#[ensures(result@ == x@ + 1)]
pub fn add_one(x: i64) -> i64 {
x + 1
}
In Creusot, contracts are how we specify the expected behavior of functions. A contract contains two kinds of assertions: preconditions, or "required clauses", describe the values of arguments that the function accepts; postconditions, or "ensured clauses", describe the result value that the function returns.
In this first example, there is one precondition (requires
) and one postcondition (ensures
).
This contract can be read as follows: if the argument x
is smaller than i64::MAX
,
then the value of add_one(x)
will be equal to x@ + 1
.
More generally, a function satisfies its contract when, for any input arguments that satisfy
the preconditions (requires
), the result of the function satisfies the postcondition (ensures
).
It is not a very exciting contract, since it just replicates the body of the function x + 1
.
But note that x@ + 1
is not exactly the same expression. The @
suffix is Creusot's view operator,
which maps a Rust value to a mathematical value. If x
has type i64
, then x@
has type Int
,
as the type of mathematical integers (..., -2, -1, 0, 1, 2, ...).
The key difference is that mathematical integers are unbounded: their arithmetic operations do not overflow.
They are more intuitive and better behaved than operations on machine integer.
For example, all x: Int
satisfy x < x + 1
whereas this cannot be true for Rust's machine integers because they are finite.
The type Int
is one of several built-in types of common mathematical objects defined by Creusot;
others include sequences and mappings (mathematical functions).
On the one hand, mathematical objects like Int
cannot be manipulated by Rust programs, they can only be used in contracts.
On the other hand, values that exist in Rust programs such as machine integers (i64
, etc.) are not as easy to manipulate
in the logical world of contracts, so we use the view operator @
to transform them into something more intuitive.
The contract of add_one
specifies the result slightly indirectly: its view as an unbounded integer, denoted by result@
,
must be equal to x@ + 1
. Note that arithmetic operations are only defined on Int
in the logical world of contracts.
Verifying the contract
cargo creusot prove
First, the Rust code is compiled to Coma, an intermediate verification language in the Why3 platform. Second, Why3 verifies the compiled program by generating verification conditions and sending them as queries to SMT solvers. When all verification conditions have been proved, that amounts to proving that functions defined in our crate satisfy their contracts.
My second contract
Here is a more involved example: given a non-negative integer n
, compute the sum of all integers between 0
and n
included.
We can do this simply by iterating through each integer up to n
. This is the function to verify.
pub fn sum_up_to(n: u64) -> u64 {
let mut sum = 0;
let mut i = 0;
while i < n {
i += 1;
sum += i;
}
sum
}
Although a for
loop would be idiomatic, that also adds some complications to the verification process.
For the sake of simplicity, we start with a while
loop, and switch to a for
loop at the end.
As a sanity check, we can write unit tests:
#[test]
pub fn test_sum_up_to() {
// 1 + 2 + 3 + 4 = 10
assert_eq!(sum_up_to(4), 10);
}
Run with
cargo test
Whereas unit tests let us check the output associated with specific inputs of a function, contracts let us specify the output for all possible inputs. Naturally, we can't exhaustively list input-output pairs. Instead, we describe the relation between inputs and outputs as properties, logical formulas that are true about the inputs and outputs of a function.
Contract
The result of this sum is also known as the triangular numbers,
with a well-known formula: n * (n + 1) / 2
. This provides a nice and concise specification for sum_up_to
.
Write an #[ensures(...)]
clause to give a postcondition to sum_up_to
using the formula for triangular numbers.
Added postcondition
#[ensures(result@ == n@ * (n@ + 1) / 2)]
pub fn sum_up_to(n: u64) -> u64 {
// ...
}
Run cargo creusot
to make sure that the postcondition is well-formed.
Of course, cargo creusot prove
will fail. The contract is still too naive at this point.
What if the sum overflows? Feel free to try different approaches to handle this problem. Here is a non-exhaustive list of possibilities.
Add a simple bound on the input as a precondition
#[requires(n@ < 1000)]
#[ensures(result@ == n@ * (n@ + 1) / 2)]
pub fn sum_up_to(n: u64) -> u64 {
// ...
}
Add a more precise upper bound as a precondition
#[requires(n@ * (n@ + 1) / 2 <= u64::MAX@)]
#[ensures(result@ == n@ * (n@ + 1) / 2)]
pub fn sum_up_to(n: u64) -> u64 {
// ...
}
Allow overflows and adjust the postcondition accordingly
#[ensures(result@ == (n@ * (n@ + 1) / 2) % (u64::MAX + 1))]
pub fn sum_up_to(n: u64) -> u64 {
// ...
}
Change the result type to be big enough
#[ensures(result@ == n@ * (n@ + 1) / 2)]
pub fn sum_up_to(n: u64) -> u128 {
// ...
}
At this point, you may have a contract that is satisfied by the function,
but cargo creusot prove
still can't verify it. This function contains a loop,
and loops must be annotated with loop invariants: assertions which must be true
at every iteration of the loop.
Loop invariants
A loop invariant looks like this:
#[invariant(i@ <= n@)]
while i < n {
i += 1;
sum += i;
}
At every step, i
is less than or equal to n
. In particular, it is true when we enter the loop (when i = 0
),
inside the loop body it must have been that i < n
, which gives us i+1 <= n
, with i+1
as the next value of i
,
and when we exit, the loop condition i < n
is false, which combined with the invariant i@ <= n@
tells us that i = n
when the function returns. We could have written this invariant as i <= n
, without the @
.
Inspecting proofs with Why3 IDE
The invariant is still incomplete.
To investigate what's missing, we can inspect proofs in progress with Why3 IDE.
The command cargo creusot prove
with the -i
option launches Why3 IDE, showing
the proof state for an as yet unverified function.
cargo creusot prove -i
If there are many functions to verify, we can pick a specific one on the command line
by naming the Coma file it was compiled to. Coma files are generated in the verif/
directory,
and their path consists of the crate name (creusot_tutorial_rlib
for the library creusot-tutorial
)
followed by the fully qualified function name (creusot_tutorial::sum_up_to
becomes creusot_tutorial/M_sum_up_to.coma
).
cargo creusot prove -i verif/creusot_tutorial_rlib/creusot_tutorial/M_sum_up_to.coma
Why3 IDE is a GUI that looks like this:
It's a three-pane window. The left panel displays the tree of goals to prove. The top right panel displays the proof context, which consists of relevant source files and a special Task tab listing available hypotheses and the goal formula. The bottom right panel contains error messages and other logs.
In the left panel, the status of every tree node is shown as an icon in the leftmost column.
Goals with complete proofs have green checkmarks, unproved goals have blue question marks.
Currently, the bottom of the tree consists of five subgoals labeled [loop invariant]
,
[sum_up_to ensures]
, [integer overflow]
twice, and [loop invariant]
again.
To verify that a function satisfies its contract, Why3 generates verification conditions which represent properties that must hold at certain points in the program:
-
Starting from the top of the function
sum_up_to
, we must check that the loop invariant holds when we enter the loop, that corresponds to the first[loop invariant]
goal. -
Inside the loop, we show that any iteration of the loop body preserves the loop invariant: we start with arbitrary values of
i
andsum
assuming that the loop invariant holds and that the conditioni < n
is true. Checking thati += 1
does not overflow corresponds to the first[integer overflow]
goal. Checking thatsum += i
does not overflow corresponds to the second[integer overflow]
goal. -
At the end of the loop body the invariant must hold again, corresponding to the last
[loop invariant]
goal.
Finally, at the loop exit, we assume that the loop invariant holds and that the condition i < n
is false,
and prove that the returned value satisfies the postcondition (ensures
clause), which corresponds to the
[sum_up_to ensures]
goal. It may seem surprising that the goal [sum_up_to ensures]
appears first even though
it corresponds to the end of the function;
this is due to internal details of compiling the program to a control-flow graph and generating verification
conditions by traversing the control-flow graph in a somewhat arbitrary order.
One way to make sense of the goals is to look at the source spans they contain.
In the left panel, click on the goal labeled [sum_up_to ensures]
.
That opens a tab lib.rs
on the top right panel; click on it.
We see a view of the Rust code we are trying to verify,
with highlighted spans relevant to the goal we selected.
What we are trying to prove is highlighted in yellow:
the goal [sum_up_to ensures]
, as its name indicates, is trying to prove that the
ensures
clause holds when the function returns.
Hypotheses and various bits of code that the goal depends on are highlighted in green:
the requires
clause and the loop invariant
are hypotheses.
Similarly, you can inspect the two [loop invariant]
goals. Both will highlight
the invariant i <= n
in yellow, but there will be a difference in the surrounding spans:
one goal will highlight the 0
from the initialization of i
, indicating that the goal
comes from entering the loop, whereas the other goal will highlight the 1
in the
increment i += 1
, indicating that the goal comes from checking that the loop body maintains the
loop invariant.
It is easy to prove that the addition in i += 1
will not overflow, because the loop condition i < n
is true. However we cannot deduce that the addition in sum += i
will not overflow yet, because
we know nothing about sum
in the loop invariant.
For that same reason, there is also no way to prove the postcondition ([sum_up_to ensures]
).
Complete the loop invariant
Complete the invariant. Below we extended the invariant with a placeholder for your solution.
Intuitively, the invariant should reflect the idea that at every step, sum
is the sum of numbers from 0
to i
included.
#[invariant(i@ <= n@)]
#[invariant(true /* YOUR INVARIANT HERE */)]
while i < n {
i += 1;
sum += i;
}
Completed invariant
#[invariant(i@ <= n@)]
#[invariant(sum@ == i@ * (i@ + 1) / 2)]
while i < n {
i += 1;
sum += i;
}
With that, cargo creusot prove
succeeds. We have proved that the function sum_up_to
satisfies its contract.
Complete sum_up_to
function with all annotations
#[requires(n@ * (n@ + 1) / 2 <= u64::MAX@)]
#[ensures(result@ == n@ * (n@ + 1) / 2)]
pub fn sum_up_to(n: u64) -> u64 {
let mut sum = 0;
let mut i = 0;
#[invariant(i@ <= n@)]
#[invariant(sum@ == i@ * (i@ + 1) / 2)]
while i < n {
i += 1;
sum += i;
}
sum
}
Use a for
loop
The while
loop above can be replaced with a more idiomatic for
loop.
for i in 1..=n {
sum += i;
}
Verifying a for
loop is more complicated because we must reason about iterators.
Another challenge is that the invariant cannot talk about the next value (i
):
there might not be a next value.
Creusot defines a special variable, produced
, which contains the sequence of elements
previously produced by the iterator of a for
loop.
Using that variable, we can restate the invariant about sum
:
#[invariant(sum@ == produced.len() * (produced.len() + 1) / 2)]
for i in 1..=n {
sum += i;
}
Indeed, produced.len()
is exactly the number of past iterations.
We no longer need the other clause of the invariant, i <= n
,
first because i
isn't even in scope in the invariant,
and also because that fact will be deduced automatically from the
postcondition of next
for the iterator 1..=n
, which is where i
comes from.
We are almost done. In principle, that invariant should be all we need to complete the proof. But the solvers can't figure it out for some reason. It may be that solvers have trouble with division. We can tweak the invariant to avoid relying on division, and the proof goes through.
#[invariant(sum@ * 2 == produced.len() * (produced.len() + 1))]
for i in 1..=n {
sum += i;
}
This kind of wrinkle is par for the course in SMT-based tools such as Creusot.
Complete sum_up_to
function with all annotations (for
loop version)
#[requires(n@ * (n@ + 1) / 2 <= u64::MAX@)]
#[ensures(result@ == n@ * (n@ + 1) / 2)]
pub fn sum_up_to(n: u64) -> u64 {
let mut sum = 0;
#[invariant(sum@ * 2 == produced.len() * (produced.len() + 1))]
for i in 1..=n {
sum += i;
}
sum
}
Conclusion
In this tutorial, we've got a glimpse of the workflow of verifying a Rust program in Creusot.
We have only scratched the surface. Creusot offers many more features for verifying Rust code at large. You can read about them in the rest of this user guide.
Basic concepts
Every Creusot macro will erase themselves when compiled normally: they only exist when using cargo creusot
.
If you need to have Creusot-only code, you can use the #[cfg(creusot)]
attribute.
Note that you must explicitly use the creusot_contracts
crate in your code (which should be the case once you actually prove things, but not necessarily when you initially set up a project), such as with the line:
use creusot_contracts::*;
or you will get a compilation error complaining that the creusot_contracts
crate is not loaded.
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
:
fn head(v: &[i32]) -> i32 {
v[0]
}
So we must require that the slice has at least one element:
#[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:
#[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
.
Invariants
When writing a loop (be it for
, while
or loop
), you will generally need to specify an invariant, that is an assertion that stays true for the duration of the loop. For example, the following program:
#[ensures(result == n)]
fn loop_add(n: u64) -> u64 {
let mut total = 0;
while total < n {
total += 1;
}
total
}
Needs an invariant: even though its proof seems obvious to us, it is not for Creusot. What Creusot knows is:
- Any variable not referenced in the loop is unchanged at the end (here this is obvious because
n
is immutable, but it might be relevant in a more complicated program). - At the end of the loop, the loop condition is false: here
total >= n
.
We still need to know that total <= n
to get result == n
:
#[ensures(result == n)]
fn loop_add(n: u64) -> u64 {
let mut total = 0;
#[invariant(total <= n)]
while total < n {
total += 1;
}
total
}
This is now provable !
Like
requires
andensures
,invariant
must contain a pearlite expression returning a boolean.
Verification conditions
An invariant
generates two verification conditions in Why3:
- First, that the invariants holds before the loop (initialization).
- Second, that if the invariant holds at the beginning of a loop iteration, then it holds at the end of it.
Compiling with rustc
Make sure that functions that contain #[invariant(...)]
attributes also have
an #[ensures(...)]
or #[requires(...)]
attribute.
You can always add #[ensures(true)]
as a trivial contract.
That enables compilation (cargo build
) with a stable Rust compiler,
preventing the following error:
error[E0658]: attributes on expressions are experimental
Indeed, the #[invariant(...)]
attribute on loops is only allowed by unstable features
(stmt_expr_attributes
, proc_macro_hygiene
). For compatibility with stable Rust,
the requires
and ensures
macros remove #[invariant(...)]
attributes during normal compilation.
Variants
A variant
clause can be attached either to a function like ensures
, or requires
or to a loop like invariant
, it should contain a strictly decreasing expression which can prove the termination of the item it is attached to.
proof_assert!
At any point in the code, you may want to insert an arbitrary verification condition. This can be useful:
- For debugging, to ensure that a property indeed holds at a certain point of the program.
- To help the provers in specific cases.
To do this, Creusot gives you the proof_assert!
macro:
fn f() {
// ...
let x = 1;
let y = 2;
proof_assert!(x@ + y@ == 3);
// ...
}
proof_assert
must contain a pearlite expression returning a boolean.
Trusted
The trusted
marker lets Creusot trust the implementation and specs.
More specifically, you can put #[trusted]
on a function like the following:
#[trusted]
#[ensures(result == 42u32)]
fn the_answer() -> u32 {
trusted_super_oracle("the answer to life, the universe and everything")
}
TODO: trusted traits
Representation of types
Creusot translates Rust code to Why3. But Rust types don't always exists in Why3, and would not have the same semantics: thus we need to map each Rust type to a Why3 type.
Base types
Most type have a very simple interpretation in Why3:
-
The interpretation for a scalar type (
i32
,bool
,f64
, ...) is itself. -
The interpretation of
[T]
isSeq<T>
, the type of sequences. -
The interpretation of
&T
is the interpretation ofT
.Creusot is able to assume that
T
and&T
behave the same, because of the "shared xor mutable" rule of Rust: we won't be able to change the value (interior mutability is handled by defining a special model for e.g.Cell<T>
), so we may as well be manipulating an exact copy of the value. -
The interpretation of
Box<T>
is the interpretation ofT
.This is because a
Box
uniquely own its content: it really acts exactly the same asT
. -
Structures and enums are interpreted by products and sum types.
-
The interpretation of
&mut T
is a bit more complicated: see the next section.
Mutable borrows
In Creusot, mutable borrows are handled via prophecies. A mutable borrow &mut T
contains:
- The current value (of type
T
) written in the borrow. - The last value written in the borrow, called the prophecy.
This works in the underlying logic by performing two key steps:
- When the borrow is created, the prophecy is given a special
any
value. We don't know it yet, we will trust the logic solvers to find its actual value based on later operations. - When dropping the borrow, Creusot adds the assumption
current == prophecy
. We call that resolving the prophecy.
Final reborrows
The above model is incomplete: in order to preserve soundness (see the Why is this needed ? section), we need to add a third field to the model of mutable borrows, which we call the id.
Each new mutable borrow gets a unique id, including reborrows. However, this is too restrictive: with this, the identity function cannot have its obvious spec (#[ensures(result == input)]
), because a reborrow happens right before returning.
To fix this, we introduce final reborrows:
A reborrow is considered final if the prophecy of the original borrow depends only on the reborrow. In that case, the reborrow id can be inherited:
- If this is a reborrow of the same place (e.g.
let y = &mut *x
), the new id is the same. - Else (e.g.
let y = &mut x.field
), the new id is deterministically derived from the old.
Examples
In most simple cases, the reborrow is final:
let mut i = 0;
let borrow = &mut i;
let reborrow = &mut *borrow;
// Don't use `borrow` afterward
proof_assert!(reborrow == borrow);
#[ensures(result == x)]
fn identity<T>(x: &mut T) -> &mut T {
x
}
It even handles reborrowing of fields:
let mut pair = (1, 2);
let mut borrow = &mut pair;
let borrow_0 = &mut borrow.0;
proof_assert!(borrow_0 == &mut borrow.0);
However, this is not the case if the original borrow is used after the reborrow:
let mut i = 0;
let borrow = &mut i;
let reborrow = &mut *borrow;
*borrow = 1;
proof_assert!(borrow == reborrow); // unprovable
// Here the prophecy of `borrow` is `1`,
// which is a value completely unrelated to the reborrow.
Or if there is an indirection based on a runtime value:
let mut list = creusot_contracts::vec![1, 2];
let borrow: &mut [i32] = &mut list;
let borrow_0 = &mut borrow[0];
proof_assert!(borrow_0 == &mut borrow[0]); // unprovable
Why is this needed ?
In essence, to prevent the following unsound code:
pub fn unsound() {
let mut x: Snapshot<bool> = snapshot! { true };
let xm: &mut Snapshot<bool> = &mut x;
let b: &mut Snapshot<bool> = &mut *xm;
let bg: Snapshot<&mut Snapshot<bool>> = snapshot! { b };
proof_assert! { ***bg == true && *^*bg == true };
let evil: &mut Snapshot<bool> = &mut *xm;
proof_assert! { (evil == *bg) == (*^evil == true) };
*evil = snapshot! { if evil == *bg { false } else { true } };
proof_assert! { **evil == !*^evil };
proof_assert! { **evil == !**evil };
}
If borrows are only a pair of current value/prophecy, the above code can be proven, and in particular it proves **evil == !**evil
, a false assertion.
Pearlite
Pearlite is the language used in:
- contracts (
ensures
,requires
,invariant
,variant
) proof_assert!
- the
pearlite!
macro
It can be seen as a pure, immutable fragment of Rust which has access to a few additional logical operations and connectives. In practice you have:
- Base Rust expressions: matching, function calls, let bindings, binary and unary operators, tuples, structs and enums, projections, primitive casts, and dereferencing
- Logical Expressions: quantifiers (
forall
andexists
), logical implication==>
, logical equalitya == b
, labels - Rust specific logical expressions: access to the final value of a mutable reference
^
, access to the view of an object@
Logical implication
Since =>
is already a used token in Rust, we use ==>
to mean implication:
proof_assert!(true ==> true);
proof_assert!(false ==> true);
proof_assert!(false ==> false);
// proof_assert!(true ==> false); // incorrect
Quantifiers
The logical quantifiers ∀ and ∃ are written forall
and exists
in Pearlite:
#[requires(forall<i: Int> i >= 0 && i < list@.len() ==> list@[i] == 0)]
fn requires_all_zeros(list: &[i32]) {
// ...
}
Logic functions
We provide two new attributes on Rust functions: logic
and predicate
.
Marked #[logic]
or #[predicate]
, a function can be used in specs and other logical conditions (requires
/ensures
and invariant
). They can use ghost functions.
The two attributes have the following difference:
- A
logic
function can freely have logical, non-executable operations, such as quantifiers, logic equalities, etc. Instead, this function can't be called in normal Rust code (the function body of alogic
function is replaced with a panic). You can use pearlite syntax for any part in the logic function by marking that part with thepearlite! { ... }
macro. - A
predicate
is a logical function which returns a proposition (in practice, returns a boolean value).
Recursion
When you write recursive ghost
, logic
or predicate
functions, you have to show that the function terminates.
For that, you can add #[variant(EXPR)]
attribute, which says that the value of the expression EXPR
strictly decreases (in a known well-founded order) at each recursive call.
The type of EXPR
should implement the WellFounded
trait.
Prophetic functions
As seen in the chapter on mutable borrow, a mutable borrow contains a prophetic value, whose value depends on future execution. In order to preserve the soundness of the logic, #[logic]
functions are not allowed to observe that value: that is, they cannot call the prophetic ^
operator.
If you really need a logic function to use that operator, you need to mark it with #[logic(prophetic)]
/#[predicate(prophetic)]
instead. In exchange, this function cannot be called from ghost code (unimplemented right now).
A normal #[logic]
function cannot call a #[logic(prophetic)]
function.
Examples
Basic example:
#[logic]
fn logic_add(x: Int, y: Int) -> Int {
x + y
}
#[requires(x < i32::MAX)]
#[ensures(result@ == logic_add(x@, 1))]
pub fn add_one(x: i32) -> i32 {
x + 1
}
Pearlite block:
#[predicate]
fn all_ones(s: Seq<i32>) -> bool {
pearlite! {
forall<i: Int> i >= 0 && i < s.len() ==> s[i]@ == 1
}
}
#[ensures(all_ones(result@))]
#[ensures(result@.len() == n@)]
pub fn make_ones(n: usize) -> Vec<i32> {
creusot_contracts::vec![1; n]
}
Recursion:
TODO
Prophetic:
TODO
Views as models of types
It is generally convenient to map a Rust type to its logical model. This logical model is the type that specifications and clients then use to describe values of this type in the logic.
In Creusot, this is provide using the creusot_contracts::View
trait, providing a view
method mapping the type to a ViewTy
associated type for the model.
Specifications can then use the @
operator as a syntactic sugar for .view()
on a type that implements View
.
For example, the following gives a spooky data type MyPair<T, U>
a nice pair model.
struct MyPair<T, U> {
fst: T,
snd: U,
}
impl<T, U> View for MyPair<T, U> {
type ViewTy = (T, U);
#[logic]
#[open]
fn view(self) -> Self::ViewTy {
(self.fst, self.snd)
}
}
#[ensures(result@ == (a, b))]
fn my_pair<T, U>(a: T, b: U) -> MyPair<T, U> {
MyPair(a, b)
}
Termination
By default, Creusot does not require that program functions terminate, which may lead to surprising situations:
#[ensures(result@ == 42)]
fn nonsense() -> u64 {
while true {}
1
}
This can be avoided at the cost of more complicated verification, by adding the terminates
attribute to a function:
#[terminates]
#[ensures(result@ == 42)]
fn nonsense() -> u64 { // Fails to compile now !
while true {}
1
}
A function with the terminates
attribute cannot:
-
Call a non-
terminates
function. -
Use a loop construct (
for
,while
,loop
). This restriction may be lifted in the future. -
Use simple recursion without the
variant
attribute.This means that this function will not be accepted:
#[terminates] fn f(x: u32) -> bool { if x == 0 { false } else { f(x - 1) } }
But this one will (the variant will be checked by why3):
#[terminates] #[variant(x)] fn f(x: u32) -> bool { if x == 0 { false } else { f(x - 1) } }
-
Use mutual recursion, like in the following example:
#[terminates] fn f() { g() } // Error: mutually recursive functions #[terminates] fn g() { f() }
Mutual recursion through traits
Traits complicate the recursion analysis. Indeed, if we have a function like
trait Tr {
fn f();
}
fn g<T: Tr>() {
<T as Tr>::f();
}
It isn't possible to know ahead of time which instance of f
will be called.
Thus, Creusot makes the decision to check recursion at instantiation time: the above example compiles, but
trait Tr {
fn f();
}
fn g<T: Tr>() {
<T as Tr>::f();
}
impl Tr for i32 {
fn f() {
g::<i32>();
}
}
does not, because we know that g::<i32>
calls <i32 as Tr>::f()
, causing a cycle.
Dependencies
The recursion check explores the body of the functions in the current crate, but it cannot do so with the dependencies, whose function bodies are not visible.
Thus, it assumes two things:
- The recursion check passed on the dependencies of the current crate.
- If a function in a dependency has a trait bound, the recursion check pessimistically assumes that all the functions of the trait are called.
Example:
// dependency/src/lib.rs
trait Tr {
fn f();
}
fn g<T: Tr>() {
// It does not matter if `<T as Tr>::f` is called or not, since we cannot see it
}
// lib.rs
impl Tr for i32 {
fn f() {
g::<i32>(); // Error !
}
}
Snapshots
A useful tool to have in proofs is the Snapshot<T>
type (creusot_contracts::Snapshot
). Snapshot<T>
is:
- a zero-sized-type
- created with the
snapshot!
macro - whose model is the model of
T
- that can be dereferenced in a logic context.
Example
They can be useful if you need a way to remember a previous value, but only for the proof:
#[ensures(array@.permutation_of((^array)@))]
#[ensures(sorted((^array)@))]
pub fn insertion_sort(array: &mut [i32]) {
let original = snapshot!(*array); // remember the original value
let n = array.len();
#[invariant(original@.permutation_of(array@))]
#[invariant(...)]
for i in 1..n {
let mut j = i;
#[invariant(original@.permutation_of(array@))]
#[invariant(...)]
while j > 0 {
if array[j - 1] > array[j] {
array.swap(j - 1, j);
} else {
break;
}
j -= 1;
}
}
proof_assert!(sorted_range(array@, 0, array@.len()));
}
#[logic]
fn permutation_of(s1: Seq<i32>, s2: Seq<i32>) -> bool {
// ...
}
#[logic]
fn is_sorted(s: Seq<i32>) -> bool {
// ...
}
Ghost code
Sometimes, you may need code that will be used only in the proofs/specification, that yet still obeys the typing rules of Rust. In this case, snapshots are not enough, since they don't have lifetimes or ownership.
This is why there exists a separate mechanism: ghost code.
ghost!
and Ghost<T>
are the counterparts of snapshot!
and Snapshot<T>
.
ghost!
blocks
At any point in the code, you may open a ghost!
block:
ghost! {
// code here
}
In ghost!
block, you may write any kind of Rust code, with the following restrictions :
- ghost code must terminate (see termination for details)
- all functions called must have the
#[pure]
attribute - When reading an outer variable, the variable must be of type
Ghost<T>
, or implementCopy
- When writing an outer variable, the variable must be of type
Ghost<T>
orSnapshot<T>
- The output of the
ghost!
block will automatically be wrapped inGhost::new
Those restriction exists to ensure that ghost code is erasable: its presence or absence does not affect the semantics of the actual running program, only the proofs.
Ghost<T>
The Ghost<T>
type is the type of "ghost data". In Creusot, it acts like a Box<T>
, while in normal running code, it is an empty type. It has the same View
as the underlying type, meaning you can use the @
operator directly.
The only restriction of Ghost<T>
is that it may not be dereferenced nor created in non-ghost code.
Examples
- Creating and modifying a ghost variable:
let mut g = ghost!(50);
ghost! {
*g *= 2;
};
proof_assert!(g@ == 100);
- Calling a function in ghost code:
#[pure]
#[requires(*g < i32::MAX)]
#[ensures((^g)@ == (*g)@ + 1)]
fn add_one(g: &mut i32) {
*g += 1;
}
let mut g = ghost!(41);
ghost! {
add_one(&mut *g);
};
proof_assert!(g@ == 42);
- Using a ghost access token:
struct Token;
struct Data {
// ...
}
impl Data {
fn new() -> (Data, Ghost<Token>) { /* */ }
fn read(&self, token: &Ghost<Token>) -> T { /* */ }
fn write(&self, t: T, token: &mut Ghost<Token>) { /* */ }
}
Ghost structures
Using imperative data structures in ghost code
Usual imperative structures like Vec
, HashMap
or HashSet
cannot be used in ghost code. To be precise, you may notice that functions like Vec::push
are not marked with the #[pure]
attribute, and so they cannot be called in ghost code.
This is because this function (and other like it) allocate memory, which on the actual machine, is finite. This currently translates to a possible inconsistency when using Vec
in ghost code:
use creusot_contracts::{proof_assert, ghost};
ghost! {
let mut v = Vec::new();
for _ in 0..=usize::MAX as u128 + 1 {
v.push(0);
}
proof_assert!(v@.len() <= usize::MAX@); // by definition
proof_assert!(v@.len() > usize::MAX@); // uh-oh
}
This case might be forbidden in the future by adding a stronger precondition to Vec::push
, but the point still stands: the capacity of a ghost vector should be infinite.
Ghost structures
As such, ghost code uses the mathematical structures Seq
, FMap
and FSet
, and adds program functions onto them.
The above snippet becomes:
use creusot_contracts::{proof_assert, ghost, Int, logic::Seq};
ghost! {
let mut s: Seq<Int> = Seq::new();
for _ in 0..=usize::MAX as u128 + 1 {
s.push_ghost(0);
}
// proof_assert!(s.len() <= usize::MAX@); // fails
proof_assert!(s.len() > usize::MAX@);
}
A few things to note:
Seq
,FMap
andFSet
are the counterparts forVec
,HashMap
andHashSet
respectively. They all live in thelogic
module.- Since
s
in directly the logical typeSeq
, we don't need the view operator@
. - To avoid name clashes, these additional "ghost" functions are almost all suffixed with
_ghost
. See the documentation for each type to know the available methods, ghost and logical.
Int in ghost
You can manipulate values of type Int
in ghost code, by using the int
suffix on integer literals:
ghost! {
let my_int = 42int;
};
Right now you cannot do much except pass it around: operations like +
or *
are only supported in pearlite.
Type Invariants
Overview
Defining a type invariant allows you to constrain a data type's set of valid values with a logical predicate that all values must satisfy. During verification, Creusot enforces that all type invariants are preserved across functions. Inside a function, values subject to type invariants may temporarily break their invariants as long as each value's invariant is restored before the value can be observed by another function.
Type invariants were added to Creusot as part of a Master's thesis available here.
Defining Type Invariants
To attach an invariant to a type, you implement the Invariant
trait provided by Creusot.
Here is an example:
struct SumTo10 {
a: i32,
b: i32,
}
// The type invariant constrains the set of valid `SumTo10`s to
// only allow values where the sum of both fields is equal to 10.
impl Invariant for SumTo10 {
#[predicate]
fn invariant(self) -> bool {
pearlite! {
self.a@ + self.b@ == 10
}
}
}
Enforcement of Type Invariants
Creusot enforces type invariants on function boundaries by generating additional pre- and postconditions based on the types of a function's arguments and return value. The type invariants of a function’s arguments are treated as additional preconditions and a type invariant of the return value corresponds to an extra postcondition. Here is an example:
// `inv` generically refers to the invariant of some value
#[requires(inv(x))] // generated by Creusot
#[ensures(inv(result))] // generated by Creusot
fn foo(x: SumTo10) -> SumTo10 {
x
}
These generated pre- and postconditions require you to prove the invariants of any values used as arguments in function calls or returned values.
Besides the proof obligations at function boundaries, you must also prove the type invariants of mutably borrowed values when the lifetimes of the created references end.
When creating a mutable reference r
, Creusot requires you to prove the type invariant of its current value at the end of r
's lifetime, since r
might have been used to break the invariant of the borrowed value.
This lets Creusot assume the invariant of the final value ^r
holds, simplifying the reasoning about mutable references.
Here is an example:
fn swap() {
let mut s = SumTo10 { a: 3, b: 7 }
let r = &mut s;
// Creusot can prophetically assume inv(^r) holds:
proof_assert! { inv(^r) };
let tmp = r.a;
*r.a = r.b;
*r.b = tmp;
// The lifetime of r ends: We must prove inv(*r)
proof_assert! { inv(v) }; // provable since v = ^r = *r
}
Structural Invariants
To determine the invariant of a particular type, Creusot considers both whether the user provided an explicit definition through the Invariant
trait, as well as any invariants the can be derived automatically based on the type's definition.
We call those automatically derived invariants structural invariants.
When neither an explicit definition exists, nor a structural invariant, the type has the trivial invariant, which does not impose any constraints on the set of valid values.
Here are some examples demonstrating structural invariants of various types:
Type of x | Invariant inv(x) |
---|---|
bool, u8, i32, ... | true |
&mut Foo | inv(*x) && inv(^x) |
&Foo | inv(*x) |
Box<Foo> | inv(*x) |
*const Foo, *mut Foo | true |
(Foo, Bar) | inv(x.0) && inv(x.1) |
struct Foo { f: Bar } | inv(x.f) |
enum Foo { A(Bar), B(Baz) } | match x { A(y) => inv(y), B(z) => inv(z) } |
Vec<Foo> | inv(x[0]) && ... && inv(x[x.len()-1]) |
Command-line Interface
List of commands
cargo creusot
cargo creusot prove
cargo creusot doc
cargo creusot clean
cargo creusot new
cargo creusot init
cargo creusot config
cargo creusot setup install
cargo creusot setup status
Main commands
creusot
cargo creusot [-- [-p <CRATE>]]
Run the Creusot compiler.
Output Coma code in the verif/
directory.
Options
-p <CRATE>
: Compile a specific crate<CRATE>
in a multi-crate project.
prove
cargo creusot prove [-i] [<COMA_FILE>]
Verify contracts.
This first runs cargo creusot
to be sure that the compiled code is up-to-date.
Then why3find
verifies the compiled Coma code: it generates verification conditions
and tries to prove them.
Options
-i
: Open the Why3 IDE on an unproved file to inspect its proof context.<COMA_FILE>
: Verify a specific file (corresponding to one Rust function).
doc
cargo creusot doc
Build documentation.
This is a variant of cargo doc
that also renders contracts (requires
and ensures
), logic functions, and predicates.
clean
cargo creusot clean [--force] [--dry-run]
Remove dangling proof files.
Removing (or renaming) Rust functions also removes (or renames) its compiled Coma file.
However, the Creusot compiler can't keep track of their associated proof files (proof.json
, why3session.xml
), which become dangling.
cargo creusot
will detect dangling files and print a warning to remind you to move those files if you want,
otherwise you can remove them with cargo creusot clean
.
Options
--force
: Don't ask for confirmation before removing dangling files.--dry-run
: Only print the list of files that would be removed bycargo creusot clean
.
Create package
new
cargo creusot new <NAME> [--main]
Create package named <NAME>
.
Create a directory <NAME>
and initialize it with starting configuration and source files.
Options
--main
: Createmain.rs
for an executable crate. (By default, only a library cratelib.rs
is created.)
init
cargo creusot init [<NAME>] [--main]
Create package in the current directory.
Options
<NAME>
: Name of the package. (By default, it is the name of the directory.)--main
: Createmain.rs
for an executable crate. (By default, only a library cratelib.rs
is created.)
config
cargo creusot config
Generate why3find
configuration.
This is used to set up Creusot in an existing Rust package.
You don't need to run this command if you used cargo creusot new
or cargo creusot init
.
Show configuration
setup status
cargo creusot setup status
Show the status of the Creusot installation.
Print tool locations and configuration paths.