Deductive Verification of Transition Systems in Why3

State Machine Specifications and Inductive Invariants


State machines and transition relations are used in computer science to model the execution of software systems, and study their semantics and safety properties, in different contexts.

Important properties of a state machine specification have to do with the configurations that can be reached from the initial states.

An invariant of such a state machine is any property that holds in all reachable configurations. Invariants are used in deductive verification to prove safety properties

In any state machine there are likely some bad/undesired configurations, for instance concerning errors or inconsistencies that should not be allowed to occur. 

A safety property of a system is any reachability property that can be framed as “no bad/undesired configuration of the system can be reached from the initial states”. To prove a safety property it suffices to find an invariant that is not true in bad configurations. This immediately implies that such configurations are unreachable. 

One way to prove that a property is an invariant is by using induction:

Not all invariants are inductive! Model checking may be able to “prove” any invariant, but in deductive verification inductive invariants are crucial, since it is possible to reason about them without exploring the state space of the system. 



State Machine Specifications in Why3, by Example

Imperative (stateful) Encoding


Stateful software modules with multiple state-changing functions can be seen to define state machines. 

Consider a verified implementation in Why3 of a “ To Do” software concept, where the state is a pair of sets of tasks, pending and finished

module ToDo_implicit
  type task 
  clone export set.SetImp with type elt = task

  type statettype = { pending : set ; finished : set }

  val state : statettype
  ...

The API of the module consists of three functions: add, complete, and delete. The first of these simply adds a task to the pending set: 

  let addT (t:task) : ()
    requires { not mem t (union state.pending state.finished) }
  = add t state.pending

In order to write the function’s specification as a contract we can use logical functions from Why3’s set theory (https://www.why3.org/stdlib/set.html).  

The requires clause (a precondition)states that tasks can only be added if they’re not yet in any of the two lists. 

We can add to this postconditions (ensures clauses) giving a logical description of the effects of the function on the module’s state, using the old operator to refer to the state before execution of the function: