Obtaining and running COMP

COMP is part of SpeX, a multi-language formal-specification environment written in Maude that supports various specification languages by means of a generic notion of information processor. Therefore, in order to use COMP, both Maude and SpeX need to be installed.

Maude

GNU/Linux and macOS binaries of Maude are available at its GitHub site. We recommend installing a recent version of Maude: 3.2 or newer. To install Maude, it suffices to:

  1. Extract the files in the downloaded ZIP archive to a convenient directory:
    
      sudo unzip Maude-3.2.1-linux.zip -d /usr/local/maude-3.2.1/
                      
  2. Make a discoverable link to the Maude executable; for example, provided that /usr/local/bin/ is in the PATH of directories where executable files are located:
    
      sudo ln -s /usr/local/maude-3.2.1/Linux64/maude.linux64 \
                 /usr/local/bin/maude
                      
  3. Set the MAUDE_LIB environment variable appropriately:
    
      export MAUDE_LIB=/usr/local/maude-3.2.1/Linux64
                      
    To make this setting persistent, you can add the above line to your .bashrc file.

SpeX

With Maude installed, you can proceed to downloading the latest distribution of SpeX from its GitLab site. You can install SpeX using the following commands:

  1. Extract the files in the downloaded archive:
    
      tar -xzf spex-22.09.tar.gz
                      
  2. Configure, make, and install SpeX (from its source-tree directory):
    
      cd spex-22.09/ && ./configure && make && sudo make install && cd -
                      

COMP

The COMP interpreter can be installed in a similar manner to SpeX. Once you download the latest distribution of the tool from its GitLab site, COMP can be installed using the following commands:

  1. Extract the files in the downloaded archive:
    
      tar -xzf comp-22.09.tar.gz
                      
  2. Configure, make, and install \comp (from its source-tree directory):
    
      cd comp-22.09/ && ./configure && make && sudo make install && cd -
                      

You can now run COMP from the command line to be greeted with the following message. Any subsequent user input (declarations of system specifications or commands) under the COMP > prompt is meant to be handled by the COMP interpreter.

Writing COMP specifications

A COMP specification consists of a series of modules that introduce either data types or behavioural objects and are interrelated through various data-importation and object-composition operators. In this tutorial, we discuss only some of the main specification features of COMP. We introduce them by means of a simple example of a mutual-exclusion protocol.

QLock is a variant of Dijkstra's concept of binary semaphore. It can be used in systems with multiple execution threads to ensure that distinct processes cannot access a critical resource at the same time. The basic idea is that, under QLock, every process cycles through three sections: remainder, waiting, and critical. All processes are initially in the remainder section. To access the critical section, they must fist announce their intention by switching to the waiting section, which also means entering a waiting queue. Only the process at the head of the waiting queue is allowed to enter the critical section – after which it can exit to return to the remainder section at any time.

Data types

We begin by specifying the data types used by QLock, namely sections and process identifiers. For sections, we consider a data module named PC (for Program Counter) with four declarations: first, of a sort PS (same as the name of the module) that captures the data type of sections; then, three constants (operations with an empty arity) of sort PC, one for each possible state of a process: rm for the remainder section, wt for the waiting section, and cs for the critical section. We indicate that these constants are data constructors through the ctor attribute.


  data PC is
    sort PC .
    ops rm, wt, cs : () -> PC [ctor] .
  enddata
              
The code can be either typed directly at the COMP prompt or loaded from an external text file. If all is written correctly and the interpreter accepts the input, you should receive a message of the form Loading module PC, as in the following image.

Process identifiers are defined by means of natural numbers. Therefore, before declaring them, we need to load the library Nat.comp, which provides basic support for working with natural numbers. The file is available (together with Bool.comp, which is used to define relations on natural numbers) in the COMP library of examples.


  load Nat.comp
              

In the module below, we use pid to build process identifiers (pids) from natural numbers, and we form sequences of pids through plain juxtaposition using the mixfix operator __. Any non-empty (i.e. not nil) sequence is of the form I ... nil, where I is a pid which may be followed by other pids. For example, the sequence of pids given by the numbers 0, 1, and 2 is written pid(0) pid(s 0) pid(s s 0) nil.


  data PID is
    protecting NAT .

    sort PId .
    op pid : Nat -> PId [ctor] .

    sort PIdSequence .
    op nil : () -> PIdSequence [ctor] .
    op __ : PId PIdSequence -> PIdSequence [ctor] .
  enddata
              

