# 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''')