6.S057: 6.S057: Verified Software Engineering

Class 5

class05.dfy
dfy
// Here's a simple container data structure: linked lists of natural numbers.
// You've seen them in the reading that was just due, so let's express past a
// quick recap.
datatype NatList = NNil | NCons(head: nat, tail: NatList)
// The 'datatype' command is for declaring an *inductive type*,
// by listing out the *constructor* functions that can be used to construct values in that type.
// Note that the 'Cons' constructor is *recursive*, mentioning the same type.
// Its name stands for "construct," and as for why that's a good name for this
// constructor, it depends on the history of functional programming, where the
// Lisp language introduced this convention!

// Here is how we build a decreasing list of the first 'n' positive numbers.
function FirstN(n: nat): NatList {
  if n == 0 then NNil else NCons(n, FirstN(n-1))
}

// We can also pull apart values of inductive types in recursive functions.
// Let's add up all the numbers in a list.
function Sum(ls: NatList): nat {
  if ls == NNil then 0 else ls.head + Sum(ls.tail)
}

// These definitions are a natural fit for lemmas proved by induction.
// Now we'll let ourselves take full advantage of Dafny's automatic induction!
lemma Sum_FirstN(n: nat)
  ensures Sum(FirstN(n)) == n * (n + 1) / 2
{
}
// Cool!  Proved automatically.

// These ideas around inductive datatypes come from a long tradition of
// *functional programming* in languages like Haskell.
// Dafny also supports (and we often prefer) a *pattern-matching* notation more
// in-line with that tradition.
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)
{
}

lemma Sum_Sum'_FirstN(n: nat)
  ensures Sum'(FirstN(n)) == n * (n + 1) / 2
{
}
// Yeah, Dafny can prove that one automatically, too.
// We're often doing more explicit proofs for didactic purposes only.
// Soon enough we will get to examples tough enough that Dafny requires help!

// You probably noticed that nothing about the definition of lists is so
// specialized to numbers as data.  Indeed, it is common to implement *generic*
// container types, able to accept any kinds of data inside.  Here is a generic
// version, with a *type parameter* 'T'.  (We are still in recap content from
// the last reading.)
datatype List<T> = Nil | Cons(head: T, tail: List<T>)

// Let's now tour through some classic list operations.
// First, how many elements does a list have?
function Length<T>(xs: List<T>): nat {
  match xs
  case Nil => 0
  case Cons(_, tail) => 1 + Length(tail)
}

// Next, we can use a version of 'Cons' that adds a new element at the *end*
// of a list, instead of the start.
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))
}

// How do the last two functions interact?
lemma LengthSnoc<T>(xs: List<T>, y: T)
  ensures Length(Snoc(xs, y)) == Length(xs) + 1
{
}

// The next classic is appending two lists together.
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))
}

// It is easy to capture the interaction with 'Length'.
lemma LengthAppend<T>(xs: List<T>, ys: List<T>)
  ensures Length(Append'(xs, ys)) == Length(xs) + Length(ys)
{
}

// However, we could also build this property into the original definition as a
// postcondition!
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))
}
// While Dafny methods become *opaque* after they are finished, and thus we only
// know their properties that were recorded in their specifications, we have
// flexibility with functions, whose definitions remain *transparent* afterward.
// The benefit of baking in properties is that Dafny is automatically "reminded"
// of them each time we call the function, saving us from needing to invoke
// lemmas manually.  The downside is that a function definition can get very
// crowded, if we need to include every relevant property!

// Let's prove some more algebraic properties of 'Append'.

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))
{
}

// And some other classic list functions:

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))
}

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)
}

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
{
}
// Note that Dafny rejects the postconditions here if we use the version of
// 'Append' without a postcondition asserting the length of the resulting list!

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)
}

lemma AtDropHead<T>(xs: List<T>, i: nat)
  requires i < Length(xs)
  ensures Drop(xs, i).Cons? && At(xs, i) == Drop(xs, i).head
{
}
// Note we used a question-mark operation to confirm which list constructor was
// used to build a certain value.  Otherwise, Dafny complains that the
// postcondition is ill-formed ("may crash").

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))
{
}
          
