Note

You are not reading the most recent version of this documentation. 1.4.7 is the latest version available.

Lesson 9: Introducing Slots

The ValveHALBase component is currently specified to receive and store the valve parameters it needs in order to interface with the underlying hardware and carry out its movements. This assumes that the ValveHALBase has sufficient resources to be able to do this. In this lesson, we are going to make the changes needed to support a more primitive ValveHALBase component that does not implement a setParameters() function, and cannot store the valve parameters itself. Instead, these parameters will be supplied by its client, ValveHALBase, each time it calls the move functions on ValveHALBase.

To achieve this, we will need to extend the ValveHALBase component to handle and store values that are of an external type, namely of type ValveParameters, which was introduced in earlier lessons. In this lesson, we will introduce the builtin type Slot for this purpose, which can be used to allow some verification to be done on non-verifiable types, such as external types. In particular, slots can be used to check that a non-verified type is accessed correctly by the program, even if the actual value is not available.

Overview of Changes

In this lesson, we will change the ValveHALBase component into one that cannot store the valve parameters, but which requires them to be provided as a parameter on every call to move(). To do this, we will:

  1. Modify the ValveHAL port to remove the setParameters() function, extend the signature of move() with an additional parameter of type ValveParameters, and change the return type of move() from Nil to Bool. In its state machine, we will update the move() transition in state Idle to conform to the new signature, and return a value of type Bool to indicate whether or not the operation started. We assume that if the valve parameters passed to ValveHAL in the move() function are incorrect, then the move operation will not start.
  2. Modify the SimulatorImpl component to implement the changes to ValveHAL.
  3. Modify the ValveDriverImpl component to store the ValveParameters using slots, and conform to the new behaviour specified by ValveHAL, which it uses.

The changes to ValveHAL and SimulatorImpl are left as an exercise for you. The modified versions of ValveHAL is in Example_09B/src/ValveHAL.coco, and the modified version of SimulatorImpl is in Example_09B/src/SimulatorImpl.coco. Try to make the changes yourself before referring to these solutions.

We will now go through the changes required to the ValveDriverImpl component on the assumption that the ValveHAL port and SimulatorImpl component have been correctly updated as described above and in Example_09B.

Step 1: Modifying ValveDriverImpl

When we examine ValveDriverImpl in Example_09B, we can see that it has two static errors. Firstly, it calls hal.setParameters() on its required port, which has been removed from the ValveHAL port. Secondly, it calls hal.move() on ValveHAL with the old signature and return type.

There is an additional problem to be addressed: in its current implementation, ValveDriverImpl does not report any errors resulting from a call to move the valve. This is because the previous version of the ValveHAL port assumed that valve movements always succeeded. In the new version of ValveHAL this is no longer the case, as the move() function now returns a boolean value to indicate whether the move request succeeded or not depending on the valve parameters it received.

To implement these changes in the ValveDriverImpl component, we will modify its state machine to:

  1. Store the valve parameters it receives from its client (via the setup() function), and pass them as parameters on every call to move().
  2. Handle the case where the move() call can fail, either because the valve parameters are incorrect, or for some other reason, and report these failures on to its client.

Storing the Valve Parameters

The ValveDriverImpl component will receive the valve parameter values from its client via the setup() function, store them, and then pass them on when it makes calls on ValveHAL to move the value.

The first step is to declare a variable of type ValveHAL.ValveParameters to store these parameter values when it receives them.

Note

If a variable is declared to be of some type T that does not have an instance of Default, then the variable declaration must either assign an initial value as part of its declaration, or it can be declared as being of type Slot<T>, which means that the verification will check whether the variable is being accessed correctly or not. See variable declaration for further details.

ValveParameters is an external type, which means that it is not verifiable, and not defaultable. We can specify that a custom external type is defaultable by declaring an instance of Default, but we will assume that there is no such instance declared for ValveParameters in this example. Further, the ValveDriverImpl is not able to assign an initial value as part of its variable declaration, as it does not have a valid value until its client calls the setup() function. This means that we cannot declare the variable as follows:

