# TLA+ Traffic Lights and Communication Protocol Specification
Table of Contents
TLA+
Traffic Lights
In this example, we'll define a simple traffic light system with three phases (red, yellow, green). Each phase lasts for a certain number of time units (in seconds), and there is a transition time between phases. The system has three intersections, each controlled by a central controller. Here's the TLA+ specification:
// Globals
consts n = 3 // Number of intersections
consts redDuration = 20 // Duration of red phase in seconds
consts yellowDuration = 5 // Duration of yellow phase in seconds
consts greenDuration = 40 // Duration of green phase in seconds
consts transitionTime = 10 // Transition time between phases in seconds
// Types
type PhaseType = Red | Yellow | Green
type IntersectionID = 1..n
// Constants
consts allIntersections = 1..n
// Signals (state variables)
sig Controller, Light[i in allIntersections]
// Actions (functions that change the state)
act NextPhase()
{
// All lights go to next phase
for I in allIntersections do
Light[i]' = (Case Light[i],
Red => Yellow,
Yellow => Green,
Green => Red)
Controller' = (Controller, NextPhase(Controller))
}
act Initialise()
{
// Initialize signals to their initial values
for I in allIntersections do
Light[i] = Red
Controller = 0
}
// Constraints (properties that must always be true)
[Initialisation, Invariant]
inv InSynch ()
{
// All controllers have the same phase
for I in allIntersections do
Controller = Case Light[i],
Red => 0,
Yellow => 1,
Green => 2
}
// Initialisation action (runs once at the beginning)
act Initialisation()
{
// Runs Initialise() action
Initialise()
}
Communication Protocol
In this example, we'll define a communication protocol for a network with multiple nodes. Each node has a unique identifier, and messages are passed between nodes based on their contents and the current state of the network. The protocol includes message priority levels, message buffering, and error handling mechanisms.
Here's the TLA+ specification:
// Globals
consts n = 10 // Number of nodes in the network
consts maxMsgSize = 1024 // Maximum size of a message (in bytes)
consts numPriorities = 3 // Number of priority levels
// Types
type NodeID = 1..n
type Message[p in numPriorities] = {data: bytes, priority: p}
type ErrorType = Discarded | Buffered
// Signals (state variables)
sig NetworkState, Queue[i in allNodes], ErrorQueue[i in allNodes], PriorityBuffer[p in
numPriorities][i in allNodes]
// Actions (functions that change state)
act ReceiveMessage(from: NodeID, msg: Message)
{
// Add message to corresponding queue
Queue[from] ~= Append(msg, Queue[from])
}
act SendMessage(to: NodeID, msg: Message)
{
// Check if message can be sent immediately or needs buffering
let (p, buf) = ChooseBuffer(PriorityBuffer[[to][Msg.priority]])
if buf = None then
// Add message to NetworkState if buffer is full
NetworkState ~= Insert(to, msg)
else
// Add message to appropriate buffer
PriorityBuffer[[to][msg.priority]] ~= Append(msg, buf)
end
// Notify sender of successful transmission
Queue[from] ~= DeleteAtHead(Queue[from])
}
act ErrorHandling(src: NodeID, err: ErrorType)
{
// Add error to corresponding queue
let (NodeID, ErrorQueue) = ChooseErrorQueue(err)
ErrorQueue ~= Append({src, err}, ErrorQueue)
}
// Constraints (properties that must always be true)
[Invariant]
inv InSynch()
{
// NetworkState is a list of active connections
// Each connection is a pair of node IDs and message
// Messages are ordered by priority and node ID
for I in allNodes do
let (conn, queue) = ChooseConnection(NetworkState)
for j in 1..len(queue) do
conn ~= (i, Choose(queue))
for k in 1..j do
conn ~= Insert(Choose(queue), conn)
end
// Connection is ordered by node ID and message priority
for l in allNodes where l < i do
if NetworkState ~= None then
let (conn', queue') = ChooseConnection(NetworkState)
if Queue[l] ~= None and
(Queue[i][j].priority == Queue[l][jHead(queue')].priority or
Queue[i][j].priority > Queue[l][jHead(queue')].priority) then
// Swap connections based on message priority
NetworkState ~= Replace(conn, conn')
end
end
end
// Connection is ordered by message size and node ID
for m in allNodes where m < i do
if Queue[m] ~= None then
let (msg', msg) = ChooseMessage(Queue[i], Queue[m])
if MsgSize(msg) > MsgSize(msg') then
// Swap messages based on message size
Queue[i] ~= ReplaceAtHead(msg', Queue[i])
end
end
end
end
// All buffers are properly synchronized with NetworkState
for p in numPriorities do
for j in allNodes where j < i do
if PriorityBuffer[p][j] ~= None then
let (msg', msg) = ChooseMessage(PriorityBuffer[p][i], PriorityBuffer[p][j])
if MsgSize(msg) > MsgSize(msg') then
// Swap messages based on message size
PriorityBuffer[p][i] ~= ReplaceAtHead(msg', PriorityBuffer[p][i])
end
end
end
// All buffered messages are ordered by node ID and priority
let (msgList, buf) = ChooseBufferedMessages(PriorityBuffer[p][i])
for k in 1..len(msgList) do
let (src, err) = ChooseMessage(msgList[k])
if NetworkState ~= None then
let (conn', queue') = ChooseConnection(NetworkState)
if Queue[src] ~= None then
if Queue[i][jHead(Queue[i])].priority == msgList[k].priority or
Queue[i][jHead(Queue[i])].priority > msgList[k].priority then
// Swap messages based on message priority
NetworkState ~= Replace(conn, conn')
end
end
end
end
// All buffered messages are ordered by message size and priority
for l in allNodes where l < i do
if PriorityBuffer[p][l] ~= None then
let (msg', msg) = ChooseMessage(PriorityBuffer[p][i], PriorityBuffer[p][l])
if MsgSize(msg) > MsgSize(msg') then
// Swap messages based on message size
PriorityBuffer[p][i] ~= ReplaceAtHead(msg', PriorityBuffer[p][i])
end
end
end
end
end
}
// Helper functions
func ChooseConnection(list: List): Tuple[List, List] =
let (_, head) = Decompose(list)
return head, Tail(list)
func ChooseMessage(list: List): Tuple[Msg, Msg] =
let (msg', msg) = list.Decompose()
return msg', msg
func ChooseBufferedMessages(list: List): Tuple[List, List] =
let (head, tail) = Decompose(list)
if head == None then
return ([], tail)
else
return head, tail
// Error queues helpers
func ChooseErrorQueue(err: ErrorType): Tuple[NodeID, List] =
let (i, queue) = list.Decompose()
return i, queue
// Connection swapping helper
func Replace(oldConn: Tuple[NodeID, Msg], newConn: Tuple[NodeID, Msg]): List =
let (src, msg) = oldConn
NetworkState ~= ReplaceAtIndex(src, newConn.fst(), msg)
return NetworkState
// Message swapping helper
func ReplaceAtHead(oldMsg: Msg, newMsg: List): List =
let (msg', queue') = newMsg.Decompose()
Queue[i] ~= Insert(newMsg, DeleteAtHead(Queue[i]))
return Queue[i]
// Decomposes a list into its head and tail
func Decompose(list: List): Tuple[List, List] =
let (head', tail') = list.Decompose()
return head', tail'
// Extracts the first element of a list
func Head(list: List): Msg =
let (msg, _) = Decompose(list)
return msg
// Returns the number of elements in a list
func Len(list: List): Int =
if list == None then 0 else len(list)
// Computes the size of a message in bytes
func MsgSize(msg: Msg): Int =
// ...
// Returns the index at which an element should be inserted into a list
func InsertAtIndex(index: Int, item: Tuple[NodeID, Msg], list: List): List =
let (src, msg) = item
let (i, _) = NetworkState.Decompose()
// Calculate the index at which item should be inserted
let mut j = I - 1
while j >= 0 and MsgSize(NetworkState[j + 1].snd()) > MsgSize(msg) do
j--
return InsertAtIndexInternal(index, item, list, j)
// Helper function for InsertAtIndex
func InsertAtIndexInternal(index: Int, item: Tuple[NodeID, Msg], list: List, j: Int): List =
let (head', tail') = Decompose(list)
// If inserting at head or tail, return new list
if index == 0 then
NetworkState ~= InsertAtIndexInternalInternal(index, item, head', tail', j)
elseif index > len(list) then
NetworkState ~= ReplaceAtIndex(src, msg)
// Otherwise insert at desired position
else
// If item should be inserted before a connection, shift it down
if I == src and msg.priority <= NetworkState[j + 1].snd().priority then
let (conn, _) = NetworkState[j + 1]
NetworkState ~= ReplaceAtIndex(src, msg)
NetworkState ~= InsertAtHead(InsertAtIndexInternal(index - 1, item, list.Tail(), j))
// Otherwise insert item at desired position
else
NetworkState ~= InsertAtIndexInternalInternal(index, item, head', tail', j)
// Helper function for InsertAtIndexInternal
func InsertAtIndexInternalInternal(index: Int, item: Tuple[NodeID, Msg], list1: List, list2:
List, j: Int): List =
let (msg', queue') = item.Decompose()
let (head', tail') = Decompose(list1)
return InsertAtIndexInternalInternalInternal(index, msg', queue', list2, j)
// Helper function for InsertAtIndexInternalInternal
func InsertAtIndexInternalInternal(index: Int, item: List, list1: List, list2: List, j: Int): List =
let mut head' = list1
let mut tail' = Tail(tail1)
// If inserting at head or tail of list1, update NetworkState and return new list
if index == 0 then
NetworkState ~= InsertAtHead(item)
return concat(list2, tail')
elseif index > len(list1) + len(list2) then
// Otherwise insert item at desired position in list1 and return new list
let mut (head'', tail'') = Decompose(concat(list1.Take(index - 1), [item], list1.Drop(index
- 1)))
head'' ~= concat(head', Head(tail'))
tail'' ~= concat(tail', Tail(tail1))
return Tail(tail'')
else
// Otherwise insert item at desired position in list2 and return new list
let mut (head'', tail'') = Decompose(concat(list1, list2.Take(index - len(list1) - 1)))
head'' ~= concat(head', Head(tail'))
tail'' ~= concat(tail', Tail(tail1))
let mut (head''', tail''') = Decompose(concat(head'', item, list2.Drop(index - len(list1) -
1)))
head''' ~= concat(head'', Head(tail''))
tail''' ~= concat(tail'', Tail(tail''))
return Tail(tail''')