Verification¶
The Coco Platform formally verifies Coco components and ports for a broad range of properties, ranging from runtime errors through to advanced liveness properties. This section gives an overview of all of the properties verified for ports and components.
Ports¶
When verifying the correctness of a Coco port, the Coco Platform verifies whether the port state machine is well-formed, usable, and implements the base port correctly in the case that it inherits a base port.
Well-formedness Checks¶
A port state machine is well-formed precisely when:
- There are no runtime errors.
- It is responsive: The port state machine must always eventually allow another function call to be made. In other words, every handler must eventually terminate.
- In any state, an event is not legal and illegal.
- There are no infinite sequences of empty spontaneous transitions, where an empty spontaneous transition is one that does not send any signals, or alter the state.
In addition, the Coco Platform will give warnings if a port state machine has any:
- Unreachable states or transitions.
- Dead states.
These two checks are highlighted as warnings instead of errors, since they do not imply that a port is ill-formed. The developer can then review these warnings, and decide whether they are errors or not in the port specification.
Runtime Errors¶
The Coco Platform verifies whether a port is free from a range of runtime errors including:
- Calls to
abort
. - Calls to
assert
with afalse
argument (either as astate invariant
, or as a direct call toassert
). - The absence of a matching clause in any
match
expression. - The absence of at least one of the guards evaluating to true in any
nondet
expression. - Any
Bounded
overflows. - Any
Array
out of bounds. - Reading or loading an invalid value (i.e. a value that has not been written to yet).
Responsiveness¶
A port is responsive precisely when it always eventually allows another function call to be made. In other words, every handler must eventually terminate.
One common cause for failing this check is forgetting to transition out of an execution state correctly:
port P {
function begin() : Nil
outgoing signal status()
machine {
state Idle {
begin() = setNextState(Running)
}
execution state Running {
status();
setNextState(Running);
}
}
}
Once port P
enters the execution state Running
, it remains in this state forever because there are no calls on a
state transition function that lead to an event state where
function calls are allowed again.
This is illustrated in the following counterexample:
Illegal-Legal Overlap¶
In any state of a port state machine, an event cannot have two corresponding enabled
event transitions
where one of them has a legal event handler
, and
the other results in the illegal
handler. For example, the following snippet of Coco violates this property:
state S {
f() = illegal
f() = { return false; }
}
In state S
, function f()
is declared to be both illegal
, and legal where it returns false
.
Empty Spontaneous Transitions¶
A port state machine is not allowed to perform an infinite sequence of empty spontaneous transitions, where an empty spontaneous transition is one that does not send any signals, or alter the state.
Consider the following examples of ports that fail this verification check:
port P1 {
function f() : Nil
outgoing signal sig()
machine {
f() = {}
spontaneous = {}
}
}
port P2 {
function f() : Nil
outgoing signal sig()
machine {
state S1 {
spontaneous = setNextState(S1)
}
}
}
P1
and P2
can each perform an infinite sequence of empty spontaneous transitions without sending any
signals or altering the state.
The failure of this verification check is commonly caused by expressions in the spontaneous transition body that have erroneous guards, as illustrated by the following example:
When P2
fires the spontaneous transition in state S2
, it can nondeterministically choose between
sending the signal status(true)
, or if the value of allowed
is true
, sending the signal status(false)
.
However, allowed
is never assigned the value true
, which means that the second clause of the
nondet
expression is always empty. P2
can choose to perform an infinite sequence of the
empty nondet
clause, as illustrated by the following counterexample:
Reachability¶
When verifying ports, reachability information is computed to identify states or transitions that can never be reached. A state S is reachable in a state machine M precisely when there exists a sequence of events from the initial state of M to an instance of S. A transition T is reachable in a state machine M precisely when there exists a path from the initial state of M to a state where T can be executed.
Consider the following example of a simple port P
:
port P {
function begin() : Nil
function doSomething() : Nil
function stop() : Nil
outgoing signal error()
outgoing signal finished()
machine {
state Initial {
begin() = setNextState(Active)
}
state Active {
var isOk : Bool
if (isOk) doSomething() = {
}
if (!isOk) doSomething() = error()
if (isOk) stop() = setNextState(Stopping)
spontaneous = {
error();
isOk = false;
}
}
state Stopping {
spontaneous = finished()
}
}
}
Port P
fails the verification with a warning that it has unreachable states or transitions. When double-clicking on the
reachability assertion, a structured tabular view of the results is displayed in the Counterexamples view, and the
corresponding code for P
is highlighted to reflect the reachability information:
Starting with the structured tabular view, the states are listed in the first column, and are indented to reflect the state hierarchy. The top two rows list the function calls that can be called on components or ports, and in the case of components only, the signals that they can receive. For every event and state pair in the table, either the entry is blank, indicating that no transition is declared for this event in this state, or it has one of the following icons:
In port P
above, there is an example of each of these: in the state Initial
, the transition for the function begin()
is reachable (denoted by ), whereas the transition declared for stop()
in the state Active
is unreachable
(denoted by ). Further, there are
multiple transitions declared for the function doSomething()
in Active
, some of which are reachable and some are not
(denoted by ).
The reachability status of states is also displayed, where the red cells with the state identifiers displayed in italics
denote unreachable states, and the green cells denote reachable ones.
The code highlighting in the Coco Editor provides another view of the reachability results. In the case where there
are both reachable and unreachable transitions declared for an event in the same state (denoted by
in the structured view), the code highlighting identifies
precisely which ones are reachable and which ones are not. This is illustrated in the example above with the transitions
for the function doSomething()
in the state Active
.
To remove the highlighting in the Coco Editor, click on the large grey X in the top right hand corner of the Counterexamples view.
The Coco Platform also displays reachability warnings as a state machine diagram. For the example above, this results in the following diagram:
By default, unreachable states and transitions are flagged as warnings (instead of errors). These warnings can be
suppressed using the attribute @ignoreWarning(.Unreachable)
. If all unreachable states and transitions are
labelled with this attribute, then the assertion will pass the verification.
Dead States¶
A dead state is one where: i) every event transition
is either disabled or results in the illegal
handler, and ii) there are no enabled spontaneous transitions
that are
guaranteed to fire. The verification will find any dead states that are
reachable from the initial state of the port state machine, with the exception of
the built-in Terminated
state, which is not considered to be a dead state.
Consider the following example:
In state S
, port P
can perform the function f()
, resulting in count
being set to -1
. The state machine
remains in state S
, but now the guard evaluates to false. This means that f()
is illegal, and therefore the state
machine has reached a dead state.
By default, dead states are flagged as warnings rather than as errors. These warnings can be
suppressed using the attribute ignoreWarning(.DeadState)
:
This makes the verification engine ignore the dead state previously found, and this check now passes.
Usability Checks¶
Ports are often nondeterministic, and can have spontaneous transitions that result in state changes without any external stimuli from components using them. This means that it is possible to build a port that is potentially unusable by components.
A port P will fail this usability check precisely when: for some function call f() of P, there exists two sequences of events p1 and p2 from P’s initial state such that:
- p1 and p2 have the same sequence of visible function calls and signals from the viewpoint of a component using P;
- p1 leads to a state S1 where f() is illegal; and
- p2 leads to a state S2 where f() is permitted.
Consider the following example:
port P {
function f() : Nil
machine {
state S1 {
f() = setNextState(S2)
}
state S2 {
spontaneous = setNextState(S1)
}
}
}
Port P
starts in state S1
where it can perform the function call f()
and transition to state S2
.
In S2
, f()
is illegal; if a component using P
called f()
while P
is in state S2
, then
this will be caught as
a verification error by the well-formedness checks done on the component.
However, in state S2
, P
is able to perform a spontaneous transition, resulting in a state change
to S1
where f()
is allowed. From the viewpoint of a component C using P
, after
it has called f()
in state S1
, C cannot determine whether the port is still in state S2
,
or has in fact performed the spontaneous transition and returned to state S1
. As a result, C is not able
to determine whether it can call f()
again. This error is illustrated by the following counterexample:
This counterexample shows two sequences of events (labelled instance1
and instance2
) from the initial state
of P
that are indistinguishable from
the viewpoint of a component using P
, but that nonetheless lead to two
different states, one where f()
is permitted and one where f()
is illegal.
When a port fails this usability check, it is treated as a warning rather than an error, as it does not always
mean that the port is unusable. Consider the following extension to port P
above:
port P1 {
function f() : Nil
function isStable() : Bool
machine {
state S1 {
isStable() = true
f() = setNextState(S2)
}
state S2 {
isStable() = false
spontaneous = setNextState(S1)
}
}
}
P1
has an additional function isStable()
, which returns true
when called in state S1
and false
when called in state S2
. Although P1
will still fail the usability check, it is
possible to build a component that uses this port and is able to determine when it can call f()
correctly:
In this simple example, C
only calls f()
on its required port source
when source
is in state S1
where f()
is permitted.
The usability warning can be suppressed by annotating the port’s state machine with the @ignoreWarning
attribute as follows:
port P {
function f() : Nil
@ignoreWarning(.Usability)
machine {
state S1 {
f() = setNextState(S2)
}
state S2 {
spontaneous = setNextState(S1)
}
}
}
Implements the Base Port Specification¶
When a derived port with a state machine inherits one or more ports with state machines, then the Coco Platform will verify whether the behaviour specified in the derived port complies with that specified in each of the base ports it inherits. When this verification check passes, it means that if a component using one of these base ports as a required port passes all of its verification checks, then the component will also work correctly with the derived port substituted in as the required port. This assumes that the component would be connected to the derived port such that it can call the functions and receive the signals that are within scope of the base port.
Port inheritance requires the runtime to be specified for the
derived port using the attribute @compatible
. This affects the runtime that the
conformance is verified in, and a port labelled with @compatible
cannot be used in a component
with a different runtime.
Consider the following port:
port P1 {
outgoing signal status(b : Bool)
machine {
spontaneous = nondet {
status(true),
status(false),
}
}
}
P1
can repeatedly perform a spontaneous transition, which results in either sending status(true)
or
status(false)
nondeterministically.
Now consider the following example of a port that inherits P1
, and has two additional function declarations,
on()
and off()
, together with additional behaviour specified in its state machine:
@compatible(.MultiThreaded)
port P2 : P1 {
function on() : Nil
function off() : Nil
machine {
state Idle {
entry() = status(false)
on() = setNextState(Live)
}
state Live {
entry() = status(true)
off() = setNextState(Idle)
}
}
}
P2
has two states, Idle
and Live
. Idle
has an entry function
that sends the
signal status(false)
, and can perform the function on()
resulting in a state transition to Live
.
Live
also has an entry function
that sends the signal status(true)
, and can perform the
function off()
resulting in a state transition back to Idle
.
When verifying whether P2
complies with P1
, the verification fails with the following
counterexample:
P1
specifies that it will send an unbounded stream of signals, so any component using this port can rely on them
being sent. However, P2
is only guaranteed to send the first signal upon entry into the Idle
state. After that,
the occurrences of signals are dependent upon the functions on()
and off()
being called alternately, which are not
guaranteed to be called.
Port inheritance is transitive: if a port P1 inherits a port P2, and P2 inherits a port P3, then by definition P1 inherits P3. Therefore, a port may inherit another port indirectly via a chain of ports, where each port inherits the next port in the chain. Since not all of these ports will necessarily have state machines, the verification will identify whether a port inherits another port both directly and indirectly, where both ports have state machines, and verify whether they satisfy this property.
A port can be labelled as being compatible in both the single-threaded and multi-threaded runtime using the
@compatible
as illustrated in the following example:
port P1 {
outgoing signal status(b : Bool)
machine {
spontaneous = nondet {
status(true),
status(false),
}
}
}
@compatible(.MultiThreaded, .SingleThreaded)
port P2 : P1 {
machine {
spontaneous = {
status(false);
status(false);
}
}
}
The verification will check whether P2
complies with P1
in each runtime. In this case, P2
complies
with P1
in the multi-threaded runtime, but fails to do so in the single-threaded runtime, as illustrated by the
following counterexample:
P1
specifies that only one signal can be sent on the spontaneous transition, after which a drainQueue
event
must occur. In contrast, P2
sends two signals on a single spontaneous transition, which means that it does not
perform drainQueue
after each signal as required by P1
.
Components¶
A Coco component is verified within the context of its environment, namely the required ports that it is connected to and the specified runtime. When verifying the correctness of a Coco component, the Coco Platform checks that the component state machine is well-formed, and that it implements all of its provided ports correctly.
Well-Formedness Checks¶
A component state machine is well-formed precisely when:
- There are no runtime errors. These include the runtime error examples listed for ports, and some additional runtime errors such as the restrictions on single-threaded components or their required ports being violated.
- It is structurally deterministic: In every state, there is precisely
one
event handler
enabled for every event. - It uses its required ports correctly: The component only makes function calls when they are permitted by the required port’s state machine, and is able to handle all signals sent by its required ports.
- The number of signals received from its required ports does not exceed the component’s queue capacity.
- It is responsive: If a client attempts to make a function call on a component,
it will eventually be able to do so, and if a call is made on
unused()
, then the component will eventually terminate.
In addition, the Coco Platform will give a warning if the component state machine has any unreachable states or transitions when it is running in parallel with its required ports. This is classed as a warning instead of an error, since it does not imply that the component is ill-formed.
Structural Determinism¶
A component state machine is structurally deterministic precisely when, in every state, there is
precisely one matching enabled event transition
that can be executed for every event.
Consider the following example:
machine M {
var counter : Int = 0
state S {
if (counter <= 0) f() = { ok(); }
if (counter >= 0) f() = { fail(); }
}
}
This state machine violates this property, since in the case where counter
is set to 0
in state S
,
there are two enabled event transitions
for f()
due to the overlapping guards. This nondeterministic behaviour is not allowed in component state machines and is
therefore caught as a well-formedness error.
Using Required Ports Correctly¶
This property checks whether a component uses its required ports correctly, namely:
- A component only makes function calls on its required ports when it is permitted to do so, and
- A component is able to handle all of the return values, and signals it receives from its required ports.
The following example illustrates the case where a component makes a function call on a required port when it is not permitted to do so:
port P {
function begin() : Nil
function stop() : Nil
machine {
state Idle {
begin() = setNextState(Active)
}
state Active {
stop() = setNextState(Idle)
}
}
}
@queue(1)
@runtime(.MultiThreaded)
component C {
val client : Provided<P>
val source : Required<P>
machine {
state Idle {
client.begin() = {
source.begin();
setNextState(Active);
}
}
state Active {
client.begin() = source.begin()
client.stop() = {
source.stop();
setNextState(Idle);
}
}
}
}
C
has the required port source
, which allows the functions begin()
and stop()
to be called alternatively,
starting with begin()
. However, the component C
can call begin()
consecutively, which is not allowed:
The following example illustrates the case where a component receives a signal from a required port that it is not able to handle:
port P {
function begin() : Nil
outgoing signal error()
outgoing signal done()
machine {
@unreliable
spontaneous = error()
state Idle {
begin() = setNextState(Active)
}
state Active {
spontaneous = {
done();
setNextState(Idle);
}
}
}
}
@queue(1)
@runtime(.MultiThreaded)
component C {
val client : Provided<P>
val source : Required<P>
machine {
state Idle {
client.begin() = {
source.begin();
setNextState(Active);
}
}
state Active {
source.done() = {
client.done();
setNextState(Idle);
}
source.error() = client.error()
}
}
}
Component C
is only able to handle signals coming from its required port source
in state S2
; any
signals it receives in state S1
are illegal
since there are no transitions defined for them.
However, source
is able to spontaneously send signals in its initial state, and therefore cause
an error in C
, as illustrated in the following counterexample:
Similarly, components must also be able to handle all return values they receive from their required ports. In the event where they are not able to do so, this will be caught by this verification check.
Queue Capacity¶
This property checks whether a component’s required ports can send signals that exceed its queue capacity.
For example, if a component specifies @queue(2)
, and a required port is able to
spontaneously post 3 signals into the its queue, then this check will fail.
Responsiveness for Components with Clients¶
A component with clients is responsive precisely when: if a client attempts to make a function call on a component, it will eventually be able to do so. This implies that every handler will eventually terminate, and the component will infinitely often be in a state where its queue is empty.
A component is assumed to have clients unless a call to unused()
is made on it, indicating that it no longer has
any clients connected to it.
The following example illustrates how a component with clients can become unresponsive:
Component C
fails the responsiveness check because when it calls f()
on its required port, the required port
sends signal sig()
to C
before the function returns. When processing this signal, C
calls f()
again on
its required port, and the cycle repeats itself continuously, thereby preventing further function calls to be made
on C
:
Consider the case where C
is extended so that it emits a visible signal to its environment when it receives the
signal from its required port:
C
still fails this responsiveness check for the same reason as the previous example: Although it is able to send
signals across its provided port, it can still perform an unbounded sequence of events without
offering function calls to its client:
Termination Responsiveness¶
A component is responsive only when following a call to unused()
, it eventually enters the
Terminated state. By definition, a
call on unused()
means that the component no longer has any clients connected to it, and therefore will not receive
any function calls again.
The following example illustrates how a component can fail the responsive check because it does not guarantee
termination following a call to unused()
:
port POperation {
function execute() : Nil
outgoing signal finished()
machine {
state Idle {
execute() = setNextState(Running)
}
state Running {
spontaneous = {
finished();
setNextState(Idle);
}
}
}
}
@queue(2)
@runtime(.MultiThreaded)
component C {
val client : Provided<POperation>
val source : Array<Required<POperation>, 2>
machine {
state Idle {
client.execute() = {
source[0].execute();
source[1].execute();
setNextState(Running);
}
}
state Running {
var shouldTerm : Bool
var waitingFor : Bounded<0, 2> = .Last
source[_].finished() = {
waitingFor = waitingFor - 1;
if (waitingFor == 0) {
client.finished();
if (shouldTerm) {
waitingFor = .Last;
source[0].execute();
source[1].execute();
} else {
setNextState(Idle);
}
}
}
unused() = {
shouldTerm = true;
}
}
}
}
In this example, when execute()
is called on C
, it calls execute()
on each required port in its array, and
moves to the state Running
. In Running
, C
waits for the signal finished()
from each of them, after which it
sends the signal finished()
to its client, and returns to the state Idle
if the variable shouldTerm
is false
.
In state Running
, the C
declares an unused transition
that sets the value of
shouldTerm
to true
. This variable is used to determine when C
should terminate once it has received the
finish()
signals from all of its required ports.
The verification checks whether C
does eventually terminate following a call to unused()
. In this case, it
fails the responsiveness check with the following counterexample:
The counterexample illustrates how C
can perform an infinite sequence of events without terminating following a call
on unused()
in state Running
. Following this call, C
receives both finished()
signals from its required ports,
and then calls execute()
again on both of them again when shouldTerm
is true
, instead of terminating as expected.
This error can be resolved by replacing the event handler for source[_].finished()
in Running
to:
source[_].finished() = {
waitingFor = waitingFor - 1;
if (waitingFor == 0) {
client.finished();
if (shouldTerm) {
setNextState(Terminated);
} else {
setNextState(Idle);
}
}
}
See also
Termination section for details about Coco’s default termination behaviour.
Unused Transition section for details and examples of components with
unused transitions
.
Terminated State section.
Reachability¶
When verifying components, reachability information is computed to flag states or transitions that can never be reached in the component state machine and in all of the required port state machines that it is connected to. See Reachability for the definitions of a reachable state and transition.
Component Implements Its Provided Ports¶
If an implementation component has one or more provided ports, then the Coco Platform will verify whether the component correctly implements each of them independently of one another. If the component does not have any provided ports, then the Coco Platform will not generate an assertion for this particular verification check for the component, and instead the component will be verified for the well-formedness properties only.
When verifying whether a component implements one of its provided ports, the errors that can arise include the component:
- Performing an event that is not allowed by the provided port. This could be a function call, signal, or return value.
- Specifying that a function call is illegal in a state where the provided port specifies that it must be allowed.
- Not performing an event in a state that the provided port specifies it must do.
- Not satisfying the eventually constraints specified by the provided port.
- Terminating when the provided port specifies that it is not allowed to do so, or vice versa.
The following examples illustrate how some of these errors are displayed as counterexamples in the Coco Platform.
Example 1: Component fails to send a signal¶
Consider the following example of component that fails to send a signal when it is required to do so by its provided port:
port P0 {
function begin() : Nil
outgoing signal ready()
machine {
state Idle {
begin() = setNextState(Starting)
}
state Starting {
spontaneous = {
ready();
setNextState(Idle);
}
}
}
}
port P1 {
function begin() : Nil
outgoing signal ok()
outgoing signal fail()
machine {
state Idle {
begin() = setNextState(Running)
}
state Running {
@slow
spontaneous = nondet {
{
ok();
setNextState(Idle);
},
{
fail();
setNextState(Idle);
},
}
}
}
}
@queue(2)
@runtime(.MultiThreaded)
component C {
val client : Provided<P0>
val source : Required<P1>
machine {
state Idle {
client.begin() = {
source.begin();
setNextState(Running);
}
}
state Running {
source.ok() = {
client.ready();
setNextState(Idle);
}
source.fail() = {
source.begin();
setNextState(Running);
}
}
}
}
Component C
has the instance client
as its provided port, and must therefore implement the behaviour specified by P0
.
However, the verification finds the following error:
P0
specifies that after function begin()
is called, C
must send the signal ready()
.
As illustrated in the counterexample above, C
can perform an unbounded sequence
of events with its required port without sending ready
. This error is caused by the fact that the
required port source
can nondeterministically choose to repeatedly send the signal fail()
, which in turn causes
C
to call begin()
on source
again, and repeat the cycle.
In many cases, it may be reasonable to assume that the required port will eventually send the signal ok()
to
indicate that it has successfully completed the action. In this case, P1
can be amended to include the
@eventually
attribute on the corresponding spontaneous transition as follows:
port P1 {
function begin() : Nil
outgoing signal ok()
outgoing signal fail()
machine {
state Idle {
begin() = setNextState(Running)
}
state Running {
@slow
spontaneous = nondet {
@eventually
{
ok();
setNextState(Idle);
},
{
fail();
setNextState(Idle);
},
}
}
}
}
With this amendment, the component C
above now correctly implements its provided port.
Example 2: Component fails to satisfy the eventually constraint¶
This check also verifies whether the component satisfies the eventually constraint specified by its provided port. Consider the following example:
port P0 {
function check() : Nil
outgoing signal ready()
outgoing signal busy()
machine {
check() = nondet {
@eventually
ready(),
busy(),
}
}
}
port P1 {
function check() : Bool
machine {
check() = true
}
}
port P2 {
function check() : Bool
machine {
check() = nondet {
true,
false,
}
}
}
@queue(1)
@runtime(.MultiThreaded)
component C {
val client : Provided<P0>
val source1 : Required<P1>
val source2 : Required<P2>
machine {
state Idle {
client.check() = {
if (source1.check() && source2.check()) {
client.ready();
} else client.busy();
}
}
}
}
Component C
has client
as its provided port, which specifies that upon calling check()
an
arbitrary but finite number of times, C
must eventually send the signal ready()
.
When verifying whether C
implements its provided port correctly, the following counterexample is found
illustrating how C
violates the eventually constraint:
The cause of this error is that the required port source2
can nondeterministically decide to always return false
,
thereby causing C
to repeatedly send the signal busy()
without ever sending the signal ready()
as required
by its provided port.
Example 3: Component returns a value that is not allowed¶
When verifying whether a component implements it provided port correctly, the verification will also find errors where the component returns values that are not allowed by its provided port. Consider the following example:
external type Data
@derive(Default)
struct DataPacket {
var haveData : Bool
var data : Slot<Data>
}
port P {
function getData() : DataPacket
machine {
getData() = {
var packet : DataPacket;
nondet {
{
packet.haveData = false;
packet.data = .Invalid;
},
{
packet.haveData = true;
packet.data = .Valid;
},
}
return packet;
}
}
}
Port P
specifies the valid combinations of fields in a DataPacket
. If the field haveData
is
true
, then the field data
(of type Slot<Data>
) must contain a valid value. If the field haveData
is false
, then the field data
must not contain a valid value.
Consider the following component C
that is intended to implement this port:
Component C
implements the getData()
transition by calling the external function makeDataPacket(…)
to complete the
field data
, which is declared as follows:
external function makeDataPacket(d : &out DataPacket) : Nil = {
d.haveData = nondet {
true,
false,
};
}
The verification fails because C
does not implement its provided port P
correctly. The Coco Platform produces the
following counterexample:
The verification fails because in component C
, the function getData()
can return a DataPacket
in which the
field haveData
is true
and the field data
has no valid value. This is not allowed by its
provided port P
, which specifies that getData()
must return a DataPacket
where haveData
is true
and data
is valid, or haveData
is false
and data
is invalid. This error in the component arises because the external
function makeDataPacket(…)
does not ensure the data
field is valid.
Example 4: Component fails to implement either of its two provided ports¶
This example consists of a component with two provided ports, but fails to implement either of them. Consider the following two ports, which will be used as the provided ports for the component below.
port P1 {
function begin() : Nil
outgoing signal ok()
outgoing signal fail()
machine {
state Idle {
begin() = setNextState(Running)
}
state Running {
spontaneous = {
nondet {
ok(),
fail(),
};
setNextState(Idle);
}
}
}
}
port P2 {
function change() : Nil
machine {
change() = {}
}
}
Port P1
allows the function begin()
to be called in the Idle
state, which causes the state machine to transition
to state Running
. In Running
, the port will spontaneously send either the signal ok()
or fail()
, and return to
Idle
. Port P2
simply allows the function change()
to be called repeatedly in the same state.
Now consider the following component, which has instances of P1
and P2
as its provided ports, and therefore has to
implement both of them independently of one another:
@runtime(.MultiThreaded)
component C {
val client1 : Provided<P1>
val client2 : Provided<P2>
machine {
state Idle {
client1.begin() = setNextState(Running)
}
state Running {
after(milliseconds(1)) = {
client1.ok();
setNextState(Idle);
}
client2.change() = setNextState(Idle)
}
}
}
The component state machine starts in state Idle
, where the function begin()
can be called via its provided port
client1
causing the state machine to transition to state Running
. In Running
, the component can either fire the timer
transition, or it can receive a call on the function change()
via its other provided port client2
and transition back
to state Idle
.
When the verification is run, the assertions verifying whether the component implements its provided ports fail in both cases.
The following counterexample illustrates the case where the component fails to implement its provided port client1
:
After performing the function call begin()
, the component must send either the signal ok()
or fail()
according
to P1
. However, before the component can perform the timer transition to send signal ok()
, it receives the
call change()
via its other provided port client2
, and transitions back to Idle
where it cannot send any signals.
The following counterexample illustrates the case where the component fails to implement its provided port client2
:
When it is in the Idle
state, the component is not able to accept a call on change()
, which it must be able to
offer in all states according to its provided port P2
.
Example 5: Spontaneous transitions that are not assumed to eventually occur¶
During verification, Coco is able to make certain optimizations in the case where it is able to statically determine
whether a spontaneous transition in a port that is being used by a component results in visible behaviour (i.e. the sending
of a signal). Specifically, in this case it is able to automatically assume that these spontaneous transitions will eventually
fire without the user having to label them with the @eventually
attribute. For spontaneous transitions
that send signals directly within the transition itself, Coco is able to apply this optimization. However, in
the cases where spontaneous transitions do not send any signals or send them indirectly, Coco is not always able to
do so for all of the verification assertions, and in some cases may require users to label the spontaneous transitions
with @eventually
. Examples of how spontaneous transitions can send signals directly versus indirectly can
be found in Optimizing How Spontaneous Transitions Are Handled.
The following example is of a component that is dependent upon eventually receiving a signal in order to implement its provided port, but where the signal is sent indirectly resulting from the occurrence of a spontaneous transition:
port SignalInEntry {
function begin() : Nil
function doSomething() : Nil
outgoing signal ready()
machine {
state Init {
begin() = setNextState(Starting)
}
state Starting {
spontaneous = setNextState(Running)
}
state Running {
entry() = ready()
doSomething() = {}
}
}
}
port SendUpdates {
outgoing signal update()
machine {
@slow
spontaneous = update()
}
}
@queue(2)
@runtime(.MultiThreaded)
component Impl {
val client : Provided<SignalInEntry>
val source1 : Required<SignalInEntry>
val source2 : Required<SendUpdates>
init() = { setUnique(source2, SendUpdates.update); }
machine {
source2.update() = {}
state Init {
client.begin() = {
source1.begin();
setNextState(Starting);
}
}
state Starting {
source1.ready() = {
client.ready();
setNextState(Running);
}
}
state Running {
client.doSomething() = source1.doSomething()
}
}
}
The component Impl
has two required ports, SignalInEntry
and SendUpdates
, and has to implement SignalInEntry
as
its provided port. When verifying whether the component implements it provided port, the verification fails with the
following counterexample:
When the component has executed the function begin()
, its provided port specifies that it must send the signal
ready()
, whereas the component can perform an unbounded sequence of behaviour without sending the signal instead. The
component sends the signal ready()
to its client only when it receives the corresponding signal from its required port
SignalInEntry
. This means that the component is dependent upon SignalInEntry
eventually sending the ready()
signal
in order to satisfy its provided port specification.
Following the sequence of actions in the counterexample, the SignalInEntry
port is in state Starting
, where it can
only perform a spontaneous transition, which results in a state change to Running
and the sending of the signal
ready()
in its entry function. Further, the component can also receive an
unbounded sequence of update()
signals from its other required port SendUpdates
, which it simply ignores.
This is an example of where Coco is not able to
statically determine whether the spontaneous transition in SignalInEntry
results in the sending of a signal or not.
It therefore cannot assume that the spontaneous transition will eventually
occur when verifying a component that uses SignalInEntry
as its required port, because in the event that a spontaneous
transition does not result in any visible behaviour, then this assumption would be unsound when verifying certain classes
of properties.
To solve this, SignalInEntry
can either be modified to follow the recommended pattern where the signal is
sent directly within the spontaneous transition itself, instead of in the entry function of Running
:
state Starting {
spontaneous = {
ready();
setNextState(Running);
}
}
Alternatively, the the spontaneous transition can be labelled with the @eventually
attribute:
state Starting {
@eventually
spontaneous = setNextState(Running)
}
In the event that a spontaneous transition does not result any visible behaviour, or the signal is being sent indirectly
due a genuine need to factor out common code, then labelling the spontaneous transition with
@eventually
is the way to resolve this. When doing so, users must be confident that this is
the intended behaviour, particularly when specifying ports for external components.