Jean-Baptiste Tristan & K. Rustan M. Leino |
How do we verify functional code?
That is, how do we tell Dafny: “I want to prove that my function has this property”?
Recall that function calls are functional expressions, and as such, they are valid logical expressions
Therefore, a lemma's formula can state properties on a function
function Increment(n: int): int {
n + 1
}
lemma IncrementLarger()
ensures forall n: int :: Increment(n) > n
lemma InrementEvenIsOdd()
ensures forall n: int :: n % 2 == 0 ==> Increment(n) % 2 == 1
match
expression, then the proof can use a match
statement
function Abs(x: int): int {
if x < 0 then -x else x
}
lemma AbsPositive(x: int)
ensures Abs(x) >= 0
{
if x < 0 {
assert -x > 0;
} else {
assert x >= 0;
}
}
(Note the syntactic difference between the if-then-else
expression and the if
statement)
datatype Size =
| Small
| Large
function Ounces(s: Size): int {
match s
case Small => 4
case Large => 8
}
lemma OuncesMultiple4(s: Size)
ensures exists n: int :: Ounces(s) == 4 * n
{
match s
case Small =>
assert Ounces(s) == 4 * 1;
case Large =>
assert Ounces(s) == 4 * 2;
}
function Increment(n: int): int {
n + 1
}
lemma IncrementLarger()
ensures forall n: int :: Increment(n) > n
{}
lemma InrementEvenIsOdd()
ensures forall n: int :: n % 2 == 0 ==> Increment(n) % 2 == 1
{
forall n: int
ensures n % 2 == 0 ==> Increment(n) % 2 == 1
{
if n % 2 == 0 {
assert Increment(n) == n + 1;
}
}
}
Increment(n) == n + 1
You may not want Dafny to use the definition of the function on its own
You can declare it opaque
opaque function Increment(n: int): int {
n + 1
}
lemma IncrementLarger()
ensures forall n: int :: Increment(n) > n
{
reveal Increment();
}
lemma InrementEvenIsOdd()
ensures forall n: int :: n % 2 == 0 ==> Increment(n) % 2 == 1
{
reveal Increment();
forall n: int
ensures n % 2 == 0 ==> Increment(n) % 2 == 1
{
if n % 2 == 0 {
}
}
}
datatype List<T> =
| Nil
| Cons(head: T, tail: List)
function Length<T>(l: List): nat {
if l.Nil? then 0 else 1 + Length(l.tail)
}
function Append<T>(l1: List, l2: List): List {
match l1
case Nil => l2
case Cons(e, l) => Cons(e, Append(l, l2))
}
lemma AppendAssoc<T>(l1: List, l2: List, l3: List)
ensures Append(Append(l1, l2), l3) == Append(l1, Append(l2, l3))
{
match l1
case Nil =>
case Cons(e, l) =>
AppendAssoc(l, l2, l3); // this makes the proof clear to a human, but this line is not needed to convince Dafny
}
lemma AppendAssoc<T>(l1: List, l2: List, l3: List)
ensures Append(Append(l1, l2), l3) == Append(l1, Append(l2, l3))
{
match l1
case Nil =>
case Cons(e, l) =>
AppendAssoc(l, l2, l3); // this makes the proof clear to a human, but this line is not needed to convince Dafny
}
lemma AppendLength<T>(l1: List, l2: List)
ensures Length(Append(l1, l2)) == Length(l1) + Length(l2)
{
match l1
case Nil =>
case Cons(e, l) =>
AppendLength(l, l2); // Dafny doesn't actually need this line
}
Consider an alternative, tail-recursive, definition of the length function
function LengthTr'<T>(l: List, acc: nat): nat {
match l
case Nil => acc
case Cons(_, tail) => LengthTr'(tail, 1 + acc)
}
function LengthTr<T>(l: List): nat {
LengthTr'(l, 0)
}
First, note how the code had to be restructured
Indeed, we had to define a more general function
The Length
function is just a special case
(As a side note, Dafny implements auto-accumulator tail recursion, so even the original definition of
Length
is compiled into a loop. But that's not the point of this example.)
l
lemma LengthSame<T>(l: List)
ensures Length(l) == LengthTr(l)
lemma LengthSame'<T>(l: List, acc: nat)
ensures acc + Length(l) == LengthTr'(l, acc)
{
match l
case Nil =>
case Cons(_, tail) =>
LengthSame'(tail, acc + 1);
}
LengthTr
, it is easy, because the code hinted at the need for generalization
ghost function Increment(n: int): int
lemma EssenceOfIncrement(n: int)
ensures Increment(n) == n + 1
Increment
a definition as a functional program
ghost function Increment(n: int): int {
n + 1
}
In most cases, the underlying order is obvious and Dafny can infer it
function Sum(n: int): int
decreases n // this line can be omitted, because Dafny will infer it
{
if n <= 0 then 0 else Sum(n - 1)
}
If Dafny infers the decreases
clause, hover text in the IDE will tell you what it is
If not, you can supply a hint using a decreases
clause
function SumUpTo(counter: int, upTo: int): int
decreases upTo - counter // here, the decreases clause is needed; Dafny will not infer it
{
if upTo <= counter then
counter
else
counter + SumUpTo(counter + 1, upTo)
}
But then, why do we need ghost
at all?
First, the ghost
keyword guarantees that your function will not end up being used by the code
ghost
ensures it will not end up in your executable
Second, while all terminating functions that can be defined using functional programming exist, there exist functions that cannot be defined algorithmically
predicate P<T>(m: T, n: T)
ghost predicate Prop<T(!new)>(m: T) {
exists n: T :: P(m, n)
}