Formal Analysis Lecture Notes
=============================
You have learned about pre- and post-conditions, loop invariants, and data invariants in 15-122, and we have extended the concept of data invariants to class invariants earlier in this course. In today's lecture, we will see how tools can aid in formal analysis, helping programmers to confirm that the program specifications they write are correct.
Dafny is a language and program verifier for functional correctness. The language is imperative and object-based--that is, it has objects like Java, but no inheritance (leaving out inheritance inhibits reuse in some cases, but simplifies verification substantially). Notably, the language includes built-in specification constructs such as pre- and post-conditions, invariants, and others.
URLs for more information on Dafny:
The Dafny Home Page http://research.microsoft.com/en-us/projects/dafny/
Try Dafny in your Browser http://rise4fun.com/dafny/Hello
Writing Specifications in Dafny
-------------------------------
Let's recall the definition of pre- and post-conditions from earlier lectures:
Precondition: a logical condition a function relies on for correct operation. Must be established by the caller before the function is invoked.
Postcondition: a logical condition a function is expected to establish upon termination.
Now let's look at how to implement a simple max function in Dafny:
http://rise4fun.com/Dafny/UO1T
We will add a specification, but before we do, let's write a test function for Max. An assertion verifies the correct result:
method TestMax()
{
var m := Max(10,5);
assert m == 10;
}
Of course, the Dafny program verifier cannot prove that the assertion will succeed, because it doesn't look inside Max--it only looks at Max's specification, which we have not yet written. So let's write one. Do it yourself, then peek at the answer at the bottom of these notes.
<< sample solution at bottom of notes>>
Now the test succeeds--and Dafny verifies that Max meets its specification as well.
Verifying Loops in Dafny
------------------------
Let's now write a function that computes the maximum element of an array:
method FindMax(a: array) returns (m: int)
// write a specification
{
// write an implementation
}
method TestFindMax()
{
var a := new int[2];
a[0], a[1] := 10, 20;
var m := FindMax(a);
assert m == 20;
}
In order to get this function to verify with a loop, we need a loop invariant. Recall the definition of loop invariant:
Loop invariant: a logical condition that is true before the loop executes and is maintained by each execution of the loop body
We choose our loop invariant so that when the loop exit condition is true, the postcondition of the function has been established. In fact our loop invariant looks just like our postcondition, except that the length of the array is replaced with the index. Thus it represents our progress computing the maximum value of a subset of the array. This is a typical pattern in loop invariants.
We would also like Dafny to check that our loop terminates. In order to do this we need to specify a variant function. A variant function is an integer-valued expression that decreases on each iteration through the array, and is bounded below by 0 at the end of the loop iteration. If the variant function decreases by at least 1 each iteration, and is bounded below by 0, then the loop must eventually terminate.
(see the end for the solution)
You may have noticed that Dafny verified our FindMax implementation even before we added the decreases annotation to the loop. Dafny tries to infer decreases clauses automatically if they are not specified, and is successful in many simple cases such as this loop.
Functions in Dafny
------------------
Dafny allows us to write mathematical functions in a style that mimics functional programming, but also provides convenient mathematical abstractions. As an introduction, consider the definition of the fibonacci function in Dafny:
function fib(n: nat): nat
decreases n;
{
if n == 0 then 0 else
if n == 1 then 1 else
fib(n - 1) + fib(n - 2)
}
Here we use the decreases notation to help Dafny prove that fib terminates. Of course, this is very inefficient. We can define a more efficent version, and prove that it is equivalent to the mathematical specification. Start with the following and see what you can prove:
method ComputeFib(n: nat) returns (b: nat)
// specification goes here
{
// implementation goes here
}
(the solution is below)
Defining mathematical functions is frequently useful, and we can define and prove the termination of quite interesting functions. How would you prove the termination of the Ackermann function?
function Ack(m: nat, n: nat): nat
{
if m == 0 then n + 1
else if n == 0 then Ack(m - 1, 1)
else Ack(m - 1, Ack(m, n - 1))
}
(the solution is below)
Classes in Dafny
----------------
We can also prove implementations of data structures correct. Let's prove the correctness of a SimpleSet class. We will start with an implementation with some test cases, and try to get it to verify by adding specifications.
see SimpleSet-orig.dfy
see SimpleSet-soln1.dfy
We observe that the test driver creates two sets that have the same backing array--and this causes verification problems and corresponding bugs. A better way is to ensure that the backing arrays are separate for each set. Allocating the arrays in the constructor and using freshness specifications allows us to address this problem.
see SimpleSet-soln2.dfy
============= SOLUTIONS =============
method Max(a: int, b:int) returns (c: int)
ensures (c == a || c == b) && c >= a && c >= b;
{
if (a > b)
{ return a; }
else
{ return b; }
}
method FindMax(a: array) returns (m: int)
requires a != null && a.Length >= 1;
ensures forall k :: 0 <= k < a.Length ==> m >= a[k];
ensures exists k :: 0 <= k < a.Length && m == a[k];
{
m := a[0];
var index := 1;
while (index < a.Length)
invariant 0 <= index <= a.Length;
invariant forall k :: 0 <= k < index ==> m >= a[k];
invariant exists k :: 0 <= k < index && m == a[k];
decreases a.Length - index;
{
m := Max(m, a[index]);
index := index + 1;
}
}
method ComputeFib(n: nat) returns (b: nat)
ensures b == fib(n);
{
if (n == 0) { return 0; }
var i: int := 1;
var a := 0;
b := 1;
while (i < n)
invariant 0 < i <= n;
invariant a == fib(i - 1);
invariant b == fib(i);
{
a, b := b, a + b;
i := i + 1;
}
}
function Ack(m: nat, n: nat): nat
decreases m, n;
{
if m == 0 then n + 1
else if n == 0 then Ack(m - 1, 1)
else Ack(m - 1, Ack(m, n - 1))
}