Verification of Object-Oriented Programs

Jean-Baptiste Tristan & K. Rustan M. Leino

Recap

  • We have seen how to reason about
    • Functional programs
    • Imperative code without and with pointers
  • Now we are going to “verify” objects

Objects and verification

  • Verifying objects is different from code

  • So far, an invariant was always attached to a single function of statement

  • In an object, the invariant is shared by all methods in the class

  • The invariant might need to be framed using this

      class B {
    
        var x: nat
    
        predicate Invariant()
          reads this
        {
          x % 2 == 0
        }
    
      }

Objects and verification – 1

  • constructor

      class B {
    
        // ...
    
        constructor(x: nat)
          requires x % 2 == 0
          ensures Invariant()
        {
          this.x := x;
        }
    
      }

Objects and verification – 2

  • Methods might also need to be framed

  • Methods/functions that read the object usually requires the invariant as a precondition

  • Methods/functions that modify the object must ensure the invariant as a postcondition

      class B {
    
        // ...
    
        method Add(y: nat)
          requires Invariant()
          modifies this
          ensures Invariant()
        {
          this.x := this.x + 2 * y;
        }
      }

Objects and verification – 3

  • Some functions/methods read from the object

  • Typically, they ensures some postcondition

  • The invariant must be strong enough for the postcondition to hold

      class B {
    
        // ...
    
        function Get(): nat
          requires Invariant()
          reads this
          ensures Get() % 2 == 0
        {
          x
        }
    
      }

Objects and verification – 4

  • Why is it difficult?
  • Once you understand the need for an object invariant predicate, what is the big deal?
  • It is hard to get it all correct at once
  • You need a strategy to co-evolve the code and the invariant and the functional specification

Objects and verification – 5

  • Note that so far it would be fine for Add not to ensure the invariant

  • What would go wrong?

  • Function precondition might not hold when you use the object

      class B {
    
        // ..., but without postcondition of Add
    
      }
    
      method Client() {
        var b: B := new B(2);
        b.Add(3);
        var c: nat := b.Get(); // Error
      }

Objects and verification – 6

  • As long as an object doesn't change what other objects in contains, the framing is static and simple

  • If an object uses another object, you must have some way to refer to its frame

  • Objects can define a ghost constant, usually called Repr (for representation), to denote the set of contained objects

      class D {
        var x: nat
        ghost const Repr: set<object> := {this}
      }
    
      class E {
        method M(a: D)
          modifies a.Repr
        {
          a.x := 4;
        }
      }

Ghost state – 10

  • Here again, ghost state is very useful

  • It can be used to relate the object to a purely functional implementation

  • It can be used to carry information about the past of the object

  • For example, to keep track of accesses to a storage

      datatype Entry<T> = Entry(key: nat, value: T)
    
      class Storage<T> {
        ghost var log: seq<Entry<T>>
      }

Dynamic data structures

  • Some data structures are aggregations of objects
  • Example: linked list
  • LRU cache, but only indirectly
  • In such a case, the frame is dynamic
  • We're going to discuss verification of such aggregates

Linked lists

  • We are going to consider two ways to implement a linked list
  • In both implementations, the data is contained in cells
  • In the first, the linked list is defined as the cell
  • In the second, the linked list has a special root object

Linked list example – 1

  • Let's grow the API, strengthening the invariant as we go

      class LL<T> {
    
        const value: T
        var next: LL?<T>
    
        constructor(value: T)
          ensures this.value == value
          ensures this.next == null
        {
          this.value := value;
          next := null;
        }
      }

Linked list example – 2

  • Cons creates a new cell and attaches to its tail

      class LL<T> ... {
    
        method Cons(v: T) returns (l: LL<T>) {
          l := new LL(v);
          l.next := this;
        }
    
      }

Linked list example – 3

  • Head and Tail can be defined as functions

      class LL<T> ... {
    
        function Head(): T {
          value
        }
    
        function Tail(): LL<T>
          requires next != null
          reads this
        {
          next
        }
    
      }

Linked list example – 4

  • Length is problematic

  • We need to ensure that the function terminates

  • Intuitively we know why it does:

  • We intend for the LL not to be cyclic

  • We will need to explicitate our intentions in the invariant

      class LL<T> ... {
    
        function Length(): nat
          reads this
        {
          1 + if next == null then 0 else next.Length() // Error: might not terminate
        }
    
      }

Dynamic frames

  • We need to reason about the set of objects composing the LL

  • We can use a ghost set dynamic frame

  • We can state that a cell is not in the representation of its successor

  • This implies the list cannot be cyclic

      class LL<T> ... {
    
        ghost var Repr: set<object>
    
        ghost predicate Invariant()
          reads this, Repr
          decreases Repr
        {
          && this in Repr
          && (next != null ==>
             && next in Repr
             && next.Repr <= Repr
             && this !in next.Repr
             && next.Invariant())
        }
    
      }