var params : ValveHAL.ValveParameters

You can try adding this declaration to your ValveDriverImpl state machine, and see the corresponding error message reported by the Coco Platform.

We are therefore going to declare a variable using slots.

Note

The type Slot<T> is defined as an alias for Abstracted<T, SlotStatus>, and can be used to enable some verification even on non-verified types, such as external types. In particular, if a variable is declared as of type Slot<T>, then the verification will instead check that the slot is only read when a value has been stored in it. It can therefore be used to check that a non-verified type is accessed correctly by the program, even if the actual value is not available.

During verification, Coco uses the abstracted type SlotStatus for all occurrences of Slot<T> to verify whether or not an instance of Slot<T> holds a valid value without knowing or storing the value itself. In the generated code, Slot<T> is replaced by T. By default, all instances of Slot<T> are invalid unless specified otherwise. The verification will find any attempts to read or load an invalid value, and provide a counterexample to highlight the error. See Slot for further details.

We achieve this by adding the following declaration to our ValveDriverImpl state machine:

    var params : Slot<ValveHAL.ValveParameters>

The next step is to modify the setup() transition in the Initial state as follows:

      client.setup(vParms : ValveHAL.ValveParameters) = {
        setNextState(Ready);
        true
      }

Due to the change in ValveHAL, setup() will never fail. In order to avoid changing its interface to ValveHALBase, we therefore return true.

Updating the Call to move()

The call to move() needs to be updated to include the valve parameters as an additional parameter, and return a boolean indicating whether or not the call was carried out. To accommodate this, we will start by reorganising the Ready and Moving states.

Currently, the Moving state is as follows:

    state Moving(cmd : ValveHAL.Cmd) {
      entry() = hal.move(cmd)
      client.terminate() = illegal
      periodic(milliseconds(500)) = if (!hal.isMoving()) setNextState(Ready)

      exit() = {
        client.endOfMovement(match (cmd) {
          ValveHAL.Cmd.Open => ValveDriver.ValveStatus.Open;
          ValveHAL.Cmd.Close => ValveDriver.ValveStatus.Closed;
        });
      }
    }

The call to hal.move() is made in the entry function. After the changes we made to the ValveHAL port, hal.move() can fail to start. We do not want to enter the Moving state unless the move() has been accepted, and the movement has started. This means that we must call hal.move() in the Ready state instead, and only enter the Moving state if the call has been accepted.

The transition handlers for open() and close() in the Ready state need to be modified to call hal.move(), check the result, and only enter Moving if the movement has started. Otherwise, they should send the endOfMovement() signal indicating the failure and remain in the Ready state. To make this change, we will declare a function in the Ready state called perhapsMove() as follows:

      function perhapsMove(cmd : ValveHAL.Cmd) : Nil = {
        if (hal.move(cmd, params)) setNextState(Moving(cmd)) else client.endOfMovement(ValveDriver.ValveStatus.Error)
      }

This function calls hal.move(). If it is accepted, it changes state to Moving; otherwise it sends the endOfMovement(ValveStatus.Error) signal, and stays in the same state.

The next step is to modify the open() and close() transitions in Ready state to call the new local function as follows:

      client.open() = perhapsMove(ValveHAL.Cmd.Open)
      client.close() = perhapsMove(ValveHAL.Cmd.Close)

The final step is to delete the entry function from the Moving state. After these changes, the ValveDriverImpl state machine should look like the following:

  machine M {
    var params : Slot<ValveHAL.ValveParameters>

    client.terminate() = setNextState(Terminated)

    state Initial {
      client.setup(vParms : ValveHAL.ValveParameters) = {
        setNextState(Ready);
        true
      }
    }
    /// Valve ready to move
    state Ready {
      client.open() = perhapsMove(ValveHAL.Cmd.Open)
      client.close() = perhapsMove(ValveHAL.Cmd.Close)
      function perhapsMove(cmd : ValveHAL.Cmd) : Nil = {
        if (hal.move(cmd, params)) setNextState(Moving(cmd)) else client.endOfMovement(ValveDriver.ValveStatus.Error)
      }
    }
    /// Asynchronously execute the synchronous valve movement
    state Moving(cmd : ValveHAL.Cmd) {
      client.terminate() = illegal
      periodic(milliseconds(500)) = if (!hal.isMoving()) setNextState(Ready)

      exit() = {
        client.endOfMovement(match (cmd) {
          ValveHAL.Cmd.Open => ValveDriver.ValveStatus.Open,
          ValveHAL.Cmd.Close => ValveDriver.ValveStatus.Closed,
        });
      }
    }
  }

