Jean-Baptiste Tristan & K. Rustan M. Leino |
A postcondition is a property we expect the function to have
It is declared using the ensures
keyword, as with lemmas
function Append<T>(l1: List, l2: List): List
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
{
match l1
case Nil => l2
case Cons(e, l) => Cons(e, Append(l, l2))
}
This defines the function and a postcondition
Of course, Dafny needs to prove the postcondition
When the function is called, the postcondition is implicitly added to Dafny's knowledge base (KB)
How should we understand the body? Is it code? Is it a proof?
In a sense, it is both
To be more precise, it really is code, but the verifier knows how to turn code into proofs
You can also interpret it as such by ignoring the values
The recursive call to Append
defines the function, but also serves as an induction hypothesis
function Append<T>(l1: List, l2: List): List
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
{
match l1
case Nil => l2
case Cons(e, l) => Cons(e, Append(l, l2))
}
function Append<T>(l1: List, l2: List): List
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
ensures forall n: nat :: n % 2 == 0 ==> exists m: nat :: n == 2 * m
What if the code isn't a good enough proof?
Answer 1: make the postcondition a lemma
When the lemma is called, Dafny's KB is extended
lemma SomeProperty()
ensures forall n: nat :: n % 2 == 0 ==> exists m: nat :: n == 2 * m
function Append<T>(l1: List, l2: List): List
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
ensures forall n: nat :: n % 2 == 0 ==> exists m: nat :: n == 2 * m
{
SomeProperty();
match l1
case Nil => l2
case Cons(e, l) => Cons(e, Append(l, l2))
}
So far, the body of function had to be an expression, but it turns out we are also allowed to call lemmas from the proof language
What if the code isn't a good enough proof?
Answer 2: inline proof scripts in the code
Syntactically, proof script must precede value
function Append<T>(l1: List, l2: List): List
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
ensures forall n: nat :: n % 2 == 0 ==> exists m: nat :: n == 2 * m
{
assert forall n: nat :: n % 2 == 0 ==> exists m: nat :: n == 2 * m by {
forall n: nat
ensures n % 2 == 0 ==> exists m: nat :: n == 2 * m
{
if n % 2 == 0 {
assert n == 2 * (n/2);
}
}
}
match l1
case Nil => l2
case Cons(e, l) => Cons(e, Append(l, l2))
}
function LengthTr'<T>(l: List, acc: nat): nat
ensures acc + Length(l) == LengthTr'(l, acc)
{
match l
case Nil => acc
case Cons(_, tail) => LengthTr'(tail, 1 + acc)
}
function LengthTr<T>(l: List): nat
ensures Length(l) == LengthTr'(l, 0)
{
LengthTr'(l, 0)
}
Should a property be declared (“intrinsically”) as a postcondition or (“extrinsically”) a lemma?
The benefit of a postcondition is that it is automatically included in the KB: more automation
However, some properties are really extrinsic
Quantification would have a high cost on verification
But you can't even do it because of termination
function Append<T(!new)>(l1: List, l2: List): List
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
// ensures forall l3: List :: Append(Append(l1, l2), l3) == Append(l1, Append(l2, l3))
A precondition is a property we require to be true when the function is called
It is declared using the requires
keyword, as with lemmas
function At<T>(l: List, index: nat): T
requires index < Length(l)
{
if index == 0 then l.head else At(l.tail, index - 1)
}
This defines the function and a precondition
To call the function, Dafny must prove precondition
The precondition is added to Dafny's KB within the function
A
to B
is of type A -> B
A
to B
is of type A --> B
Preconditions can make your code simpler and faster
lemma AtProp<T(!new)>(l: List, idx: nat)
requires idx < Length(l)
ensures exists v: T :: At(l, idx) == v
{
}
But for every call, Dafny must prove the preconditions
If it cannot, you might need to add to the KB of the caller
If a function argument is partial, you may need to use its requires
clause
function Hof<U, V>(f: U --> V, a: U): V
requires f.requires(a)
{
f(a)
}
.requires
member is Boolean-valued total ghost function
function Hof<U, V>(f: U --> V, a: U): V
requires f.requires(a)
{
f(a)
}
You can attach a predicate to a value
Allows you not to constantly require and ensure basic properties
The predicate must be proved for any expression of that type
type MyNat = n: int | 0 <= n
MyNat
is called a subset type, because its values form of subset of its base type, int
Dafny provides a way to guarantee that a subset type is not empty
It is done using a witness
clause
In the witness
clause, you must provide an example of a value that satisfies the predicate
type MyNat = n: int | 0 <= n witness 4
Always type variables whose values might contain a subset type
For example, if x
and y
are meant to be natural numbers
method m(z: nat) {
var x := z + 3; // No
var y: nat := z + 3; // Yes
}
Not so important for nat
, but is often a good idea for more complicated subset types
ghost predicate Associative<T(!new)>(BinOp: (T, T) -> T) {
forall x: T, y: T, z: T :: BinOp(BinOp(x, y), z) == BinOp(x, BinOp(y, z))
}
ghost predicate Identity<T(!new)>(BinOp: (T, T) -> T, elt: T) {
forall x: T :: BinOp(x, elt) == x
}
ghost predicate Inverse<T(!new)>(BinOp: (T, T) -> T, elt: T) {
forall x: T :: exists y: T :: BinOp(x, y) == elt && BinOp(y, x) == elt
}
type Group<!T(!new)> = S: ((T, T) -> T, T) |
&& Associative(S.0)
&& Identity(S.0, S.1)
&& Inverse(S.0, S.1)
witness *
!
and !new
for now