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

Author: Jason Walsh

j@wal.sh

Last Updated: 2024-10-30 16:43:54