6 Inductive Proofs
6.1 Proofs of functions
In this lecture, we will learn how to construct formal proofs to verify the correctness of functions.
Dafny
function TriangleNumber(n: int): int requires 0 <= n {if n == 0 then 0 else TriangleNumber(n - 1) + n }
Dafny
method ComputeTriangleNumber(n: int) returns (s: int) requires 0 <= n // omit this line; what happens? ensures s == TriangleNumber(n) {if n == 0 { 0; return else { } 1); s := ComputeTriangleNumber(n - return s + n; } }
For this method to be correct, every control path leading to the end of the method body must be shown to establish the postcondition.
Dafny
method ComputeTriangleNumber2(n: int) returns (s: int) requires 0 <= n ensures s == TriangleNumber(n) ensures s == n * (n + 1) / 2 {if n == 0 { 0; return else { } 1); s := ComputeTriangleNumber2(n - return s + n; } }
A consequence of the two postconditions
s == TriangleNumber(n)1) / 2 s == n * (n +
1) / 2 TriangleNumber(n) == n * (n +
Dafny
method ComputeTriangleNumber3(n: int) returns (s: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n == 0 { 0; return else { } 1); s := ComputeTriangleNumber3(n - return s + n; } }
Dafny
method ComputeTriangleNumber4(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n == 0 { else { } 1); ComputeTriangleNumber4(n - } }
As before, correctness of this method means that every control path leading to the end establishes the postcondition. But why would anyone want to call such a method, since it does not return anything? By calling the method, the caller learns the postcondition. This is called a lemma!
6.2 Lemma
Dafny
lemma Gauss(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n == 0 { else { } 1); Gauss(n - } }
The proof has the same structure as you may have encountered in previous classes or in high-school geometry. It is a proof by induction with the base case n == 0 and an induction step for n > 0. The induction hypothesis is obtained by calling the lemma recursively.
Lemmas in Dafny are proof artifacts, so they are ghost. All code inside a lemma must also be ghost (i.e., it cannot affect runtime behavior). Lemmas are not executable at runtime—they are erased.
In contrast, a method is executable and may have side effects. Because of this separation, a lemma generally cannot call a regular method, since that would mix ghost and non-ghost code.
Dafny
lemma Gauss'(n: int) requires 0 <= n {var v := ComputeTriangleNumber(n); to methods are disallowed // error: direct calls lemma may call other lemmas, but not methods. // A assert v == TriangleNumber(n); }
If you want this construct to be executable, you can rewrite the lemma as a method:
Dafny
method Gauss'(n: int) requires 0 <= n {var v := ComputeTriangleNumber(n); assert v == TriangleNumber(n); assert v == n * (n + 1) / 2 by { Gauss(n); } }
In practice, to reason about methods, you specify their behavior using functions or predicates. Lemmas can then call those functions (or other lemmas) to establish properties about the method’s specification. For example, we used a function
ensures s == TriangleNumber(n)
Dafny
lemma Gauss2(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n == 0 { assert TriangleNumber(n) == 0; // by definition of TriangleNumber assert n * (n + 1) / 2 == 0; // by arithmetic // Therefore:assert TriangleNumber(n) == n * (n + 1) / 2; else { } var n' := n - 1; Gauss2(n');assert TriangleNumber(n') == n' * (n' + 1) / 2; // we get this information from the postcondition of the // call Gauss(n') assert TriangleNumber(n) == TriangleNumber(n') + n; // by definition of TriangleNumber // Therefore:assert TriangleNumber(n) == n' * (n' + 1) / 2 + n; // We also have:assert n' * (n' + 1) / 2 + n == (n - 1) * n / 2 + n; assert (n - 1) * n / 2 + n == (n - 1) * n / 2 + 2 * n / 2; assert (n - 1) * n / 2 + 2 * n / 2 == (n - 1 + 2) * n / 2; assert (n - 1 + 2) * n / 2 == n * (n + 1) / 2; assert TriangleNumber(n) == n * (n + 1) / 2; } }
Sometimes, you may have to write out proofs in a lot of detail, maybe to figure out what’s missing in a proof that does not go through automatically or maybe to give the verifier enough hints about the proof structure so that it can complete the proof. When you do, long sequences of assert statements (like in Gauss2 above) are hard to read. Here are two structuring devices you can use to make proofs easier to read, assert by and calc.
Dafny
lemma Gauss3(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n == 0 { assert TriangleNumber(n) == n * (n + 1) / 2 by { assert TriangleNumber(n) == 0; // by definition of TriangleNumber assert n * (n + 1) / 2 == 0; // by arithmetic } else { } assert TriangleNumber(n - 1) == (n - 1) * n / 2 by { var n' := n - 1; Gauss3(n');assert TriangleNumber(n') == n' * (n' + 1) / 2; } calc { end of each line // Note the semi-colon at the to forget // it can be easy TriangleNumber(n); == // by definition of TriangleNumber1) + n; TriangleNumber(n - "assert by" above == // from the 1) * n / 2 + n; (n - == // arithmetic1) * n / 2 + 2 * n / 2; (n - and + == // distribute * 1) + 2) * n / 2; ((n - == // arithmetic1)) / 2; (n * (n + } } }
Dafny
lemma Gauss4(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n == 0 { // easyelse { } calc { TriangleNumber(n); == // by definition of TriangleNumber1) + n; TriangleNumber(n - 1); } // this recursive call to the lemma == { Gauss4(n - // obtains the induction hypothesis1) * n / 2 + n; (n - assert n == 2 * n / 2; } == { 1) * n / 2 + 2 * n / 2; (n - == // arithmetic1)) / 2; (n * (n + } } }
Dafny
lemma Gauss5(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 {if n != 0 { // to make things shorter, the "if" condition was and one branch omitted // negated calc { TriangleNumber(n);1) + n; TriangleNumber(n - 1); } { Gauss5(n - 1) * n / 2 + n; (n - assert n == 2 * n / 2; } { 1) * n / 2 + 2 * n / 2; (n - 1)) / 2; (n * (n + } } }
if n > 0 { Gauss(n - 1); }
Dafny
lemma Gauss6(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 { }
But where’s the fun in that? You wouldn’t really learn anything. In fact, for Project 1 you’ll be required to disable automatic induction. Here’s how to do that for a lemma:
Dafny
lemma {:induction false} Gauss7(n: int) requires 0 <= n ensures TriangleNumber(n) == n * (n + 1) / 2 { // error is reported here, saying the verifier // cannot find the proof }
Now that we understand what lemmas are, know that they can be called recursively (which, for a lemma, gives us the usual induction hypothesis), and have seen two basic proof-structuring constructs—assert by and calc—let’s start writing some proofs.
6.3 Sum of Cubes
Let’s prove that the square of the sum of the first n positive numbers equals the sum of the first n positive cubes.
13+23+33+ ... +n3=(1+2+3+... +n)2
Rather than declaring the parameter to be of type "int" and using a precondition that says the parameter is non-negative, we can use the type "nat".
Dafny
function SumCubesThrough(n: nat): nat { if n == 0 then 0 else SumCubesThrough(n - 1) + n * n * n } lemma Sums(n: nat) ensures TriangleNumber(n) * TriangleNumber(n) == SumCubesThrough(n) {if n == 0 { else { } for things with long names // For brevity, here are names var tn := TriangleNumber(n); var tn' := TriangleNumber(n - 1); var s' := SumCubesThrough(n - 1); with non-linear arithmetic can require a lot of // Working with the verifier. Sometimes, it can help to name // patience "nn" here. // subexpressions, so we introduce the name var nn := n * n; calc { tn * tn; (tn' + n) * (tn' + n);2 * tn' * n + nn; tn' * tn' + 1); } { Sums(n - 2 * tn' * n + nn; s' + 1); } { Gauss(n - 1) * nn + nn; s' + (n - s' + n * nn; SumCubesThrough(n); } } }
6.4 Fibonacci
Dafny
function Fib(n: nat): nat { if n < 2 then n else Fib(n - 2) + Fib(n - 1) }
Dafny
lemma {:induction false} FibGetsLarger(n: nat) requires n >= 5 ensures Fib(n) >= n {if n == 5 { assert Fib(5) == 5; else if n == 6 { } assert Fib(6) == 8; else { } calc { Fib(n);2) + Fib(n - 1); Fib(n - 2); FibGetsLarger(n - 1); } >= { FibGetsLarger(n - 2 + n - 1; n - assert n >= 3; } >= { n; } } }
6.5 Sum of Differences
The next two lemmas demonstrate variations in proof style.
Dafny
function Sum(s: seq<int>): int { if |s| == 0 then 0 else s[0] + Sum(s[1..]) }
Dafny
function SumOfDifferences(s: seq<int>, t: seq<int>): int requires |s| == |t| {if |s| == 0 then 0 else s[0] - t[0] + SumOfDifferences(s[1..], t[1..]) }
Dafny
lemma {:induction false} SequenceMinus(s: seq<int>, t: seq<int>) requires |s| == |t| ensures SumOfDifferences(s, t) == Sum(s) - Sum(t) {if |s| == 0 { else { } // Here are some things we know:assert SumOfDifferences(s, t) == 0] - t[0] + SumOfDifferences(s[1..], t[1..]); s[assert Sum(s) == s[0] + Sum(s[1..]); assert Sum(t) == t[0] + Sum(t[1..]); lemma recursively, we can obtain a property // By calling the 1..]) terms on the previous two lines. // that relates the Sum(_[assert SumOfDifferences(s[1..], t[1..]) == 1..]) - Sum(t[1..]) Sum(s[1..], t[1..]); by {SequenceMinus(s[ } } }
Dafny
lemma {:induction false} SequenceMinus'(s: seq<int>, t: seq<int>) requires |s| == |t| ensures SumOfDifferences(s, t) == Sum(s) - Sum(t) {if |s| == 0 { else { } calc { // Here, we start from the LHS of the proof goal SumOfDifferences(s, t); // apply the definition of `SumOfDifferences`0] - t[0] + SumOfDifferences(s[1..], t[1..]); s[ // In the line above, we have something that looks // like a smaller version of the proof goal,lemma recursively // so let's call the 1..], t[1..]); } { SequenceMinus'(s[0] - t[0] + Sum(s[1..]) - Sum(t[1..]); s[ // From the definition of `Sum(s)`0] + Sum(s[1..]) - t[0] - Sum(t[1..]); s[ Sum(s) - Sum(t); } } }
6.6 Multiplication is Commutative
Dafny
function Mult(x:nat, y:nat):nat{ if y == 0 then 0 else x + Mult(x, y-1) }
Dafny
lemma MultiComm (x:nat, y:nat) ensures Mult(x,y) == Mult(y,x) {if y==0{ else{ }calc{ Mult(x,y); ==-1); x + Mult(x, y ==-1,x); x + Mult(y } } } lemma {:induction false} MultiComm2 (x:nat, y:nat) ensures Mult(x,y) == Mult(y,x) {if x == y { else if x == 0{ }-1);} {MultiComm2(x,yelse if y==0{ }-1, y); MultiComm2(x }else{ calc{ Mult(x,y); ==-1); x + Mult(x, y ==-1);} {MultiComm2(x,y-1,x); x + Mult(y ==-1 + Mult(y-1,x-1); x + y ==-1,y-1);} {MultiComm2(x-1 + Mult(x-1,y-1); x + y ==-1,y); y + Mult(x-1, y);} == {MultiComm2(x-1); y + Mult(y,x == Mult(y,x); } } } lemma {:induction false} MultiComm3 (x:nat, y:nat) ensures Mult(x,y) == Mult(y,x) {if x == y { else if x == 0{ }-1);} {MultiComm3(x,yelse if y==0{ }-1, y); MultiComm3(x }else{ assert Mult(x,y) == x + Mult(x, y-1); assert x + Mult(x, y-1)== x + Mult(y-1,x) -1);} by {MultiComm3(x,yassert x + Mult(y-1,x) == x + Mult(y-1,x); assert x + Mult(y-1,x) == x + y-1 + Mult(y-1,x-1); assert x + y-1 + Mult(y-1,x-1) == x + y-1 + Mult(x-1,y-1) -1,y-1);} by {MultiComm3(xassert x + y-1 + Mult(x-1,y-1) == y + Mult(x-1,y); assert y + Mult(x-1,y) == y + Mult(y,x-1) -1, y);} by {MultiComm3(xassert y + Mult(y,x-1) == Mult(y,x); } }
6.7 Postage Stamps
Dafny
lemma PostageStampsExists(amount: int) requires 12 <= amount ensures exists threes, fours :: 0 <= threes 0 <= fours && 3 * threes + 4 * fours == amount &&
Dafny
lemma PostageStamps(amount: int) returns (threes: nat, fours: nat) requires 12 <= amount ensures 3 * threes + 4 * fours == amount {if amount == 12 { 0, 3; threes, fours := else { } 1); threes, fours := PostageStamps(amount - if threes == 0 { assert 3 <= fours; 3; fours := fours - 4; threes := threes + }assert 3 * threes + 4 * fours == amount - 1; 1; threes := threes - 1; fours := fours + } }
Dafny
lemma PostageStamps2(amount: int) returns (threes: nat, sevens: nat) requires 12 <= amount ensures 3 * threes + 7 * sevens == amount {if amount ==12{ 4; threes := 0; sevens := else{ }1); threes, sevens := PostageStamps2(amount - assert threes >= 2 || sevens >= 2; if threes >= 2{ 2 ; threes := threes - 1; sevens := sevens + else if sevens >= 2{ }-2; sevens := sevens 5 ; threes := threes + }assert 3 * threes + 7 * sevens == amount; } } lemma PostageStamps3(amount: int) returns (threes: nat, sevens: nat) requires 12 <= amount ensures 3 * threes + 7 * sevens == amount {if amount ==12{ 4; threes := 0; sevens := else if amount == 13{ }2; threes := 1; sevens := else if amount == 14{ }0; threes := 2; sevens := else{ }3); threes, sevens := PostageStamps3(amount - 1; threes := threes + assert 3 * threes + 7 * sevens == amount; } }
6.8 Reversing a Sequence
Dafny
function Reverse(s: seq<int>): seq<int> { if s == [] then [] else Reverse(s[1..]) + [s[0]] } lemma ReverseShuffle(s: seq<int>, x: int) ensures Reverse(s + [x]) == [x] + Reverse(s) {if s == [] { // let's write out simplifications of the various subexpressionsassert s + [x] == [x]; assert Reverse([x]) == [x]; assert Reverse(s) == []; assert [x] + Reverse(s) == [x]; else { } var s' := s + [x]; calc { Reverse(s'); // definition of Reverse1..]) + [s'[0]]; Reverse(s'[assert s'[1..] == s[1..] + [x]; } { 1..] + [x]) + [s'[0]]; Reverse(s[1..], x); } { ReverseShuffle(s[1..]) + [s'[0]]; [x] + Reverse(s[assert s'[0] == s[0]; } { 1..]) + [s[0]]; [x] + Reverse(s[ // definition of Reverse [x] + Reverse(s); } } } lemma ReverseIsItsOwnInverse(s: seq<int>) ensures Reverse(Reverse(s)) == s {if s == [] { else { } calc { Reverse(Reverse(s));1..]) + [s[0]]); Reverse(Reverse(s[1..]), s[0]); } { ReverseShuffle(Reverse(s[0]] + Reverse(Reverse(s[1..])); [s[1..]); } { ReverseIsItsOwnInverse(s[0]] + s[1..]; [s[ s; } } }
Dafny
function InInterval(i: int, p: (int, int)): bool { 0 <= i < p.1 p. }
Dafny
function InIntervalSequence(i: int, s: seq<(int, int)>): bool { if |s| == 0 then false else if InInterval(i, s[0]) then true else 1..]) InIntervalSequence(i, s[ }
Dafny
function InIntervalSequence'(i: int, s: seq<(int, int)>): bool { 0 && (InInterval(i, s[0]) || |s| != 1..])) InIntervalSequence'(i, s[ }
Dafny
lemma {:induction false} SameIntervalDefinitions(i: int, s: seq<(int, int)>) ensures InIntervalSequence(i, s) == InIntervalSequence'(i, s) { // Let's divide up the proof into three casesif |s| == 0 { assert !InIntervalSequence(i, s); assert !InIntervalSequence'(i, s); else if InInterval(i, s[0]) { } assert InIntervalSequence(i, s); assert InIntervalSequence'(i, s); else { } assert InIntervalSequence(i, s) == InIntervalSequence(i, s[1..]); assert InIntervalSequence'(i, s) == InIntervalSequence'(i, s[1..]); 1..]); SameIntervalDefinitions(i, s[ } }
Dafny
lemma {:induction false} SameIntervalDefinitions2(i: int, s: seq<(int, int)>) ensures InIntervalSequence(i, s) == InIntervalSequence'(i, s) {if |s| == 0 { assert InIntervalSequence(i, s) == InIntervalSequence'(i, s); else{ }calc{ InIntervalSequence(i, s);0]) || InIntervalSequence(i, s[1..]); InInterval(i, s[0]]) || InIntervalSequence(i, s[1..]); InIntervalSequence'(i, [s[1..]);} {SameIntervalDefinitions2(i, s[0]]) || InIntervalSequence'(i, s[1..]); InIntervalSequence'(i, [s[ InIntervalSequence'(i, s); } } } lemma {:induction false} SameIntervalDefinitions3(i: int, s: seq<(int, int)>) ensures InIntervalSequence(i, s) == InIntervalSequence'(i, s) {if |s| == 0 || InInterval(i, s[0]) { else{ }assert InIntervalSequence(i, s) == InIntervalSequence(i, s[1..]); assert InIntervalSequence'(i, s) == InIntervalSequence'(i, s[1..]); 1..]); SameIntervalDefinitions3(i, s[ } }
Dafny
function Optimize(s: seq<(int, int)>): seq<(int, int)> { if s == [] then []else if s[0].1 <= s[0].0 then 0] is empty, so let's omit it // interval s[1..]) Optimize(s[else 0]] + Optimize(s[1..]) [s[ }
Dafny
lemma {:induction false} OptimizationIsCorrect(i: int, s: seq<(int, int)>) ensures InIntervalSequence(i, Optimize(s)) == InIntervalSequence(i, s) {as in the Optimize function. // We consider the same three cases if s == [] { // easyelse if s[0].1 <= s[0].0 { } assert !InInterval(i, s[0]); 1..]); OptimizationIsCorrect(i, s[else { } // This is almost too easy.to apply the induction hypothesis, // Once we figure out how // the automated verifier does all of the heavy lifting.1..]); OptimizationIsCorrect(i, s[ } }