Métodos Formais em Engenharia de Software

Mestrado em Engenharia Informática

Why3: Abstract Specifications and Ghost Code

Recall the Trash Software Concept in Alloy, and the corresponding state variables and actions:

sig Item {}
var sig Accessible in Item {}
var sig Trashed in Item {}

pred create[i : Item] {
        i not in Accessible + Trashed
        Accessible' = Accessible + i
        Trashed' = Trashed }

pred delete[i : Item] {
        i in Accessible
        Accessible' = Accessible - i
        Trashed' = Trashed + i }

pred restore[i : Item] {
        i in Trashed
        Accessible' = Accessible + i
        Trashed' = Trashed - i }

pred empty {
        some Trashed
        no Trashed'
        Accessible' = Accessible }

Recall also that in the last lecture we already implemented this concept as a WhyML module, using lists to represent the two sets Accessible and Trashed:

module Trash_Record
  type item 
  type stateT = { mutable accessible : list item ;
                  mutable trashed : list item }
    invariant  { forall x :item. not (mem x accessible /\ mem x trashed) }
    by { accessible = Nil ; trashed = Nil }

  val state : stateT

  let create (i : item) 
    requires { not mem i state.accessible /\ not mem i state.trashed }
  = state.accessible <- Cons i state.accessible 

  let delete (i : item)
    requires { mem i state.accessible }
  = state.accessible <- deleteAll i state.accessible ;
    state.trashed <- Cons i state.trashed 

  let restore (i : item)
    requires { mem i state.trashed }
  = state.trashed <- deleteAll i state.trashed ;
    state.accessible <- Cons i state.accessible 

  let empty ()
    requires { not is_nil state.trashed }
  = state.trashed <- Nil ;

end

Abstract Specifications in WhyML

We will now see how to convert a sets/relations-based Alloy specification to WhyML, to allow concrete implementations (such as the one given above for Trash) to be proven correct. This will result in a complete development workflow:

We will write the Alloy specification in WhyML using the notion of ghost code.

Ghost code operates on ghost state, and is a very useful device in program specification. It allows for programs to be equipped with ‘shadow’ instructions working on ghost variables. These variables can appear in function contracts, which allows for properties relating regular code and ghost code to be written.

A WhyML version of the Alloy specification:

sig Item {}
var sig Accessible in Item {}
var sig Trashed in Item {}

can be written as follows:

  use set.Fset

  type stateT = { ghost mutable accessible : fset item ;
                  ghost mutable trashed : fset item }

  val state : stateT

A state variable is declared in the module of the stateT record type, which has ghost fields only. Note that ghost data has logic types such as **fset** item. Just as in the Alloy spec, the trash concept has two sets of items as state variables.

Recall that the following invariant has been established for the Alloy spec:

always no Accessible & Trashed

This can be included in the WhyML specification as a type invariant:

  type stateT = { ghost mutable accessible : fset item ;
                  ghost mutable trashed : fset item }

     invariant  { disjoint accessible trashed }

     by { accessible = empty ; trashed = empty }

As to the actions in the Alloy specification, they will be defined as functions manipulating the ghost state. We inspect the specification and identify a precondition in which action predicate. The remaining clauses describe the state change. For instance:

pred create[i : Item] {
        i not in Accessible + Trashed
        Accessible' = Accessible + i
        Trashed' = Trashed }

The new state will be produced by writing instructions manipulating the ghost fields (note that add is an Fset function).

  let ghost create (i : item) 
    requires { not mem i (union state.accessible state.trashed) }
  = state.accessible <- add i state.accessible

Note that the mem predicate is here Fset.mem. The function is marked as **ghost** since it contains exclusively ghost code.

We can of course equip it with a postcondition

  let ghost create (i : item) 
    requires { not mem i (union state.accessible state.trashed) }
    ensures  { state.accessible = add i (old state.accessible) /\
               state.trashed = old state.trashed }
  = state.accessible <- add i state.accessible

The ghost instruction state.accessible <- **add** i state.accessible and the postcondition are equivalent logic descriptions of how this action changes the state. However, only the former allows for the state invariant to be established.

  let ghost delete (i : item)
    requires { mem i state.accessible }
  = state.accessible <- remove i state.accessible ;
    state.trashed <- add i state.trashed

  let ghost restore (i : item)
    requires { mem i state.trashed }
  = state.trashed <- remove i state.trashed ;
    state.accessible <- add i state.accessible 

  let ghost empty ()
    requires { not (is_empty state.trashed) }
  = state.trashed <- empty

From this specification Why3 will generate 4 valid verification conditions labeled “type invariant”, which together ensure that the provided state invariant is valid (no item is simultaneously accessible and in the trash).

[permalink]

Implementing Abstract Specifications

We will now implement the specified concept by adding regular state variables (record fields) and code to the type and function definitions in the previous module.

A crucial element of this method is the presence of a refinement invariant that establishes a correspondence between the specification state (ghost fields) and the implementation state (regular fields).

In the current example we simply put together in a single module the previous two (specification and implementation). The state record type contains two ghost fields (sets of items) and two regular fields (lists of items). The refinement invariant simply states that the sets contain the same elements as the lists.

  type stateT = { mutable accessible_ : list item ;
                  mutable trashed_ : list item ;
                  ghost mutable accessible : fset item ;
                  ghost mutable trashed : fset item }

    (* Specification-level invariant *)
    invariant  { disjoint accessible trashed } 

    (* Refinement correspondence *)
    invariant  { accessible = elements accessible_ /\
                 trashed    = elements trashed_ }

    by { accessible_ = Nil ;
         trashed_ = Nil ; 
         accessible = empty ;
         trashed = empty }

  val state : stateT

Noe that there is no need for the following implementation-level invariant, which follows from the spec-level invariant and the refinement correspondence:

    invariant  { forall x :item. not (mem x accessible /\ mem x trashed) }

Observe also that it may be a good idea to keep separate modules for

  1. Specification, containing only ghost code and proofs of specification-level invariants
  2. Implementation, containing both the ghost and regular code.

In this case there is no need to include specification-level invariants in the implementation module.

Similarly, in the presence of specification contracts the corresponding implementation-level preconditions can be omitted. For instance

  let ghost create (i : item) 
    requires { not mem i (union state.accessible state.trashed) } 
    requires { not mem i state.accessible_ /\ not mem i state.trashed_ } 
    ...

The complete definition would be:

  let create (i : item) 
    requires { not mem i (union state.accessible state.trashed) }
  = state.accessible_ <- Cons i state.accessible_ ;
    state.accessible <- add i state.accessible

Note that the simultaneous execution of regular and ghost code ensures the preservation of the refinement correspondence invariant, which in turn guarantees that the implementation adheres to the specification.

As a second example (deleteAll is a function that removes all occurrences of an element from a list):

  let delete (i : item)
    requires { mem i state.accessible }
  = state.accessible_ <- deleteAll i state.accessible_ ;
    state.trashed_ <- Cons i state.trashed_ ;
    state.accessible <- remove i state.accessible ;
    state.trashed <- add i state.trashed

[permalink]

Exercise

Sets can of course be represented by lists not containing repeated elements, in which case the list delete function can be optimized (no need to to look for further occurrences when the the desired element is found).

Modify the previous implementation by adding the following invariant to the state record type:

    invariant { forall i :item. num_occ i accessible_ <= 1 /\
                forall i :item. num_occ i trashed_ <= 1 }

Then write the optimized function deleteOne and substitute it for deleteAll.

What happens if you use deleteOne without adding the above type invariant ?

A Second Example: the Reservations Concept

It is not always appropriate to interpret an Alloy domain predicate as a FOL type. This example illustrates how a finite set can be used instead.

Recall the Reservations concept, where users book resources:

sig Resource {}
sig User {
        var reservations : set Resource
}
var sig Available in Resource {}

We will translate this as follows:

    type resource 
    type user
    type pair = (user,resource)

    type stateT = { ghost resources : fset resource ;
                    mutable ghost available : fset resource ;
                    mutable ghost reservations : fset pair }

The available and reservations state record fields are the expected sets, but we also include a field resources corresponding to the Resource predicate in the Alloy spec. Note that this field is immutable, since the set of resources is fixed in the Alloy specification.

Note that resources in the Alloy spec now correspond to inhabitants of the WhML type resource that are elements of the set resources. This requires the presence typing invariants stating that available and reserved resources are all elements of resources :

    invariant  { forall r :resource. mem r available -> mem r resources }

    invariant  { forall r :resource.
            (exists u :user. mem (u,r) reservations) -> mem r resources }

Recall the following two state invariants identified in the Alloy specification:

all r :Resource, u :User | always (r in Available implies r not in u.reservations)

always (all r : Resource | lone reservations.r)

We include them as type invariants as follows:

    invariant  { forall u :user, r :resource. 
                   mem (u,r) reservations -> not mem r available }

    invariant  { forall u1 u2 :user, r :resource. 
                   mem (u1,r) reservations /\ mem (u2,r) reservations -> u1=u2 }

    by { resources = empty ; available = empty ; reservations = empty }

    val state : stateT

The all-sets-empty record is provided as an example of a state for which all the provided invariants hold.

Recall the first three action predicates of this specification:

pred reserve[u : User, r : Resource] {
        r in Available 
        Available' = Available - r        reservations' = reservations + u->r 
}
pred cancel[u : User, r : Resource] {
        r in u.reservations        
        Available' = Available + r        reservations' = reservations - u->r        
}
pred use[u : User, r : Resource] {
        r in u.reservations         
        Available' = Available            reservations' = reservations - u->r        
}

Their WhyML versions offer no surprises:

    let ghost reserve (u:user) (r:resource) 
      requires { mem r state.available }
      ensures  { state.available = remove r (old state.available) }
      ensures  { state.reservations = add (u,r) (old state.reservations) }
    = 
      state.available <- remove r state.available ;
      state.reservations <- add (u,r) state.reservations

    let ghost cancel (u:user) (r:resource) 
      requires { mem (u,r) state.reservations }
      ensures  { ... }
    = state.available <- add r state.available ;
      state.reservations <- remove (u,r) state.reservations

    let ghost uses (u:user) (r:resource) 
      requires { mem (u,r) state.reservations }
      ensures  { ... }
    = state.reservations <- remove (u,r) state.reservations

Cleanup is a more challenging action.

pred cleanup {
        some Resource - Available - User.reservations 
        Available' = Resource - User.reservations      
        reservations' = reservations 
}

We will use the **filter** library function to obtain the set of resources that are not reserved by any user.

    function filterRsr (s:fset resource) (sp:fset pair) : fset resource =
      filter s (fun r -> forall u :user. not mem (u,r) sp)

    let ghost cleanup ()
      requires { exists r :resource. mem r state.resources /\
                   not mem r state.available /\
                   forall u :user. not mem (u,r) state.reservations }
      ensures  { forall r :resource. mem r state.available <->
                   mem r state.resources /\ 
                   forall u :user. not mem (u,r) state.reservations }
      ensures  { state.reservations = old state.reservations }
    =
      state.available <- filterRsr state.resources state.reservations

Refinement

In order to implement this specification it will be useful to keep a list of resources that are currently in use, so that they can be found without looking at the entire list of resources.

We can add this state variable at the specification level, thus refining the previous specification.

    type stateT = { ghost resources : fset resource ;
                    mutable ghost available : fset resource ;
                    mutable ghost used : fset resource ;
                    mutable ghost reservations : fset pair }
      (* ... *)

      (* Refinement correspondence *)
      invariant  { forall r :resource. mem r used <->
                     mem r resources /\
                     not (exists u :user. mem (u,r) reservations) /\
                     not mem r available }

    by { resources = empty ; available = empty ; used = empty ; reservations = empty }

Note the presence of a new type invariant stating that the set of used resources consists of all resources that are neither reserved nor available.

The definitions of the reserve and cancel functions remain unchanged, but uses now updates resources in use:

    let ghost uses (u:user) (r:resource) 
      requires { mem (u,r) state.reservations }
    =
      state.reservations <- remove (u,r) state.reservations ; 
      state.used <- add r state.used 

Finally, cleanup makes available resources in use without filtering the entire set of resources, and empties the set of used resources:

    let ghost cleanup ()
      requires { exists r :resource. mem r state.resources /\
                   not mem r state.available /\
                   forall u :user.not mem (u,r) state.reservations }
    =
      state.available <- union state.available state.used ;
      state.used <- empty

[permalink]

Exercise

Now it’s time to implement this concept.

Since the reservations relation has a unicity constraint (resources are reserved by at most one user), one natural possibility is to implement it as a map from resources to (single) users. The used and available sets can be implemented as lists. Take the following as a starting point.

Note that for an implementation with uninterpreted types for users and resources, you will need to declare an equality predicate to be instantiated later when the types are defined. See below.

  val (=) (x y : resource) : bool
    ensures { result <-> x = y }

  (* map from resources to users *)
  clone fmap.MapApp with type key = resource
  type  mapRU = MapApp.t user

  type stateT = { ghost resources : fset resource ;
                  mutable available_ : list resource ;
                  mutable used_ : list resource ;
                  mutable reservations_ : mapRU ;
                  mutable ghost available : fset resource ;
                  mutable ghost used : fset resource ;
                  mutable ghost reservations : fset pair }

  val state : stateT

  ...