Go over existing code again – 1

  • We strengthened the invariant

  • We need to review all the methods for which it is a postcondition

      class LL<T> ... {
    
        constructor OH(value: T)
          ensures this.value == value
          ensures this.next == null
          ensures Invariant()
        {
          this.value := value;
          next := null;
          Repr := {this};
        }
    
      }

Go over existing code again – 2

  • This is a good strategy to develop verified data structures

  • Add methods one by one, strengthening invariant as you go, adapting existing code every time

      class LL<T> ... {
    
        method Cons(v: T) returns (l: LL<T>)
          requires Invariant()
          modifies Repr
          ensures Invariant()
          ensures l.Invariant()
        {
          l := new LL(v);
          l.next := this;
          l.Repr := Repr + {l};
        }
    
      }

Length

  • Now the invariant is strong enough for Length

  • In practice, there is a continuous co-design of invariant and code

      class LL<T> ... {
    
        function Length(): nat
          requires Invariant()
          reads Repr
          decreases Repr
        {
          1 + if next == null then 0 else next.Length()
        }
    
      }

Append – 1

  • Append is interesting because it involves two LLs

  • We need to require the LLs to be made of distinct objects

      class LL<T> ... {
    
        method Append(l: LL<T>)
          requires next != null
          requires Invariant()
          requires l.Invariant()
          modifies Repr
          ensures Invariant()
    
      }

Append – 2

  • We do so with the frame rule

  • Here, Append requires some proof work

      class LL<T> ... {
    
        method Append(l: LL<T>)
          requires next == null
          requires Invariant()
          requires Repr !! l.Repr
          requires l.Invariant()
          modifies Repr
          ensures Invariant() // Fails
        {
          next := l;
          Repr := Repr + l.Repr;
        }
    
      }

Proof methodology

  • Use the forward/backward approach to meet in the middle and identify the issue
  • It could be one of 3 things: invariant too weak, code wrong, proof not detailed enough
  • Proofs help you diagnose the source of the problem, in this case: Dafny!
  • Meeting in the middle: forall c: LL :: c in old(Repr) ==> c !in l.Repr
  • Dafny needs some help with the following lemma

Linked list example – 5

  • The previous example is a common way to verify linked list

  • In Dafny, you can do much much better

  • Let's use dummy cells and a special root

      class Cell<T> {
    
        var next: Cell?<T>
        var value: T
    
        constructor(value: T)
          ensures this.value == value
          ensures this.next == null
        {
          this.value := value;
          next := null;
        }
    
      }

Roots to the rescue

  • The LL will be defined as its root, as opposed to a cell

  • Here, Dafny shines: you can use any existing ghost data structure

  • Intuitively, we think of an LL as a sequence of cells

  • We can capture this directly in the representation!

  • Hence, we declare the representation to be a sequence

      class LL<T> {
    
        var length: nat
        ghost var Repr: seq<Cell<T>>
        var hd: Cell?<T>
    
      }

Thinking ahead of the invariant

  • Again, we could grow the invariant

  • But with the additional structured dynamic frames, it is easier to capture our intuition

      class LL<T> ... {
    
        ghost predicate Invariant()
          reads this, Repr
        {
          && length == |Repr|
          && (length == 0 <==> hd == null)
          && (length > 0 ==> Repr[0] == hd)
          && (length > 0 ==> Repr[length-1].next == null)
          && (forall idx: nat :: idx < length - 1 ==>
              Repr[idx].next != null
              && Repr[idx].next == Repr[idx+1]
             )
        }
    
      }

Linked list example – 6

  • Nothing surprising, just need to initialize the dynamic frame

      class LL<T> ... {
    
        constructor()
          ensures hd == null
          ensures length == 0
          ensures Invariant()
        {
          length := 0;
          hd := null;
          Repr := [];
        }
    
      }

Linked list example – 7

  • Implement methods one by one
  class LL<T> ... {

    method Cons(v: T)
      requires Invariant()
      modifies this, Repr
      ensures Invariant()
    {
      var o: Cell := new Cell(v);
      o.next := hd;
      hd := o;
      Repr := [o] + Repr;
      length := length + 1;
    }

  }

Linked list example – 8

  • The anti-aliasing is surprisingly easier to state
  class LL<T> ... {

    method AppendNull(l: LL<T>)
      requires length == 1
      requires Invariant()
      requires l.Invariant()
      requires Repr[0] !in l.Repr
      modifies this, Repr
      ensures Invariant()
    {
      if l.length == 0 {
      } else {
        hd.next := l.hd;
        Repr := Repr + l.Repr;
        length := length + l.length;
      }
    }

  }