9 Loop Invariants
In Floyd Hoare Logic, we studied the Hoare Triple rules for assignments and conditionals. In this lecture, we will extend that discussion to the Hoare Triple rules for loops.
9.1 While Loops
The Hoare rule for while loops is based on the idea of an invariant: an assertion whose truth is guaranteed before and after executing a statement. An assertion P is an invariant of s if
{P} s {P}
As a first attempt at a [while] rule, we could try:
{P} s {P}
---------------------while b { s } {P} {P}
{P} s {P}
---------------------------while b { s } {P && !b} {P}
{P && b} s {P}
--------------------------- (Hoare While)while b { s } {P && !b} {P}
If the loop body executes zero times, the rule is like an empty block in that the precondition survives to become (part of) the postcondition.
Like a conditional, we can assume guard b holds on entry to the substatement
True before the loop starts
True after each iteration of the loop
Helps prove that the loop works correctly
Unlike most programming languages, Dafny requires formal verification that your loops behave correctly. Since loops may run many times, Dafny cannot rely on checking just a single iteration. Instead, it needs a logical guarantee that the loop preserves correctness at every step and will eventually terminate. This is achieved using two key elements: loop invariants, which express conditions that must remain true throughout the loop, and a termination metric (typically specified with a decreases clause) to prove that the loop will not run forever. In Dafny, loop invariants are written using the invariant keyword within the while loop.
Let’s look at examples of loops with no bodies. We ignore Dafny’s complaints that loops have no bodies.
To enter the loop, we must show that the chosen invariant holds.
Exiting the loop, we may assume that the chosen invariant holds and the loop test failed.
Dafny
method Loop1() {var x: int; assume x % 2 == 0; while x < 300 invariant x % 2 == 0 { }assert !(x < 300) && x % 2 == 0; }
You can apply the same recipe step by step for the following code:
Dafny
method Loop2() {var x: int; assume 0 <= x <= 100; while x % 2 == 1 invariant 0 <= x <= 100 { }assert !(x % 2 == 1) && 0 <= x <= 100; }
Dafny
method Loop3() {var x: int := 2; assert x % 2 == 0; while x < 50 invariant x % 2 == 0 { }assert !(x < 50) && x % 2 == 0; }
Dafny
method Loop4() {var x: int := 0; assert 0 <= x <= 20; while x % 2 == 0 invariant 0 <= x <= 20 { }assert !(x % 2 == 0) && 0 <= x <= 20; assert x == 19; //oops }
Dafny
method Loop5() {var i: int := 0; while i != 100 invariant 0 <= i <= 100 { }assert i == 100; }
Dafny
method Loop6() {var i: int := 0; while i < 10 invariant 0 <= i <= 10 {if we have i := i + 3; //what }assert i == 10; }
Dafny
method Loop7() {var x, y := 0, 0; while x < 300 invariant 2 * x == 3 * y { }assert 200 <= y; }
Now that we’ve covered enough of the basics, let’s consider a simple loop that terminates when x ≤ 0.
Dafny
method LoopToZero_1(m : int, p : int) returns (x: int, z: int) { x := m; z := p;while x > 0 { 1; z := z - 1; x := x - } }
Dafny
assert true; x := m; z := p;while x > 0 { 1; z := z - 1; x := x - }assert z == p - m;
Note: of course, Dafny can’t prove this without an invariant annotation
Dafny
method LoopToZero_2(m : nat, p : int) returns (x: nat, z: int) requires true ensures z == p - m { x := m; z := p;while x > 0 invariant (z - x == p - m) {1; z := z - 1; x := x - } }
Here is an annotated version of this program, embodying a proof of this specification:
Dafny
method LoopToZero_3(m : nat, p : int) returns (x: nat, z: int) { x := m;assert x == m; z := p;assert x == m && z == p; // -> assert z - x == p - m; while x > 0 invariant z - x == p - m {assert z - x == p - m && x > 0; // -> assert (z - 1) - (x - 1) == p - m; 1; z := z - assert z - (x - 1) == p - m; 1; x := x - assert z - x == p - m; }assert z - x == p - m && !(x > 0) ==> z == p - m; }
That is, an annotated program consists of the program’s text interleaved with assertions (sometimes multiple assertions separated by implications).
An annotated program can be viewed as a compact representation of a proof in Hoare Logic: the assertions surrounding each command specify the Hoare triple to be proved for that part of the program using one of the Hoare Logic rules, and the structure of the program itself shows how to assemble all these individual steps into a proof for the whole program.
Dafny’s goal is to verify such annotated programs "mostly automatically." But, before we can verify anything, we need to be able to find a proof for a given specification, and for this we need to discover the right assertions. This can be done in an almost mechanical way, with the exception of finding loop invariants. We’ll first explain in detail how to construct annotations for several short programs, all of which are loop-free or have simple loop invariants. We’ll return to finding more interesting loop invariants later.
9.1.1 Reduce to Zero
Here is a while loop that is so simple that true suffices as a loop invariant when x is a nat - that is, Dafny doesn’t even need an invariant annotation to verify it (why?)
Dafny
method LoopToZero_Simple(n : nat) returns (x : nat) ensures x == 0 { x := n;while x > 0 invariant true {1; x := x - } }
Dafny
1) {true} (while x > 0 { 2) { true && x > 0 } -> (3) { true } (1 x := x - 4) { true } ( }5) { true && !(x > 0) } -> (6) { x = 0 } (
Start with the outer precondition (1) and postcondition (6).
Following the format dictated by the [Hoare While] rule, we copy (1) to (4). We conjoin (1) with the guard to obtain (2). We also conjoin (1) with the negation of the guard to obtain (5).
Because the final postcondition (6) does not syntactically match (5), we add an implication between them.
Using the assignment rule with assertion (4), we trivially substitute and obtain assertion (3).
We add the implication between (2) and (3).
9.1.2 Division
Let’s do one more example of simple reasoning about a loop.
Dafny
method QuotDiv (m : int, n : int) returns (x : int, y : int) { x := m;0; y := while n <= x { x := x - n;1; y := y + } }
If we execute this program, it will terminate with the variable x set to the remainder when m is divided by n and y set to the quotient.
In order to give a specification to this program we need to remember that dividing m by n produces a remainder x and a quotient y such that n * y + x == m && x < n.
Dafny
1) { true } -> (2) { n * 0 + m == m } ( x := m;3) { n * 0 + x == m } (0; y := 4) { n * y + x == m } (while n <= x { 5) { n * y + x == m && n <= x } -> (6) { n * (y + 1) + (x - n) == m } ( x := x - n;7) { n * (y + 1) + x == m } (1; y := y + 8) { n * y + x == m } ( }9) { n * y + x == m && !(n <= x) } -> (10) { n * y + x == m && x < n } (
(1) -> (2): trivial, by algebra.
(5) -> (6): because [n <= x], we are guaranteed that the subtraction in (6) does not get zero-truncated. We can therefore rewrite (6) as [n * y + n + x - n] and cancel the ns, which results in the left conjunct of (5).
(9) -> (10): if [!(n <= x)] then [x < n]. That’s straightforward from high-school algebra.
Dafny
method QuotDiv' (m : int, n : int) returns (x : int, y : int) requires n > 0 ensures n > x ensures y * n + x == m { x := m;0; y := assert y * n + x == m; while n <= x invariant y * n + x == m {assert n <= x && y * n + x == m ==> y * n + x == m; assert (y+1) * n + (x-n) == m; x := x - n;assert (y+1) * n + x == m; 1; y := y + assert y * n + x == m; }assert !(n <= x) && y * n + x == m ==> n > x && y * n + x == m; }
9.2 Finding Loop Invariants
Strengthening a loop invariant means that you have a stronger assumption to work with when trying to establish the postcondition of the loop body, but it also means that the loop body’s postcondition is stronger and thus harder to prove.
Strengthening an induction hypothesis means that you have a stronger assumption to work with when trying to complete the induction step of the proof, but it also means that the statement being proved inductively is stronger and thus harder to prove.
As you recall the [Hoare While] rule,
{P && b} s {P}
--------------------------- (Hoare While)while b { s } {P && !b} {P}
P must be strong enough to prove the postcondition and weak enough to be inferred from the precondition. This section explains how to approach the challenge of finding loop invariants through a series of examples and exercises.
9.2.1 Slow Subtraction
Dafny
{ x == m && y == n }while !(x == 0) do 1; y := y - 1; x := x - end { y == n - m }
1) { x == m && y == n } -> (a)
(2) { Inv }
(while x > 0 {
3) { Inv && x > 0 } -> (c)
(4) { Inv [x := x-1] [y := y-1] }
(1;
y := y - 5) { Inv [x := x-1] }
(1
x := x - 6) { Inv }
(
}7) { Inv && !(x > 0) } -> (b)
(8) { y == n - m } (
it must be weak enough to be implied by the loop’s precondition, i.e., (1) must imply (2);
(b) it must be strong enough to imply the program’s postcondition, i.e., (7) must imply (8);
(c) it must be preserved by each iteration of the loop (given that the loop guard evaluates to true), i.e., (3) must imply (4).
One way to find an invariant that simultaneously satisfies these three conditions is by using an iterative process: start with a "candidate" invariant (e.g., a guess or a heuristic choice) and check the three conditions above; if any of the checks fails, try to use the information that we get from the failure to produce another – hopefully better – candidate invariant, and repeat.
For instance, in the reduce-to-zero example above, we saw that, for a very simple loop, choosing [true] as an invariant did the job. Maybe it will work here too. To find out, let’s try instantiating [Inv] with [true] in the skeleton above and see what we get...
1) { x == m && y == n } -> (a - OK)
(2) { true }
(while x > 0 {
3) { true && x > 0 } -> (c - OK)
(4) { true }
(1;
y := y - 5) { true }
(1
x := x - 6) { true }
(
}7) { true && !(x > 0) } -> (b - WRONG!)
(8) { y == n - m } (
If we want (b) to hold, we need to strengthen the invariant so that it implies the postcondition (8). One simple way to do this is to let the invariant be the postcondition. So let’s return to our skeleton, instantiate [Inv] with [y == n - m], and try checking conditions (a) to (c) again.
1) { x == m && y == n } -> (a - WRONG!)
(2) { y == n - m }
(while x > 0 {
3) { y == n - m && x > 0 } -> (c - WRONG!)
(4) { y - 1 == n - m }
(1;
y := y - 5) { y == n - m }
(1
x := x - 6) { y == n - m }
(
}7) { y == n - m && !(x > 0) } -> (b - OK)
(8) { y == n - m } (
This failure is not very surprising: the variable y changes during the loop, while m and n are constant, so the assertion we chose didn’t have much chance of being an invariant!
1) { x == m && y == n } -> (a - OK)
(2) { y - x == n - m }
(while x > 0 {
3) { y - x == n - m && x > 0 } -> (c - OK)
(4) { (y - 1) - (x - 1) == n - m }
(1;
y := y - 5) { y - (x - 1) = n - m }
(1
x := x - 6) { y - x == n - m }
(
}7) { y - x == n - m && !(x > 0) } -> (b - OK?)
(8) { y = n - m } (
1) { m >= 0 && n >= 0 && x == m && y == n } -> (a - OK)
(2) { y - x == n - m }
(while x > 0 {
3) { x >= 0 && y - x == n - m && x > 0 } -> (c - OK)
(4) { (x - 1) >= 0 && (y - 1) - (x - 1) == n - m }
(1;
y := y - 5) { (x - 1) >= 0 && y - (x - 1) = n - m }
(1
x := x - 6) { x >= 0 && y - x == n - m }
(
}7) { x >= 0 && y - x == n - m && !(x > 0) } -> (b - OK)
(8) { y = n - m } (
Dafny
method SlowSubtraction(n:int, m:int) returns (y:int) requires m >= 0 {assert true ==> n - m == n - m; assert n - m == n - m; var x := m; assert n - x == n - m; y := n;assert y - x == n - m; while !(x == 0) invariant y - x == n - m decreases x {assert (y - 1)-(x - 1) == n - m; 1; y := y - assert y - (x - 1) == n - m; 1; x := x - assert y - x == n - m; }assert y - x == n - m && x == 0 ==> y == n - m; }
Dafny
method SlowSubtraction'(n:int, m:int) returns (y:int) requires m >= 0 {var x := m; y := n;while !(x == 0) invariant y - x == n - m decreases x {1; y := y - 1; x := x - } }
9.2.2 Slow Assignment
Dafny
method Slow_Assignment(m : nat) returns (y:nat) ensures m == y {var x:= m; assert x == m ==> x >= 0; 0; y := assert x + y == m && x >= 0; while x > 0 invariant x + y == m && x >= 0 {assert x + y == m && x >= 0; assert x-1 + (y+1) == m && x >= 0; 1; x := x - assert x + (y+1) == m && x >= 0; 1; y := y + assert x + y == m && x >= 0; }assert x + y == m && x >= 0 && ! (x > 0) ==> y == m; assert x + y == m; }
9.2.3 Parity
{ x == m }while 2 <= x do
2
x := x - end
{ x = parity m }
function parity(x:nat): nat {
match x
case 0 => 0
case 1 => 1
case _ => parity (x - 2)
}
Dafny
method compute_parity(m:nat) returns (x:nat) requires true // ensures (x == parity(m)) // {true } -> // { // { parity(m) == parity(m) } x := m; // { parity(x) == parity(m) }while 2 <= x {2 <= x} // {parity(x) == parity(m) && 2) == parity(m)} // {parity(x - 2; x := x - // {parity(x) == parity(m)} }2 <= x)} -> // { parity(x) == parity(m) && !( // { x == parity(m) } }
The postcondition does not hold at the beginning of the loop, since [m == parity m] does not hold for an arbitrary m, so we cannot hope to use that as an invariant. To find an invariant that works, let’s think a bit about what this loop does.
{ x == m } -> (a - OK)
{ parity x == parity m }while 2 <= x {
2 <= x } -> (c - OK)
{ parity x == parity m && -2) == parity m }
{ parity (x2
x := x -
{ parity x == parity m }
}2 <= x) } -> (b - OK)
{ parity x == parity m && !( { x == parity m }
Dafny
method compute_parity(m:nat) returns (x:nat) ensures (x == parity(m)) {assert true==> parity(m) == parity(m); x := m;assert parity(x) == parity(m); while 2 <= x invariant parity(x) == parity(m) {assert parity(x) == parity(m) && 2 <= x; assert parity(x - 2) == parity(m); 2; x := x - assert parity(x) == parity(m); }assert parity(x) == parity(m) && !(2 <= x) ==> x == parity(m); }
9.2.4 Square Root
The following program computes the integer square root of m by naive iteration:
0;
z := while (z+1)*(z+1) <= m {
1
z := z+
}1)*(z+1) } { z*z<=m && m<(z+
Very often, when a variable is used in a loop in a read-only fashion (i.e., it is referred to by the program or by the specification and it is not changed by the loop), it is necessary to record the fact that it doesn’t change in the loop invariant.
Dafny
method SquareRoot(m:int) returns (z:int) ensures z * z <= m <= (z + 1) * (z+1) requires m >= 0 {assert m >0 ==> 0 * 0 <= m; assert 0 * 0 <= m; 0; z := assert z * z <= m; while (z+1)*(z+1) <= m invariant z * z <= m decreases m - z * z {assert (z+1)*(z+1) <= m; 1; z := z + assert z * z <= m; }assert z*z <= m < (z+1)*(z+1) && !((z+1)*(z+1) <= m); }
9.2.5 Squaring
{ x == m }0;
y := 0;
z := while y < x {
z := z + x;1
y := y +
} { z == m*m }
Dafny
method Square(x:nat) returns (z:nat) ensures x * x == z {assert true ==>0 == 0 * x; assert 0 == 0 * x; var y := 0; assert 0 == y * x; 0; z := assert z == y * x; while y < x invariant z == x * y invariant 0 <= y <= x decreases x - y {assert z + x == x * (y+1) ==> z == x * y; z := z + x;assert z == x * (y+1); 1; y := y + assert z == x * y; }assert z == x * y && !(y < x) ==> z == x * x; }
9.2.6 Factorial
Recall that n! denotes the factorial of n (i.e., n! = 1*2*...*n). We can easily define it in Dafny as follows:
function fact (n : nat) : nat {
match n
case 0 => 1
case _ => n * (fact(n-1))
}
Dafny
method factorial(n:nat) returns (x:nat) requires true ensures (x == fact(n)) {var i := 0; 1; x := assert x == fact(i) ; while (i < n) invariant x == fact(i) invariant 0 <= i <= n decreases n - i {assert x == fact(i) && 0 <= i <= n; 1; i := i + x := x * i;assert x == fact(i) && 0 <= i <= n; } assert x == fact(i) && 0 <= i <= n && !(i < n); // simplifyassert x == fact(i) && i == n; }
9.2.7 Minimum
Dafny
method Min(a:nat, b:nat) returns (z:nat) ensures z == a || z == b ensures z <= a && z <= b {assert true; assert a == a && b == b; var x := a; assert x == a && b == b; var y := b; assert x == a && y == b; 0; z := assert x + z == a && y + z == b; while x != 0 && y != 0 decreases x decreases y invariant x + z == a && y + z == b invariant 0 <= x <= a invariant 0 <= y <= b {assert (x-1) + (z+1) == a && (y-1) + (z+1) == b ==> x + z == a && y + z == b;1; x := x - assert x + (z+1) == a && (y-1) + (z+1) == b; 1; y := y - assert x + (z+1) == a && y + (z+1) == b; 1; z := z + assert x + z == a && y + z == b; }assert x + z == a && y + z == b && !(x != 0 && y != 0) ==> (z == a || z == b) && z <= a && z <= b; }
OCaml REPL
method Min'(a:nat, b:nat) returns (z:nat) ensures z == a || z == b ensures z <= a && z <= b { var x := a; var y := b;0; z := while x != 0 && y != 0 invariant x + z == a && y + z == b0 <= x <= a && 0 <= y <= b invariant {1; x := x - 1; y := y - 1; z := z + } }
9.2.8 Two Loops
0;
x := 0;
y :=
z := c;while x != a {
1;
x := x + 1
z := z +
};while y <> b {
1;
y := y + 1
z := z + }
Dafny
method TwoLoops(a:nat, b:nat, c:nat) returns (z:nat) ensures z == a + b + c {assert true; assert c == c; var x := 0; var y := 0; assert c == x + c; z := c;assert z == x + c; while x != a decreases a - x invariant 0 <= x <= a invariant z == c + x {assert z+1 == x+1 + c ==> z == x + c; 1; x := x + assert z+1 == x + c; 1; z := z + assert z == x + c; }assert z == x + c && !(x != a) ==> z == a + c; assert z == a + c + y; while y != b decreases b - y invariant 0 <= y <= b invariant z == a + c + y {assert z+1 == a + c + y+1 ==> z == a + c + y; 1; y := y + assert z+1 == a + c + y; 1; z := z + assert z == a + c + y; }assert z == a + c + y && !(y != b) ==> z == a + b + c; }
9.2.9 Power Series
1 + 2 + 2^2 + ... + 2^m = 2^(m+1) - 1 |
0;
x := 1;
y := 1;
z := while x != m {
2 * z;
z :=
y := y + z;1;
x := x + }
Here is the annotated imperative implemenation of Power
Dafny
function pow2(n : nat) : nat { match n case 0 => 1 case _ => 2 * (pow2 (n-1)) }method power(n:nat) returns (y:nat) ensures y == pow2(n+1)-1 {assert true; assert 3 == 3; assert 1 + 2 * 1 == pow2(0+1+1)-1; var x := 0; assert 1 + 2 * 1 == pow2(x+1+1)-1; 1; y := assert y + 2 * 1 == pow2(x+2)-1; var z := 1; assert y + 2 * z == pow2(x+2)-1; while x != n decreases n - x invariant 0 <= x <= n invariant z == pow2(x) invariant y == pow2(x+1)-1 {assert y + 2 * z == pow2(x+2)-1; 2 * z; z := assert y+z == pow2(x+2)-1; y := y + z;assert y == pow2(x+1+1)-1; 1; x := x + assert y == pow2(x+1)-1; }assert !(x != n) && z == pow2(x) && y == pow2(x+1)-1 ==> 1)-1; y == pow2(n+ }
9.2.10 Fibonacci Again
Dafny
function Fib(n: nat): nat { if n < 2 then n else Fib(n - 2) + Fib(n - 1) } method ComputeFib(n: nat) returns (x: nat) ensures x == Fib(n) {0; x := var i, y := 0, 1; assert 0 <= i <= n && x == Fib(i) && y == Fib(i + 1); while i != n decreases n - i invariant 0 <= i <= n invariant x == Fib(i) && y == Fib(i + 1) {assert i != n && 0 <= i <= n && x == Fib(i) && y == Fib(i + 1); assert 0 <= i+1 <= n && y == Fib(i + 1) && x + y == Fib(i + 2); x, y := y, x + y;assert 0 <= i+1 <= n && x == Fib(i + 1) && y == Fib(i + 2); 1; i := i + assert 0 <= i <= n && x == Fib(i) && y == Fib(i + 1); }assert i == n && x == Fib(i) && y == Fib(i + 1) ==> x == Fib(n); }
9.2.11 Lists Revisited
Dafny
datatype List<T> = Nil | Cons(head: T, tail: List<T>) function Length<T>(ls: List<T>): nat { match ls case Nil => 0 case Cons(_, ls') => 1 + Length(ls') } method LengthLoop<T>(ls: List<T>) returns (sum: nat) ensures sum == Length(ls) {var l := ls; 0; sum := while l.Cons? invariant sum + Length(l) == Length(ls) to be done, // Invariant: what additional work needs to produce the final answer // decreases Length(l) function to show termination // Use the Length {1; sum := sum + l := l.tail; } } predicate Member<T(==)>(x: T, ls: List<T>) { match ls case Nil => false case Cons(y, ls') => y == x || Member(x, ls') } method MemberLoop<T(==)>(x: T, ls: List<T>) returns (b: bool) ensures b == Member(x, ls) {var l := ls; while l.Cons? invariant Member(x, l) == Member(x, ls) invariant for the Memeber is simpler, because the // The to // transformed current problem instance is exactly equivalent // the original instance. decreases Length(l) {if l.head == x { true; return else { } l := l.tail; } }false; return } function At<T>(xs:List<T>, i:nat):(r:T) requires i < Length(xs) ensures Member(r,xs) {if i == 0 then xs.head else At(xs.tail, i-1) } method AtLoop1<T>(ls:List<T>, n:nat) returns (r:T) requires n < Length(ls) ensures r == At(ls,n) {var l := ls; var i := n; while true invariant 0 <= i <= n invariant Length(l) == Length(ls) - (n - i) invariant At(l,i) == At(ls,n) decreases i {if i == 0 { return l.head;else{ }1; i := i - l := l.tail; } } } method AtLoop2<T>(ls:List<T>, n:nat) returns (r:T) requires n < Length(ls) ensures r == At(ls,n) {var l := ls; var i := n; while i > 0 invariant 0 <= i <= n invariant Length(l) == Length(ls) - (n - i) invariant At(l,i) == At(ls,n) decreases i {1; i := i - l := l.tail; } return l.head; }