The modified Coco for this component is in Example_09C/src/ValveDriverImpl.coco.

Note

Coco is warning us that the variable params never has a value assigned to it and the parameter to the setup() is never used. We are choosing to ignore this in order for the verification to discover that params had no valid value assigned to it.

Verifying your Changes (Example_09C)

The verification finds a runtime error in the ValveDriverImpl component with the following counterexample:

Exception counterexampleUserValveDriverImplhalsetup(<unavailable>)return trueopen()An assertion failed: value was not written to before being loaded.

Analysis

The counterexample shows the setup() function being called on ValveDriverImpl, followed by a call to open(). The open() call generates an error because it is passing params as an input parameter to the hal.move() call without ever having written a value to it. Looking at the state machine, we can indeed see that when the setup() function is executed in the Initial state, its argument is not assigned to params:

      client.setup(vParms : ValveHAL.ValveParameters) = {
        setNextState(Ready);
        true
      }

Therefore, params does not have a valid value at the point where ValveDriverImpl is attempting to read it when it calls hal.move() with the value of params as its argument. Coco did try to tell us this with the warning that params is read but never written to!

Assigning a Value to params

To fix this runtime error, we need to modify the setup() transition handler to capture the input parameter vParms as follows:

      client.setup(vParms : ValveHAL.ValveParameters) = {
        params = vParms;
        setNextState(Ready);
        true
      }

The modified Coco for this component is in Example_09D/src/ValveDriverImpl.coco.

Verifying your Changes (Example_09D)

The verification fails for the assertion that checks whether the ValveHALBase component implements its provided port ValveHAL correctly, and produces the following counterexample:

Trace counterexampleclientUserValveDriverImplhalsetup(<unavailable>)setup(<unavailable>)[nondet] τ Para...eters are validreturn truereturn trueopen()open()move(Cmd.Open, <unavailable>)[nondet] τ Move...failed to startreturn falseendOfMovement(V...veStatus.Error) The last event performed by the component was not allowed by the provided port, which expected one of: return nil

Analysis

The ValveDriverImpl component was called to open() the valve, and in turn calls hal.move() on its required port. The hal.move() call was rejected for some reason, and returns false. As a result, the ValveDriverImpl synchronously sends the endOfMovement() signal, which is not allowed by its provided port, ValveDriver.

Examining ValveDriver, we see the following two issues:

  1. It specifies that the endOfMovement() signal must occur after the open() function returns.
  2. ValveStatus.Error is not one of the values allowed in the signal.

To solve this, we need to modify ValveDriver so that both the open() and close() transitions allow the ValveDriverImpl to signal the failure before the functions return to the caller.

Step 2: Modifying ValveDriver

Modify the open() and close() transitions in Ready as follows:

    state Ready {
      open() = nondet {
        @represents("Movement started")
        setNextState(Moving(ValveStatus.Open)),
        @represents("Movement failed to start")
        endOfMovement(ValveStatus.Error),
      }
      close() = nondet {
        @represents("Movement started")
        setNextState(Moving(ValveStatus.Closed)),
        @represents("Movement failed to start")
        endOfMovement(ValveStatus.Error),
      }
    }

Both transitions now make a nondeterministic choice between starting the movement and changing state to Moving, or synchronously signalling a failure and remaining in the Ready state.

The modified Coco for this component is in Example_09E/src/ValveDriver.coco.

Verifying your Changes (Example_09E)

All of the assertions for the components and ports now pass the verification.

See also

See the description of slots for further details and examples. Another more complex example using slots can also be found in the verification section.