In this post, I describe the approach we (the Electrum team) follow to devise most of our models of state machines, protocols, etc. This is also to illustrate Electrum now that our release candidate is ready.
Most of this approach is not new as it is based upon work by Leslie Lamport in TLA+ and Daniel Jackson in Alloy. Our main addition is a convenient way to combine a form of modelling that is as expressive as the Alloy “events-as-predicates” idiom and as visualization-friendly as the “events-as-signatures” idiom. We dub it the “event reification” idiom (for want of a better name!).
Let’s consider the leader election protocol studied in Sect. 6.1 of Daniel’s book. Our version is a bit modified to make it more user-friendly, but the overall algorithm is the same. I will not enter into details but focus on the general pattern. Notice however that in this version, only one node does something at a time, for clarity (interleaving).
Structure
First, we describe the system structure. We have nodes with mutable inboxes and outboxes:
open util/ordering[Id]
sig Node {
id : one Id,
succ : one Node,
var inbox : set Id,
var outbox : set Id
}
sig Id {}
(some additional facts are needed to specify that nodes form a ring…).
Events
3 events are introduced as predicates. They may have:
- a guard saying when the event is allowed to happen
- an effect on some or all of the state variables (var sigs and fields)
- a frame condition saying what does not change.
One of the events (called skip
) does nothing. One of the reasons it is useful to consider it is that, foe some models, the execution of other events may stop. In that case, the execution trace would be finite. However, as Electrum only considers infinite traces, such traces would not be considered by Electrum and we could miss meaningful behaviors. With the skip
event, such finite traces can “become” infinite by performing skip
on the last state forever.
pred send [n : Node] {
some i : n.outbox {
// transmit message
...
}
// frame condition
all m : Node - n.succ | m.inbox' = m.inbox
all m : Node - n | m.outbox' = m.outbox
}
pred compute [n : Node] { ... }
pred skip {
inbox' = inbox
outbox' = outbox
}
Behaviors
We now specify the shape of behaviors. Remark the some
: in principle, several events may happen at the same time, but due to frame conditions, they will have the same effect and will therefore be indistinguishable.
fact behaviour {
// initial conditions
no inbox
...
// execution
always (skip or some n : Node | send[n] or compute[n])
}
Run
You may now run the Analyzer to look for an instance where some node is elected. Mind the steps
keyword that replaces Time
in Alloy and previous versions of Electrum. We ask for traces with as much as 6 steps, including the last step which loops back to a former state or itself.
pred example { eventually some elected }
run example for 3 but exactly 3 Node, 6 steps
Event Reification
Until now, we have followed the “events-as-predicates” idiom. For this reason, we can walk along the trace returned by the Analyzer but we must guess which event happened (in each state) as event names aren’t displayed.
Here comes event reification!
- add an enum with as many tags as there are events:
enum Event { Send, Compute, Skip }
- add functions for every such tag its possible parameter values (use the same name with an underscore in front of it, for instance):
fun _send : Event -> Node {
Send -> { n : Node | send[n] }
}
fun _compute : Event -> Node {
Compute -> { n : Node | compute[n] }
}
fun _skip : Event {
{ e : Skip | skip }
}
- give a name to the set of event instances:
fun events : set Event {
(_send + _compute).Node + _skip
}
- Now open the Visualizer and change the theme to your taste. In particular, to see which event(s) happen at a time, do the following:
- sig Event: Show = Off
- set $events: Show = On, Show as labels = Off
This way, you will see which events happen as well as their parameters (like $_send: Node0
in the picture below).
As explained above, several indistinguishable events may be displayed in a given state.
Hope this helps!