// extrinsec verification of functional programs = proofs live outside of the functions // more control // intrinsec verification (next week) = proofs live within the function // better automation function Increment(n : int) : int { n + 1 } lemma IncrementSpec() ensures forall n : int :: Increment(n) > n { forall n : int | true ensures Increment(n) > n { calc { Increment(n); == n + 1; > n; } } } lemma IncrementProp1() ensures forall n : int :: n % 2 == 0 ==> Increment(n) % 2 == 1 { forall n : int | n % 2 == 0 ensures Increment(n) % 2 == 1 { calc { Increment(n) % 2; == (n + 1) % 2; == (n % 2 + 1 % 2) % 2; == (0 + 1) % 2; == 0 + 1; == 1; } } } // opaque functions: prover does not know how they are defined opaque function IncrementOpaque(n : int) : int { n + 1 } lemma IncrementOpaqueSpec() ensures forall n : int :: IncrementOpaque(n) > n { forall n : int | true ensures IncrementOpaque(n) > n { calc { IncrementOpaque(n); == { reveal IncrementOpaque(); // need to use "reveal" to let system know its definition // helps keep the proof context small } n + 1; > n; } } } opaque function AbsoluteValue(i : int) : int { if i < 0 then -i else i } // Pattern: proof structure follows definition lemma AbsoluteValueNonnegative(i : int) ensures AbsoluteValue(i) >= 0 { if i < 0 { calc { AbsoluteValue(i); == { reveal AbsoluteValue(); } -i; >= 0; } } else { calc { AbsoluteValue(i); == { reveal AbsoluteValue(); } i; >= 0; } } } datatype TicTacToeCell = EmptyCell | X | O opaque function count(cell : TicTacToeCell) : int { match cell case EmptyCell => 0 case X => 1 case O => 1 } // Pattern: proof structure follows definition lemma countAtMost1(cell : TicTacToeCell) ensures count(cell) <= 1 { match cell case EmptyCell => assert count(cell) == 0 <= 1 by { reveal count(); } case X => assert count(cell) == 1 <= 1 by { reveal count(); } case O => assert count(cell) == 1 <= 1 by { reveal count(); } } datatype List = Empty | Cons(head : T, tail : List) function Length(l : List) : nat { if l.Empty? then 0 else 1 + Length(l.tail) } function Append(l1 : List, l2 : List) : List { if l1.Empty? then l2 else Cons(l1.head, Append(l1.tail, l2)) } // Pattern: // Recursive function? Inductive proof (typically)! lemma {:induction false} AppendLength(l1 : List, l2 : List) // {:induction false} tells system not to automatically bring induction hypothesis // into scope (allows us more control, verification performance, better understanding) ensures Length(Append(l1, l2)) == Length(l1) + Length(l2) { match l1 { case Empty => case Cons(head, tail) => calc { Length(Append(l1, l2)); == Length(Append(Cons(head, tail), l2)); == Length(Cons(head, Append(tail, l2))); == 1 + Length(Append(tail, l2)); == { AppendLength(tail, l2); // bring induction hypothesis into scope: // Length(Append(tail, l2)) == Length(tail) + Length(l2) // allowed because tail is "smaller" than l1 } 1 + Length(tail) + Length(l2); == Length(Cons(head, tail)) + Length(l2); == Length(l1) + Length(l2); } } } lemma {:induction false} AppendAssoc(l1: List, l2: List, l3: List) ensures Append(Append(l1, l2), l3) == Append(l1, Append(l2, l3)) { match l1 { case Empty => case Cons(head, tail) => calc { Append(Append(l1, l2), l3); == Append(Append(Cons(head, tail), l2), l3); == Append(Cons(head, Append(tail, l2)), l3); == Cons(head, Append(Append(tail, l2), l3)); == { AppendAssoc(tail, l2, l3); // recursive lemma call = bring induction hypothesis // into scope } Cons(head, Append(tail, Append(l2, l3))); == Append(Cons(head, tail), Append(l2, l3)); == Append(l1, Append(l2, l3)); } } } // consider an efficient tail-recursive version of Length: function LengthAux(l : List, acc : nat) : nat { match l { case Empty => acc case Cons(head, tail) => LengthAux(tail, acc + 1) } } function Length'(l : List) : nat { LengthAux(l, 0) } // Proving this lemma by induction fails. // Induction hypothesis not strong enough. lemma {:induction false} LengthSameFail(l : List) ensures Length(l) == Length'(l) { assume false; } // When this happens (often), you need to generalize what you are proving: lemma {:induction false} LengthSameAux(l : List, acc : nat) ensures Length(l) + acc == LengthAux(l, acc) { if l.Empty? { } else { calc { LengthAux(l, acc); == LengthAux(l.tail, 1 + acc); == { LengthSameAux(l.tail, 1 + acc); } Length(l.tail) + 1 + acc; == Length(Cons(l.head, l.tail)) + acc; } } } lemma {:induction false} LengthSame(l : List) ensures Length(l) == Length'(l) { LengthSameAux(l, 0); }