// At what position in a list does an element appear?
// And what do we do if the element is not found?
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)
}
// Note the '(==)' annotation on the type parameter, to require it to support a
// particular operator.

// Answer to question just above: we return the list length when the element is
// not found.
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)
}

// Surprisingly, the following isn't proved automatically!
lemma LengthSlowReverse<T>(xs: List<T>)
  ensures Length(SlowReverse(xs)) == Length(xs)
{
  match xs
  case Nil =>
  case Cons(x, tail) => LengthSnoc(SlowReverse(tail), x);
}

// There is an asymptotically faster version of reverse that appears in most
// standard libraries of functional languages.
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);
  LengthSlowReverse(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(_, _) =>
}

// 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 | Suc(pred: Unary)
// 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.

// We can convert back to the built-in type.
function UnaryToNat(x: Unary): nat {
  match x
  case Zero => 0
  case Suc(x') => 1 + UnaryToNat(x')
}

// And we can convert in the opposite direction.
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) {
  y != Zero && (x.Suc? ==> Less(x.pred, y.pred))
}

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)
{
}

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
{
}

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);
    }
}

// Let's finish up with another classic container structure,
// which we will spend more time with next time.

datatype BST<V> = Leaf
  | Node(key: int, value: V, left: BST<V>, right: BST<V>)
// Here we have binary search trees (BSTs), specialized to integers
// as keys, because it takes some more work to handle arbitrary key types.
// (We'll see one way next time.)

// In fact, this is a good excuse to introduce another classic container,
// much more commonly found in functional languages than elsewhere.
datatype Option<T> = None | Some(value: T)

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))
}

// This property gets a nice automated proof.
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>)

predicate InRange(n: int, r: range) {
  (r.0.None? || r.0.value < n)
    && (r.1.None? || n < r.1.value)
}

// A BST is well-formed w.r.t. a range if all its keys fall in the range.
predicate WellFormedBST<V>(t: BST<V>, rng: range) {
  match t
  case Leaf => true
  case Node(k, _, l, r) =>
    InRange(k, rng)
    && WellFormedBST(l, (rng.0, Some(k)))
    && WellFormedBST(r, (Some(k), rng.1))
}

// The next two proofs are interesting for forcing us to invoke IHes explicitly.
lemma LookupRange<V>(t: BST<V>, k: int, rng: range)
  requires WellFormedBST(t, rng)
  requires Lookup(t, k).Some?
  ensures InRange(k, rng)
{
  match t
  case Leaf =>
  case Node(k', v, l, r) =>
    if k == k' {
    } else if k < k' {
      LookupRange(l, k, (rng.0, Some(k')));
    } else {
      LookupRange(r, k, (Some(k'), rng.1));
    }
}

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' {
      InsertWellFormed(l, (rng.0, Some(k')), k, v);
    } else {
      InsertWellFormed(r, (Some(k'), rng.1), k, v);
    }
}

Exercises

class05/at_append.dfy
dfy
// Definitions to prove something about

datatype List<T> = Nil | Cons(head: T, tail: List<T>)

function Length<T>(xs: List<T>): nat {
  match xs
  case Nil => 0
  case Cons(_, tail) => 1 + Length(tail)
}

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))
}

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)
}

// Your task

lemma AtAppend<T>(xs: List<T>, ys: List<T>, i: nat)
  requires i < Length(Append(xs, ys))
  ensures At(Append(xs, ys), i) == At(Append(xs, ys), i)
  // Change the righthand side of this 'ensures' to say something more interesting.
  // Try to capture the expected behavior of 'At' as completely as possible.
  // The proof should go through automatically if you get it right!
{
}
class05/reverse.dfy
dfy
// Defining lists and some operations on them

datatype List<T> = Nil | Cons(head: T, tail: List<T>)

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))
}

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))
}

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 SlowReverse<T>(xs: List<T>): List<T> {
  match xs
  case Nil => Nil
  case Cons(x, tail) => Snoc(SlowReverse(tail), x)
}

// Lemmas you'll likely find helpful

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))
{
}

// The lemma for you to prove

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);
      == ReverseAux(xs, acc);
    }
    // Extend this proof to pass Dafny checking.
}
Copyright 6.S057 course staff.