7 Inductive Types
7.1 Linked Lists
Here’s a simple container data structure: linked lists of natural numbers.
datatype NatList =
NNil nat, tail: NatList) | NCons(head:
The datatype command declares an inductive type by specifying its constructor functions—the functions used to build values of that type. The Cons constructor is recursive, since it refers to the same type in its definition. Its name comes from “construct,” a term rooted in the history of functional programming, where the Lisp language first introduced this convention.
Here is how to construct a list of the first n n positive integers, arranged in decreasing order.
Dafny
function FirstN(n: nat): NatList { if n == 0 then NNil else NCons(n, FirstN(n-1)) }method TestFirstN(){ assert FirstN(0) == NNil; assert FirstN(3) == NCons(3,NCons(2,NCons(1,NNil))); }
We can also pull apart values of inductive types in recursive functions. Let’s add up all the numbers in a list.
We can also unpack values of inductive types within recursive functions. For example, let us write a function that sums all the numbers in a list.
Dafny
function Sum(ls: NatList): nat { if ls == NNil then 0 else ls.head + Sum(ls.tail) }
These definitions lend themselves naturally to lemmas proved by induction. We will now make full use of Dafny’s capability to perform automatic induction.
Dafny
lemma Sum_FirstN(n: nat) ensures Sum(FirstN(n)) == n * (n + 1) / 2 { }
Dafny
lemma{:induction false} Sum_FirstN'(n: nat) ensures Sum(FirstN(n)) == n * (n + 1) / 2 {if n == 0{} //base case. Trivially holds. else{ calc{ Sum(FirstN(n));-1)); n + Sum(FirstN(n-1);} {Sum_FirstN'(n-1 )* n / 2; n + (n 1) / 2; n * (n + } } }
Dafny
lemma{:induction false} Sum_FirstN''(n: nat) ensures Sum(FirstN(n)) == n * (n + 1) / 2 {if n == 0{} else{ assert Sum(FirstN(n)) == n + Sum(FirstN(n-1)); assert Sum(FirstN(n-1)) == (n -1 )* n / 2 -1);} by {Sum_FirstN'(nassert Sum(FirstN(n)) == n * (n + 1) / 2; } }
The concepts underlying inductive datatypes originate from the long-standing tradition of functional programming, as exemplified by languages such as Haskell. Dafny likewise provides a pattern-matching notation, which aligns with this tradition and is often the preferred style.
Dafny
function Sum'(ls: NatList): nat { match ls case NNil => 0 case NCons(n, ls') => n + Sum'(ls') }lemma Sum_Sum'(ls: NatList) ensures Sum(ls) == Sum'(ls) { } // turn off the automatic inductionlemma {:induction false} Sum_Sum''(ls: NatList) ensures Sum(ls) == Sum'(ls) {if ls == NNil{} else{ calc{ Sum(ls); ls.head + Sum(ls.tail); {Sum_Sum''(ls.tail);} ls.head + Sum'(ls.tail); } } } lemma Sum_Sum'_FirstN(n: nat) ensures Sum'(FirstN(n)) == n * (n + 1) / 2 { }
7.2 Generic Lists
Notice that the definition of lists is not inherently specialized to numerical data. It is common to define generic container types that can store values of arbitrary type. Below is a generic version, parameterized by a type variable T.
datatype List<T> =
Nil List<T>) | Cons(head: T, tail:
We now turn our attention to several classic operations on lists.
Dafny
function Length<T>(xs: List<T>): nat { match xs case Nil => 0 case Cons(_, tail) => 1 + Length(tail) }method TestLength(){ var l:List<int> := Cons(10,Cons(20,Nil)); assert Length(l) == 2; }
Dafny
function Snoc<T>(xs: List<T>, y: T): List<T> { match xs case Nil => Cons(y, Nil) case Cons(x, tail) => Cons(x, Snoc(tail, y)) }method TestSnoc(){ var l:List<int> := Cons(10,Cons(20,Nil)); assert Snoc(l,30) == Cons(10,Cons(20,Cons(30,Nil))); }
Dafny
lemma LengthSnoc<T>(xs: List<T>, y: T) ensures Length(Snoc(xs, y)) == Length(xs) + 1 { }
Dafny
lemma{:induction false} LengthSnoc'<T>(xs: List<T>, y: T) ensures Length(Snoc(xs, y)) == Length(xs) + 1 {if xs == Nil{} else{ calc{ Length(Snoc(xs, y)); Length(Cons(xs.head, Snoc(xs.tail, y)));1; Length(Snoc(xs.tail, y)) + {LengthSnoc'(xs.tail,y);}1; Length(xs) + } } }
Dafny
function Append'<T>(xs: List<T>, ys: List<T>): List<T> { match xs case Nil => ys case Cons(x, tail) => Cons(x, Append'(tail, ys)) }
Dafny
lemma LengthAppend<T>(xs: List<T>, ys: List<T>) ensures Length(Append'(xs, ys)) == Length(xs) + Length(ys) { }
Dafny
function Append<T>(xs: List<T>, ys: List<T>): List<T> ensures Length(Append(xs, ys)) == Length(xs) + Length(ys) {match xs case Nil => ys case Cons(x, tail) => Cons(x, Append(tail, ys)) }method TestAppend(){ var l1:= Cons(1,Cons(2,Nil)); var l2:= Cons(3,Cons(4,Nil)); assert Append(l1,l2) == Cons(1,Cons(2,Cons(3,Cons(4,Nil)))); }
In Dafny, methods become opaque once they are completed, meaning that only the properties specified in their contracts are available for reasoning. In contrast, function definitions remain transparent, allowing their full definitions to be visible. The advantage of embedding properties directly in function definitions is that Dafny automatically “remembers” them at each call site, eliminating the need to invoke lemmas manually. The drawback is that the definition can become cluttered if numerous properties must be included.
Dafny
lemma AppendNil<T>(xs: List<T>) ensures Append(xs, Nil) == xs { } lemma AppendAssociative<T>(xs: List<T>, ys: List<T>, zs:List<T>) ensures Append(Append(xs, ys), zs) == Append(xs, Append(ys, zs)) { } lemma SnocAppend<T>(xs: List<T>, y: T) ensures Snoc(xs, y) == Append(xs, Cons(y, Nil)) { }
Dafny
lemma{:induction false} List<T>, ys: List<T>, zs:List<T>) AppendAssociative'<T>(xs: ensures Append(Append(xs, ys), zs) == Append(xs, Append(ys, zs)) {match xs case Nil => case: // Base // Append(Append(Nil, ys), zs) == Append(Nil, Append(ys, zs)) // Left: Append(Nil, ys) = ys, so Append(ys, zs) // Right: Append(Nil, Append(ys, zs)) = Append(ys, zs) // So both sides equal.case Cons(h, t) => calc{ Append(Append(Cons(h, t), ys), zs); Append(Cons(h, Append(t, ys)), zs); {AppendAssociative'(t, ys, zs);} Cons(h, Append(t, Append(ys, zs))); Append(Cons(h,t), Append(ys, zs)); Append(xs, Append(ys, zs)); } }
Dafny
lemma{:induction false} List<T>, ys: List<T>, zs:List<T>) AppendAssociative''<T>(xs: ensures Append(Append(xs, ys), zs) == Append(xs, Append(ys, zs)) {if xs == Nil{} else{ calc{ Append(Append(xs, ys), zs); {AppendAssociative''(xs.tail, ys, zs);} Append(xs, Append(ys, zs)); } }}
Dafny
lemma SnocAppend<T>(xs: List<T>, y: T) ensures Snoc(xs, y) == Append(xs, Cons(y, Nil)) { }
Dafny
function Take<T>(xs: List<T>, n: nat): List<T> requires n <= Length(xs) {if n == 0 then Nil else Cons(xs.head, Take(xs.tail, n - 1)) }method testTake(){ var t := Take(Cons(1,Cons(2,Cons(3,Cons(4,Nil)))),2); assert t == Cons(1,Cons(2,Nil)); } function Drop<T>(xs: List<T>, n: nat): List<T> requires n <= Length(xs) {if n == 0 then xs else Drop(xs.tail, n - 1) }method testDrop(){ var t := Drop(Cons(1,Cons(2,Cons(3,Cons(4,Nil)))),2); assert t == Cons(3,Cons(4,Nil)); } lemma AppendTakeDrop<T>(xs: List<T>, n: nat) requires n <= Length(xs) ensures Append(Take(xs, n), Drop(xs, n)) == xs { } lemma TakeDropAppend<T>(xs: List<T>, ys: List<T>) ensures Take(Append(xs, ys), Length(xs)) == xs ensures Drop(Append(xs, ys), Length(xs)) == ys { }
Dafny
function At<T>(xs: List<T>, i: nat): T requires i < Length(xs) {if i == 0 then xs.head else At(xs.tail, i - 1) }method testAt(){ var t := At(Cons(1,Cons(2,Cons(3,Cons(4,Nil)))),2); assert t == 3; } lemma AtDropHead<T>(xs: List<T>, i: nat) requires i < Length(xs) ensures Drop(xs, i).Cons? && At(xs, i) == Drop(xs, i).head { }
Dafny
lemma AtAppend<T>(xs: List<T>, ys: List<T>, i: nat) requires i < Length(Append(xs, ys)) ensures At(Append(xs, ys), i) if i < Length(xs) then == At(xs, i)else At(ys, i - Length(xs)) { }
How can we determine the position of an element in a list? What should we do if the element is not present?
Dafny
function Find<T(==)>(xs: List<T>, y: T): nat ensures Find(xs, y) <= Length(xs) {match xs case Nil => 0 case Cons(x, tail) => if x == y then 0 else 1 + Find(tail, y) }method testFind(){ assert Find(Nil,0) == 0; assert Find(Nil,5) == 0; var t := Find(Cons(1,Cons(2,Cons(3,Cons(4,Nil)))),2); assert t == 1; if not found, return list length //assert Find(Cons(1,Cons(2,Cons(3,Cons(4,Nil)))),5) == 4; }
The answer to the question above is that, if the element is not found, we return the length of the list.
Dafny
lemma AtFind<T>(xs: List<T>, y: T) ensures Find(xs, y) == Length(xs) || At(xs, Find(xs, y)) == y { } lemma BeforeFind<T>(xs: List<T>, y: T, i: nat) ensures i < Find(xs, y) ==> At(xs, i) != y { } lemma FindAppend<T>(xs: List<T>, ys: List<T>, y: T) ensures Find(xs, y) == Length(xs) || Find(Append(xs, ys), y) == Find(xs, y) { } lemma FindDrop<T>(xs: List<T>, y: T, i: nat) ensures i <= Find(xs, y) ==> Find(xs, y) == Find(Drop(xs, i), y) + i { } function SlowReverse<T>(xs: List<T>): List<T> { match xs case Nil => Nil case Cons(x, tail) => Snoc(SlowReverse(tail), x) }
Dafny
lemma LengthSlowReverse<T>(xs: List<T>) ensures Length(SlowReverse(xs)) == Length(xs) {match xs case Nil => case Cons(x, tail) => LengthSnoc(SlowReverse(tail), x); }
An asymptotically efficient version of reverse is included in most standard libraries of functional programming languages.
Dafny
function ReverseAux<T>(xs: List<T>, acc: List<T>): List<T> {match xs case Nil => acc case Cons(x, tail) => ReverseAux(tail, Cons(x, acc)) } function Reverse<T>(xs: List<T>): List<T> { ReverseAux(xs, Nil) } lemma ReverseAuxSlowCorrect<T>(xs: List<T>, acc: List<T>) ensures ReverseAux(xs, acc) == Append(SlowReverse(xs), acc) {match xs case Nil => case Cons(x, tail) => calc { Append(SlowReverse(xs), acc); Append(Snoc(SlowReverse(tail), x), acc); { SnocAppend(SlowReverse(tail), x); } Append(Append(SlowReverse(tail), Cons(x, Nil)), acc); { AppendAssociative(SlowReverse(tail), Cons(x, Nil), acc); } Append(SlowReverse(tail), Append(Cons(x, Nil), acc));assert Append(Cons(x, Nil), acc) == Cons(x, acc); } { Append(SlowReverse(tail), Cons(x, acc)); { ReverseAuxSlowCorrect(tail, Cons(x, acc)); } ReverseAux(tail, Cons(x, acc)); ReverseAux(xs, acc); } } lemma ReverseCorrect<T>(xs: List<T>) ensures Reverse(xs) == SlowReverse(xs) {calc { Reverse(xs); ReverseAux(xs, Nil); { ReverseAuxSlowCorrect(xs, Nil); } Append(SlowReverse(xs), Nil); { AppendNil(SlowReverse(xs)); } SlowReverse(xs); } } lemma ReverseAuxCorrect<T>(xs: List<T>, acc: List<T>) ensures ReverseAux(xs, acc) == Append(Reverse(xs), acc) { ReverseCorrect(xs); ReverseAuxSlowCorrect(xs, acc); } lemma LengthReverse<T>(xs: List<T>) ensures Length(Reverse(xs)) == Length(xs) { ReverseCorrect(xs); //Reverse(xs) == SlowReverse(xs) LengthSlowReverse(xs); // Length(SlowReverse(xs)) == Length(xs) } lemma ReverseAuxAppend<T>(xs: List<T>, ys: List<T>, acc: List<T>) ensures ReverseAux(Append(xs, ys), acc) == Append(Reverse(ys), ReverseAux(xs, acc)) {match xs case Nil => ReverseAuxCorrect(ys, acc); case Cons(_, _) => }
Dafny
list (Tail recursive) // Length of a function LengthTailCall<T>(xs:List<T>, acc:nat):(l:nat) ensures LengthTailCall(xs,acc) == Length(xs) + acc {match xs { case Nil => acc case Cons(_, tail) => LengthTailCall(tail, acc+1) } }
Dafny
lemma LengthCorrect<T>(xs:List<T>) ensures LengthTailCall(xs,0) == Length(xs) {}
Dafny
list (imperative) // Length of a method LengthImp<T>(xs:List<T>) returns (l:nat) ensures Length(xs) == l //postcondition {var lst := xs; var len := 0; while lst != Nil decreases Length(lst) invariant len + Length(lst) == Length(xs) {1; len := len + lst := lst.tail; } return len; }
Dafny
// Insert an item at indexfunction insert<T>(xs:List<T>,v:T, index:nat):(l:List<T>) requires index <= Length(xs) ensures Length(xs) + 1 == Length(l) ensures At(l,index) == v type parameter T(==) is not needed // Equality-supporting for ghost context // {if index == 0 then Cons(v,xs) else Cons(xs.head, insert(xs.tail, v, index - 1)) }
Dafny
// Check membershipfunction contains<T(==)>(xs:List<T>, v:T):bool{ match xs { case Nil => false case Cons(h,t) => v == h || contains(t,v) } }
Dafny
list to q sequence // Convert a function List2Seq<T>(xs:List<T>):(s:seq<T>) ensures Length(xs) == |s| ensures forall i:nat:: i < Length(xs) ==> At(xs,i) == s[i] {match xs{ case Nil => [] case Cons(h,t)=>[h] + List2Seq(t) } }
7.3 Natural Numbers
OK, now for something completely different! Let’s do some number theory, explaining the natural numbers and their key operations from first principles.
datatype Unary =
Zero pred: Unary) | Suc(
That’s Suc for successor and pred for predecessor. These are the Peano numbers: every natural is either zero or the successor of another natural.
Dafny
function UnaryToNat(x: Unary): nat { match x case Zero => 0 case Suc(x') => 1 + UnaryToNat(x') }
Dafny
function NatToUnary(n: nat): Unary { if n == 0 then Zero else Suc(NatToUnary(n-1)) } lemma NatUnaryCorrespondence(n: nat, x: Unary) ensures UnaryToNat(NatToUnary(n)) == n ensures NatToUnary(UnaryToNat(x)) == x { } predicate Less(x: Unary, y: Unary) { pred, y.pred)) y != Zero && (x.Suc? ==> Less(x. } lemma LessCorrect(x: Unary, y: Unary) ensures Less(x, y) <==> UnaryToNat(x) < UnaryToNat(y) { } lemma LessTransitive(x: Unary, y: Unary, z: Unary) requires Less(x, y) && Less(y, z) ensures Less(x, z) { }
Dafny
lemma{:induction false} LessTransitive2(x: Unary, y: Unary, z: Unary) requires Less(x, y) && Less(y, z) ensures Less(x, z) {match x case Zero => case Succ(x') => match y case Succ(y') => and Less(x', y') // y == Succ(y') and // Also, Less(y, z) => y == Succ(y') and Less(y', z') // z == Succ(z') match z case Succ(z') => // x == Succ(x'), y == Succ(y'), z == Succ(z')and Less(y', z') // Less(x', y') // Apply induction on x', y', z' LessTransitive(x', y', z');assert Less(x', z'); //assert Less(Succ(x'), Succ(z')); //assert Less(x, z); // }
If we change the explicit requires to an implication, it requires a bit of extra work because Less(x, y) && Less(y, z) is no longer available as an assumption.
Dafny
lemma LessTransitive3(x: Unary, y: Unary, z: Unary) ensures (Less(x, y) && Less(y, z)) ==> Less(x, z) {if Less(x, y) && Less(y, z) { match x case Zero => // Zero is less than any non-zero y,and y < z implies z != Zero // assert y != Zero; // assert z != Zero; // assert Less(Zero, z); // case Succ(x') => and Less(x', y') // Since Less(x, y), y must be Succ(y') match y case Succ(y') => assert x == Succ(x') && y == Succ(y'); //assert Less(x', y'); //and Less(y', z') // Also, Less(y, z) implies z is Succ(z') match z case Succ(z') => assert y == Succ(y') && z == Succ(z'); //assert Less(y', z'); // // Apply inductioncalc { Less(x', y'); Less(y', z'); {LessTransitive3(x',y',z');} Less(x', z'); // by induction Less(x, z); }assert Less(x, z); // } }
Dafny
function Add(x: Unary, y: Unary): Unary { match y case Zero => x case Suc(y') => Suc(Add(x, y')) } lemma AddCorrect(x: Unary, y: Unary) ensures UnaryToNat(Add(x, y)) == UnaryToNat(x) + UnaryToNat(y) { } lemma SucAdd(x: Unary, y: Unary) ensures Suc(Add(x, y)) == Add(Suc(x), y) { } lemma AddZero(x: Unary) ensures Add(Zero, x) == x { } lemma LessAdd(x:Unary) ensures Less(x,Add(Suc(Zero),x)) {} function Sub(x: Unary, y: Unary): Unary requires !Less(x, y) {match y case Zero => x case Suc(y') => Sub(x.pred, y') } lemma SubCorrect(x: Unary, y: Unary) requires !Less(x, y) ensures UnaryToNat(Sub(x, y)) == UnaryToNat(x) - UnaryToNat(y) { } function Mul(x: Unary, y: Unary): Unary { match x case Zero => Zero case Suc(x') => Add(Mul(x', y), y) } lemma MulCorrect(x: Unary, y: Unary) ensures UnaryToNat(Mul(x, y)) == UnaryToNat(x) * UnaryToNat(y) {match x case Zero => case Suc(x') => calc { UnaryToNat(Mul(x, y)); == UnaryToNat(Add(Mul(x', y), y)); == { AddCorrect(Mul(x', y), y); } UnaryToNat(Mul(x', y)) + UnaryToNat(y); } }
7.4 BST
Let’s finish up with another classic container structure,
datatype BST<V> =
Leafint, value: V, left: BST<V>, right: BST<V>) | Node(key:
datatype Option<T> =
None
Some(value: T) |
Dafny
function Lookup<V>(t: BST<V>, k: int): Option<V> {match t case Leaf => None case Node(k', v, l, r) => if k == k' then Some(v) else if k < k' then Lookup(l, k)else Lookup(r, k) } function Insert<V>(t: BST<V>, k: int, v: V): BST<V> {match t case Leaf => Node(k, v, Leaf, Leaf) case Node(k', v', l, r) => if k == k' then Node(k, v, l, r)else if k < k' then Node(k', v', Insert(l, k, v), r)else Node(k', v', l, Insert(r, k, v)) }
Dafny
lemma Lookup_Insert<V>(t: BST<V>, k: int, v: V, k': int) ensures Lookup(Insert(t, k, v), k') == if k' == k then Some(v) else Lookup(t, k') { }
Let’s also reason about the ranges of values that appear in BSTs. A range has optional lower and upper bounds, pushed into pairs like so.
type range = (Option<int>, Option<int>)
Dafny
predicate InRange(n: int, r: range) { 0.None? || r.0.value < n) (r.1.None? || n < r.1.value) && (r. }
Dafny
predicate WellFormedBST<V>(t: BST<V>, rng: range) { match t case Leaf => true case Node(k, _, l, r) => InRange(k, rng)0, Some(k))) && WellFormedBST(l, (rng.Some(k), rng.1)) && WellFormedBST(r, ( } lemma LookupRange<V>(t: BST<V>, k: int, rng: range) requires WellFormedBST(t, rng) requires InRange(k, rng) ensures Lookup(t, k).Some? ==> InRange(k, rng) '?' syntax for checking if a value was // Note the above with a certain constructor. // formed { }
Dafny
lemma InsertWellFormed<V>(t: BST<V>, rng: range, k : int, v: V) requires WellFormedBST(t, rng) requires InRange(k, rng) ensures WellFormedBST(Insert(t, k, v), rng) {match t case Leaf => case Node(k', v', l, r) => if k == k' { else if k < k' { } 0, Some(k')), k, v); InsertWellFormed(l, (rng.else { } Some(k'), rng.1), k, v); InsertWellFormed(r, ( } }
7.5 Higher-order Functions
Dafny
function Map<A, B>(f: A -> B, ls: List<A>): List<B> { match ls case Nil => Nil case Cons(x, ls') => Cons(f(x), Map(f, ls')) } lemma LengthMap<A, B>(f: A -> B, ls: List<A>) ensures Length(Map(f, ls)) == Length(ls) { } lemma MapAppend<A, B>(f: A -> B, ls1: List<A>, ls2: List<A>) ensures Map(f, Append(ls1, ls2)) == Map(f, ls1), Map(f, ls2)) Append( { }
We can work with functions in an event more first-class way.
Dafny
function Identity<A>(): A -> A { x => x } lemma MapIdentity<A>(ls: List<A>) ensures Map(Identity(), ls) == ls { } function Compose<A, B, C>(f: A -> B, g: B -> C): A -> C { x => g(f(x)) } lemma MapCompose<A, B, C>(f: A -> B, g: B -> C, ls: List<A>) ensures Map(Compose(f, g), ls) == Map(g, Map(f, ls)) { }
Dafny
function Foldl<A, B>(f : (A, B) -> B, acc: B, ls: List<A>): B { match ls case Nil => acc case Cons(x, ls') => Foldl(f, f(x, acc), ls') }
Dafny
lemma Foldl_example() ensures Foldl((n, m) => n + m, 0, 1, Cons(2, Cons(3, Nil)))) == Cons(3 + 2 + 1 + 0 { }
Dafny
lemma FoldlReverseAux<A>(ls: List<A>, acc: List<A>) ensures Foldl((h, t) => Cons(h, t), acc, ls) == ReverseAux(ls, acc) { } lemma FoldlReverse<A>(ls: List<A>) ensures Foldl((h, t) => Cons(h, t), Nil, ls) == Reverse(ls) { FoldlReverseAux(ls, Nil); }
Dafny
lemma FoldlMap<A, B, C>(f: A -> B, g : (B, C) -> C, List<A>) acc: C, ls: ensures Foldl(g, acc, Map(f, ls)) == Foldl((x, a) => g(f(x), a), acc, ls) {match ls case Nil => case Cons(x, ls') => calc { Map(f, ls)); Foldl(g, acc, Map(f, Cons(x, ls'))); Foldl(g, acc, Map(f, ls'))); Foldl(g, acc, Cons(f(x), Map(f, ls')); Foldl(g, g(f(x), acc), { FoldlMap(f, g, g(f(x), acc), ls'); } Foldl((x, a) => g(f(x), a), g(f(x), acc), ls'); Foldl((x, a) => g(f(x), a), acc, ls); } // Or just the following line suffices: // FoldlMap(f, g, g(f(x), acc), ls'); }
7.6 Generic BST<T>
Now back to binary search trees. This time, using higher-order functions, we venture into making trees generic in their key types!
datatype Option<T> =
None
Some(value: T) |
datatype BST<K, V> =
Leaf | Node(key: K, value: V, left: BST<K, V>, right: BST<K, V>)
Dafny
function Lookup<K(==), V>(lt: (K, K) -> bool, Option<V> t: BST<K, V>, k: K): {match t case Leaf => None case Node(k', v, l, r) => if k == k' then Some(v) else if lt(k, k') then Lookup(lt, l, k)else Lookup(lt, r, k) } function Insert<K(==), V>(lt: (K, K) -> bool, t: BST<K, V>, k: K, v: V): BST<K, V> {match t case Leaf => Node(k, v, Leaf, Leaf) case Node(k', v', l, r) => if k == k' then Node(k, v, l, r)else if lt(k, k') then Node(k', v', Insert(lt, l, k, v), r)else Node(k', v', l, Insert(lt, r, k, v)) }
Dafny
lemma Lookup_Insert<K, V>(lt: (K, K) -> bool, t: BST<K, V>, k: K, v: V, k': K)ensures Lookup(lt, Insert(lt, t, k, v), k') == if k' == k then Some(v) else Lookup(lt, t, k') { }
Dafny
function Mirror<K, V>(t: BST<K, V>): BST<K, V> { match t case Leaf => Leaf case Node(k, v, l, r) => Node(k, v, Mirror(r), Mirror(l)) }
Dafny
function Flip<A, B, C>(f: (A, B) -> C): (B, A) -> C { (b, a) => f(a, b) } function Less(x: int, y: int): bool { x < y } method testFlip(){ assert Less(10, 20) == Flip(Less)(20,10); }
Dafny
lemma LookupMirror<K, V>(lt: (K, K) -> bool, t: BST<K, V>, k: K) requires forall a, b : K :: lt(a, b) == (!lt(b, a) && a != b) ensures Lookup(lt, t, k) == Lookup(Flip(lt), Mirror(t), k) { // This proof actually goes through automatically, }
Here we show a calc that helped us realize what precondition to add.
Dafny
lemma LookupMirror'<K, V>(lt: (K, K) -> bool, t: BST<K, V>, k: K) requires forall a, b : K :: lt(a, b) == (!lt(b, a) && a != b) ensures Lookup(lt, t, k) == Lookup(Flip(lt), Mirror(t), k) {match t case Leaf => case Node(k', v', l, r) => if k == k' { else if lt(k, k') { } calc { Lookup(lt, t, k); Lookup(lt, Node(k', v', l, r), k); Lookup(lt, l, k); { LookupMirror'(lt, l, k); } Lookup(Flip(lt), Mirror(l), k);assert Flip(lt)(k', k); } Lookup(Flip(lt), Node(k', v', Mirror(r), Mirror(l)), k); { Lookup(Flip(lt), Mirror(Node(k', v', l, r)), k); Lookup(Flip(lt), Mirror(t), k); }else { } calc { Lookup(lt, t, k); Lookup(lt, Node(k', v', l, r), k); Lookup(lt, r, k); { LookupMirror(lt, r, k); } Lookup(Flip(lt), Mirror(r), k);assert Flip(lt)(k, k'); } Lookup(Flip(lt), Node(k', v', Mirror(r), Mirror(l)), k); { Lookup(Flip(lt), Mirror(Node(k', v', l, r)), k); Lookup(Flip(lt), Mirror(t), k); } } }
This property may initially seem unnecessary to state explicitly: Lookup behaves identically when given two functions that themselves behave identically. However, this property does not hold automatically for arbitrary Dafny functions. In practice, we must examine each function individually. Intuitively, while different sorting algorithms may always produce equivalent outputs, we may not want to consider the algorithms themselves as equivalent.
Dafny
lemma LookupExtensional<K, V>(lt: (K, K) -> bool, lt': (K, K) -> bool, t: BST<K, V>, k: K) requires forall a, b : K :: lt(a, b) == lt'(a, b) ensures Lookup(lt, t, k) == Lookup(lt', t, k) { }
Dafny
lemma LookupMirrorInt<V>(t: BST<int, V>, k: int) ensures Lookup((a, b) => a < b, t, k) == Lookup((b, a) => a < b, Mirror(t), k) {var lt := (a: int, b: int) => a < b; var lt' := (b: int, a: int) => a < b; LookupMirror(lt, t, k); LookupExtensional(Flip(lt), lt', Mirror(t), k); }
Dafny
function Size<K, V>(t: BST<K, V>): nat { match t case Leaf => 0 case Node(_, _, l, r) => 1 + Size(l) + Size(r) }
Dafny
function Keys<K, V>(t: BST<K, V>): List<K> { match t case Leaf => Nil case Node(k, _, l, r) => Cons(k, Append(Keys(l), Keys(r))) } lemma SizeKeys<K, V>(t: BST<K, V>) ensures Length(Keys(t)) == Size(t) { }
Dafny
predicate Member<A(==)>(x: A, xs: List<A>) { match xs case Nil => false case Cons(y, xs') => x == y || Member(x, xs') } lemma MemberAppend<A>(x: A, xs1: List<A>, xs2: List<A>) ensures Member(x, Append(xs1, xs2)) == (Member(x, xs1) || Member(x, xs2)) { }
Dafny
predicate NoDup<A(==)>(xs: List<A>) { match xs case Nil => true case Cons(x, xs') => !Member(x, xs') && NoDup(xs') }
Dafny
type range<K> = (Option<K>, Option<K>) predicate InRange<K>(lt: (K, K) -> bool, n: K, r: range<K>) { 0.None? || lt(r.0.value, n)) (r.1.None? || lt(n, r.1.value)) && (r. }
Dafny
predicate WellFormedBST<K, V>(lt: (K, K) -> bool, t: BST<K, V>, rng: range) { match t case Leaf => true case Node(k, _, l, r) => InRange(lt, k, rng)0, Some(k))) && WellFormedBST(lt, l, (rng.Some(k), rng.1)) && WellFormedBST(lt, r, ( }
Dafny
lemma RangeKeys<K, V>(lt: (K, K) -> bool, t : BST<K, V>, rng: range<K>, k: K) requires WellFormedBST(lt, t, rng) requires Member(k, Keys(t)) requires forall a, b, c :: lt(a, b) && lt(b, c) ==> lt(a, c) ensures InRange(lt, k, rng) {match t case Leaf => case Node(k', v', l, r) => MemberAppend(k, Keys(l), Keys(r)); }
Dafny
predicate Disjoint<A(==)>(ls1: List<A>, ls2: List<A>) { match ls1 case Nil => true case Cons(x, ls1') => !Member(x, ls2) && Disjoint(ls1', ls2) }
Dafny
lemma NoDupAppend<A>(ls1: List<A>, ls2: List<A>) ensures NoDup(Append(ls1, ls2)) == (Disjoint(ls1, ls2) && NoDup(ls1) && NoDup(ls2)) {match ls1 case Nil => case Cons(v, ls1') => calc { NoDup(Append(ls1, ls2)); == NoDup(Append(Cons(v, ls1'), ls2)); == NoDup(Cons(v, Append(ls1', ls2))); == !Member(v, Append(ls1', ls2)) && NoDup(Append(ls1', ls2)); == { NoDupAppend(ls1', ls2); } !Member(v, Append(ls1', ls2)) && Disjoint(ls1', ls2) && NoDup(ls1') && NoDup(ls2); == { MemberAppend(v, ls1', ls2); } !Member(v, ls1') && !Member(v, ls2) && Disjoint(ls1', ls2) && NoDup(ls1') && NoDup(ls2); } }
Dafny
lemma DisjointAppend<A>(ls1: List<A>, ls2: List<A>, ls: List<A>) ensures Disjoint(Append(ls1, ls2), ls) == (Disjoint(ls1, ls) && Disjoint(ls2, ls)) { }
Dafny
lemma BSTDisjoint<K, V>(lt: (K, K) -> bool, t1: BST<K, V>, t2: BST<K, V>,Option<K>, upper1: K, lower2: K, upper2: Option<K>) lower1: requires WellFormedBST(lt, t1, (lower1, Some(upper1))) requires WellFormedBST(lt, t2, (Some(lower2), upper2)) requires lt(upper1, lower2) || upper1 == lower2 // That last one is critical: the two trees' ranges are orderedin a particular way, overlapping in *at most one* element, // and given how we interpret range ends strictly, that implies // // *no overlap of elements*. requires forall a, b, c :: lt(a, b) && lt(b, c) ==> lt(a, c) requires forall a :: !lt(a, a) // New property: 'lt' is *irreflexive*. ensures Disjoint(Keys(t1), Keys(t2)) {match t1 case Leaf => case Node(k, v, l, r) => // Note a little *proof by contradiction* here,to show how we know a given fact doesn't hold. // if Member(k, Keys(t2)) { assert InRange(lt, k, (lower1, Some(upper1))); assert lt(k, upper1); assert InRange(lt, k, (Some(lower2), upper2)) by Some(lower2), upper2), k); } { RangeKeys(lt, t2, (assert lt(lower2, k); assert lt(k, k); // by transitivity assert false; // by antireflexivity }assert !Member(k, Keys(t2)); calc { Disjoint(Cons(k, Append(Keys(l), Keys(r))), Keys(t2)); !Member(k, Keys(t2)) && Disjoint(Append(Keys(l), Keys(r)), Keys(t2)); { DisjointAppend(Keys(l), Keys(r), Keys(t2)); } !Member(k, Keys(t2)) && Disjoint(Keys(l), Keys(t2)) && Disjoint(Keys(r), Keys(t2)); { BSTDisjoint(lt, l, t2, lower1, k, lower2, upper2); }true && Disjoint(Keys(r), Keys(t2)); !Member(k, Keys(t2)) && Some(k), upper1, lower2, upper2); } { BSTDisjoint(lt, r, t2, true && true; !Member(k, Keys(t2)) && true && true && true; true; } }
Dafny
lemma NoDupKeys<K, V>(lt: (K, K) -> bool, t: BST<K, V>, rng: range<K>)requires WellFormedBST(lt, t, rng) requires forall a :: !lt(a, a) requires forall a, b, c :: lt(a, b) && lt(b, c) ==> lt(a, c) ensures NoDup(Keys(t)) {match t case Leaf => case Node(k, v, l, r) => assert !Member(k, Keys(l)) by { if Member(k, Keys(l)) { 0, Some(k)), k); RangeKeys(lt, l, (rng. } } assert !Member(k, Keys(r)) by { if Member(k, Keys(r)) { Some(k), rng.1), k); RangeKeys(lt, r, ( } } assert !Member(k, Append(Keys(l), Keys(r))) by { MemberAppend(k, Keys(l), Keys(r)); } NoDupAppend(Keys(l), Keys(r));assert Disjoint(Keys(l), Keys(r)) by 0, k, k, rng.1); } { BSTDisjoint(lt, l, r, rng. }