Next, we define a couple of operations on pids and pid sequences that will be used in the later part of the specification: just builds a singleton sequence; add appends a pid at the end of a sequence; head returns, if possible, the prefix of length one of a sequence, or nil otherwise; and tail returns the pids after the head of a sequence. We capture the application of these operations through axioms (in this case, plain equations), which we introduce using the keyword ax.


  data PID/OPS is
    protecting PID .
    vars I, J : PId . var S : PIdSequence .

    op just : PId -> PIdSequence .
    ax just(I) = I nil .

    op add : PId PIdSequence -> PIdSequence .
    ax add(I, nil) = I nil .
    ax add(I, J S) = J add(I, S) .

    ops head, tail : PIdSequence -> PIdSequence .
    ax head(nil) = nil .
    ax head(I S) = I nil .
    ax tail(nil) = nil .
    ax tail(I S) = S .
  enddata
              

Behavioural objects

We have thus arrived at the objects of the QLock protocol. The hierarchical structure of the system is depicted in the diagram below, where PS represents a database of processes (specified by PROCESS) indexed by pids, QUEUE models the waiting queue for accessing the critical section, and QLOCK is the specification of the protocol. Each solid line denotes a composition of objects. The first one, from PROCESS, intersecting the dashed line from PID, indicates an indexed composition: PS (the compound object) can be regarded as an arbitrarily large database containing a copy of PROCESS for each pid. The second one, from PS and QUEUE, indicates a synchronized composition: QLOCK (the compound object in this case) is obtained by putting together the objects PS and QUEUE and by synchronizing them (using additional operations).

The QLock system

Let's start with PROCESS. A process supports three actions (operations that change the state of the process: want, to signal the intent to access the critical section; try, to enter the critical section; and exit, to leave the critical section. The actual program counter is given by the observation pc – similarly to actions, observations are defined on process states; however, they do not change those states and return instead some kind of useful data. We use init to denote the initial state of a process, and axioms to specify how the program counter changes under the effect of the three actions. Note that, at this stage, since a process has no knowledge of the program counters of other processes, we impose no restriction on the try action (such as checking that the process can really enter the critical section). That feature is added later on in QLOCK.


  bobj PROCESS is
    protecting PC .
    var S : State .

    op init : () -> State .
    act _>>=`want : State -> State .
    act _>>=`try : State -> State .
    act _>>=`exit : State -> State .

    obs pc : State -> PC .
    ax pc(init) = rm .
    ax pc(S >>= want) = wt .
    ax pc(S >>= try) = cs .
    ax pc(S >>= exit) = rm .
  endbo
              

To define the process database, we simply use the indexing operator. We add a constant init to denote the initial state of the database, and an axiom to define the state of every process I (identified by its pid) in the initial state of the database. So, notice that, in the context of PS, we deal with two kinds of behavioural objects, and thus with two notions of state: states of processes, denoted PROCESS/State, and states of the process database, denoted State.


  bobj PS is
    indexing PROCESS on PID by PId .
    var I : PId .

    op init : () -> State .
    ax PROCESS/State(I, init) = init .
  endbo
              

The QUEUE has two defining actions: enq for adding a new process to the waiting queue; and deq for removing the top/front process of the waiting queue. We consider two observations: one of them, seq, which lists all pids in the queue, is a core observation; the other, head is derived (i.e., it may be seen as a macro). In addition, here too we consider an initial state where the queue has no waiting processes.


  bobj QUEUE is
    protecting PID/OPS .
    protecting NAT/PREDECESSOR .
    var Q : State . var I : PId .

    op init : () -> State .
    act _>>=`enq`(_`) : State PId -> State .
    act _>>=`deq : State -> State .
    obs seq : State -> PIdSequence .

    ax seq(init) = nil .
    ax seq(Q >>= enq(I)) = add(I, seq(Q)) .
    ax seq(Q >>= deq) = tail(seq(Q)) .

    obs head : State -> PIdSequence .
    ax head(Q) = head(seq(Q)) .
  endbo
              

The last object in this tutorial is QLOCK, which synchronizes the process database, PS, with QUEUE.


  bobj QLOCK is
    syncing PS and QUEUE .
    var S : State . var I : PId .

    op init : () -> State .
    ax PS/State(init) = init .
    ax QUEUE/State(init) = init .
              

Besides the initial state, we also consider three actions: want, try, and exit. These mirror the actions declared for PROCESS; however, this time, because they are meant to update the state of the protocol (which includes a database of processes), we need to pass a process id as an additional argument. For example, for the want action, we write:


    act _>>=`want`(_`) : State PId -> State .
              

We specify how protocol states change under the effect of want in a componentwise fashion. That is, we axiomatize how each of the two components of QLOCK (PS and QUEUE) is affected by want – and in this way we capture the inner workings of the QLock protocol. For example, the first of the following four axioms specifies that a process I can enter the waiting queue (in a state S of the protocol) only if its program counter is in the remainder section.


    ax PS/State(S >>= want(I))
     = PS/State(S) >>= want :: PROCESS(I) if PS/PROCESS/pc(I, S) = rm .
    ax PS/State(S >>= want(I))
     = PS/State(S) if not PS/PROCESS/pc(I, S) = rm .
    ax QUEUE/State(S >>= want(I))
     = QUEUE/State(S) >>= enq(I) if PS/PROCESS/pc(I, S) = rm .
    ax QUEUE/State(S >>= want(I))
     = QUEUE/State(S) if not PS/PROCESS/pc(I, S) = rm .
              

The remaining two actions are defined in a similar manner:


    act _>>=`try`(_`) : State PId -> State .
    ax PS/State(S >>= try(I))
     = PS/State(S) >>= try :: PROCESS(I)
     if PS/PROCESS/pc(I, S) = wt and QUEUE/head(S) = just(I) .
    ax PS/State(S >>= try(I))
     = PS/State(S)
     if not PS/PROCESS/pc(I, S) = wt or not QUEUE/head(S) = just(I) .
    ax QUEUE/State(S >>= try(I))
     = QUEUE/State(S) .

    act _>>=`exit`(_`) : State PId -> State .
    ax PS/State(S >>= exit(I))
     = PS/State(S) >>= exit :: PROCESS(I) if PS/PROCESS/pc(I, S) = cs .
    ax PS/State(S >>= exit(I))
     = PS/State(S) if not PS/PROCESS/pc(I, S) = cs .
    ax QUEUE/State(S >>= exit(I))
     = QUEUE/State(S) >>= deq if PS/PROCESS/pc(I, S) = cs .
    ax QUEUE/State(S >>= exit(I))
     = QUEUE/State(S) if not PS/PROCESS/pc(I, S) = cs .
  endbo
              

This concludes the specification of the QLock protocol. To get a list of all modules that we've loaded so far into the COMP interpreter, we can use the command list modules. Alternatively, we can use list data modules or list bobj modules to list only data-type or object modules.

Checking behavioural properties

COMP supports the verification of behavioural properties of objects through a check command, which becomes available once an object module is opened for verification.


  open QLOCK
              

Notice that the prompt changes to COMP/QLOCK to indicate that the module QLOCK is open and that new commands are now available. For example, we can use the reduce command to inspect how the initial state of the protocol changes when one of the processes, with pid 0, enters the waiting queue. In the image below, we examine the state init >>= want(pid(0)) obtained by executing the action want for the pid 0 in the initial state of QLOCK. To that end, we evaluate the sequence of pids in the waiting queue, given by the observation QUEUE/seq (i.e., seq from the component QUEUE), and the program counter of the process with id 0, given by the observation PS/PROCESS/pc (i.e., pc from the subcomponent PROCESS of the component PS). These yield, as expected, pid(0) nil and wt; in other words, the process with id 0 is now waiting to enter the critical section.

Similarly, we can inspect the state of the protocol after a more complex chain of interactions like init >>= want(pid(0)) >>= want(pid(s 0)) >>= try(pid(0)) >>= exit(pid(0))). Here, both process 0 and 1 announce (in this order) their intention to access the critical resource; process 0 attempts and succeeds to enter the critical section, and then leaves. These additional tests are included in the full source code of the QLock protocol.

Let us now turn our attention to a simple behavioural property, namely that, for any two distinct processes in the waiting section, and which are not the head of the queue, the order of the attempts to enter the critical section is insignificant. For that purpose, we let s be a state of the protocol and i and j be two distinct pids. We add them to the opened QLOCK module using the command let.


  let op s : () -> State .
  let ops i, j : () -> PId .
              

Next, we specify that i and j are indeed distinct.


  let ax not i = j .
              

We also need to encode the hypothesis of the property that we intend to verify: the processes with ids i and j are both waiting, and none of them is at the front of the queue.


  let ax pc(PROCESS/State(i, PS/State(s))) = wt .
  let ax pc(PROCESS/State(j, PS/State(s))) = wt .
  let ax not head(seq(QUEUE/State(s))) = i nil .
  let ax not head(seq(QUEUE/State(s))) = j nil .
              

Finally, to verify the property, which we formalize using ~ to denote the behavioural equivalence of two states, we simply use the check command:


  check s >>= try(i) >>= try(j) ~ s >>= try(j) >>= try(i) .
              

The execution ends with the message Proved! The property holds. We can now close the module to conclude the analysis.