TLA+ Traffic Lights and Communication Protocol
Table of Contents
1. TLA+
The 2020 version of this page set out two exercises: a traffic light system with three phases (red, yellow, green), fixed phase durations and three intersections under a central controller; and a communication protocol for a network of nodes with message priority levels, message buffering and error handling.
The code that accompanied them was not TLA+. It used consts, type, sig,
act and // comments, closer to a C-flavoured pseudo-Alloy than to anything
SANY would parse, and the protocol "invariant" was a hundred lines of
imperative list surgery that never stated a property. It has been removed.
The exercises are kept and done properly below: each spec parses, and each was
run through TLC 2.19 (tla2tools.jar, 08 August 2024) on 2026-10-08.
To reproduce, save the module and its config side by side and run:
java -cp tla2tools.jar pcal.trans -nocfg PriorityProtocol.tla # PlusCal only
java -cp tla2tools.jar tlc2.TLC -workers auto TrafficLight
1.1. Traffic lights
TLC: 300 states generated, 128 distinct, depth 29, no error (TypeOK, InSynch, NoGreenToRed, AlwaysEventuallyGreen); guard removed gives InSynch counterexample at state 15
A controller holds the commanded phase and a countdown. Each intersection's light follows the controller asynchronously, as real actuation does. The controller's clock runs regardless; it may only advance to the next phase when every light has caught up. The 2020 sketch had a separate "transition time" between phases; here yellow is that transition, and an all-red clearance would be a fourth phase. Durations are abstract ticks, kept small so TLC can enumerate them.
---------------------------- MODULE TrafficLight ----------------------------
(* A central controller commands a phase; N intersection lights follow it
asynchronously. The controller's clock runs regardless, but it only
advances once every light has caught up. Time is in abstract ticks. *)
EXTENDS Naturals
CONSTANTS N, RedTicks, GreenTicks, YellowTicks
Intersections == 1..N
Phases == {"Red", "Green", "Yellow"}
NextPhase(p) == CASE p = "Red" -> "Green"
[] p = "Green" -> "Yellow"
[] p = "Yellow" -> "Red"
Duration(p) == CASE p = "Red" -> RedTicks
[] p = "Green" -> GreenTicks
[] p = "Yellow" -> YellowTicks
VARIABLES phase, timer, light
vars == <<phase, timer, light>>
TypeOK == /\ phase \in Phases
/\ timer \in 0..(RedTicks + GreenTicks + YellowTicks)
/\ light \in [Intersections -> Phases]
AllCaughtUp == \A i \in Intersections : light[i] = phase
Init == /\ phase = "Red"
/\ timer = Duration("Red")
/\ light = [i \in Intersections |-> "Red"]
Tick == /\ timer > 0
/\ timer' = timer - 1
/\ UNCHANGED <<phase, light>>
Advance == /\ timer = 0
/\ AllCaughtUp
/\ phase' = NextPhase(phase)
/\ timer' = Duration(NextPhase(phase))
/\ UNCHANGED light
Follow(i) == /\ light[i] # phase
/\ light' = [light EXCEPT ![i] = phase]
/\ UNCHANGED <<phase, timer>>
Next == Tick \/ Advance \/ \E i \in Intersections : Follow(i)
Spec == Init /\ [][Next]_vars /\ WF_vars(Next)
\* Every light shows the commanded phase or is exactly one step behind it.
InSynch == \A i \in Intersections : light[i] = phase \/ NextPhase(light[i]) = phase
\* No light goes straight from Green to Red.
NoGreenToRed == [][\A i \in Intersections :
light[i] = "Green" => light'[i] \in {"Green", "Yellow"}]_vars
\* Every light turns green infinitely often.
AlwaysEventuallyGreen == \A i \in Intersections : []<>(light[i] = "Green")
=============================================================================
TrafficLight.cfg, three intersections as in 2020:
CONSTANTS N = 3 RedTicks = 4 GreenTicks = 8 YellowTicks = 1 SPECIFICATION Spec INVARIANTS TypeOK InSynch PROPERTIES NoGreenToRed AlwaysEventuallyGreen
Model checking completed. No error has been found. 300 states generated, 128 distinct states found, 0 states left on queue. The depth of the complete state graph search is 29.
InSynch is the 2020 page's invariant of the same name, stated as a
predicate instead of a loop. To see that it is doing work, delete
/\ AllCaughtUp from Advance and rerun. TLC finds a 15-state behaviour in
which the controller ticks through all of red and green while no light has
moved:
Error: Invariant InSynch is violated. ... State 15: <Advance line 37, col 12 to line 40, col 29 of module TrafficLight> /\ light = <<"Red", "Red", "Red">> /\ timer = 1 /\ phase = "Yellow"
What the spec does not say: because the clock runs while lights lag, a slow
light can show green for fewer than GreenTicks ticks. Bounding that needs
either a per-light timer or a real-time module; neither is attempted here.
1.2. Communication protocol
TLC: 59424 states generated, 34896 distinct, depth 9, no error; receiver ignoring priority gives a PriorityRespected counterexample at state 4
Nodes send messages to each other through bounded buffers, one per
destination and priority. A receiver always takes the head of its most urgent
non-empty buffer (priority 1 is highest). Error handling is reduced to one
rule: a send into a full buffer is not lost silently, the message moves to a
dropped set, which stands in for the error report back to the sender. Each
message's route is left open in the initial state, so TLC
checks every assignment of source, destination and priority to the message
ids, loopback included.
This one is PlusCal; pcal.trans fills in the translation between the
BEGIN / END TRANSLATION markers.
-------------------------- MODULE PriorityProtocol --------------------------
EXTENDS Naturals, Sequences, FiniteSets
CONSTANTS Nodes, Priorities, Cap, MsgIds
ASSUME Priorities \subseteq Nat /\ Cap \in Nat /\ Cap > 0
Range(s) == {s[i] : i \in DOMAIN s}
(* --algorithm PriorityProtocol
variables
route \in [MsgIds -> [src : Nodes, dst : Nodes, pri : Priorities]],
pending = MsgIds,
buf = [n \in Nodes |-> [p \in Priorities |-> <<>>]],
delivered = [n \in Nodes |-> <<>>],
dropped = {};
define
Buffered(n) == UNION {Range(buf[n][p]) : p \in Priorities}
Top(n) == CHOOSE p \in Priorities :
/\ buf[n][p] # <<>>
/\ \A q \in Priorities : q < p => buf[n][q] = <<>>
Busy(n) == \/ \E m \in pending : route[m].src = n \/ route[m].dst = n
\/ Buffered(n) # {}
end define;
fair process node \in Nodes
begin
Loop:
while Busy(self) do
either
with m \in {x \in pending : route[x].src = self} do
pending := pending \ {m};
if Len(buf[route[m].dst][route[m].pri]) < Cap then
buf[route[m].dst][route[m].pri] :=
Append(buf[route[m].dst][route[m].pri], m);
else
dropped := dropped \union {m};
end if;
end with;
or
await Buffered(self) # {};
with p = Top(self) do
delivered[self] := Append(delivered[self], Head(buf[self][p]));
buf[self][p] := Tail(buf[self][p]);
end with;
end either;
end while;
end process;
end algorithm; *)
\* BEGIN TRANSLATION
\* END TRANSLATION
TypeOK == /\ pending \subseteq MsgIds
/\ dropped \subseteq MsgIds
/\ \A n \in Nodes : Range(delivered[n]) \subseteq MsgIds
BoundedBuffers == \A n \in Nodes, p \in Priorities : Len(buf[n][p]) <= Cap
\* Every message is in exactly one place: pending, buffered at its
\* destination, delivered at its destination, or dropped.
Conservation ==
\A m \in MsgIds :
LET d == route[m].dst IN
(IF m \in pending THEN 1 ELSE 0)
+ (IF m \in Buffered(d) THEN 1 ELSE 0)
+ (IF m \in Range(delivered[d]) THEN 1 ELSE 0)
+ (IF m \in dropped THEN 1 ELSE 0) = 1
NoDuplicates == \A n \in Nodes : Len(delivered[n]) = Cardinality(Range(delivered[n]))
\* A delivery never skips over a more urgent buffered message.
PriorityRespected ==
[][\A n \in Nodes :
Len(delivered'[n]) > Len(delivered[n]) =>
\A q \in Priorities :
q < route[delivered'[n][Len(delivered'[n])]].pri => buf[n][q] = <<>>]_vars
\* Eventually nothing is pending or buffered: every message is delivered or dropped.
Settles == <>[](pending = {} /\ \A n \in Nodes : Buffered(n) = {})
=============================================================================
PriorityProtocol.cfg, deliberately tight so the drop path is exercised:
CONSTANTS Nodes = {1, 2} Priorities = {1, 2} Cap = 1 MsgIds = {1, 2, 3}
SPECIFICATION Spec
INVARIANTS TypeOK BoundedBuffers Conservation NoDuplicates
PROPERTIES PriorityRespected Settles
Finished computing initial states: 512 distinct states generated Model checking completed. No error has been found. 59424 states generated, 34896 distinct states found, 0 states left on queue. The depth of the complete state graph search is 9.
The 512 initial states are the route assignments: 8 possible routes per message, three messages.
Replace with p = Top(self) by
with p \in {q \in Priorities : buf[self][q] # <<>>} (take any non-empty
buffer) and TLC reports PriorityRespected violated in four states: node 1
buffers message 1 at priority 1, then message 3 at priority 2, then delivers
message 3 first.
Two things the model leaves out. Strict priority can starve the low-priority
buffer when high-priority traffic never stops; with a finite MsgIds that
cannot happen, so Settles holds here and says nothing about it. And
dropped is a global set, not a message back to the sender, so a lost error
report is not modelled.
2. Since then
The blocks that sat here until 2026 looked like a specification and had never been near a parser. The fix is mechanical: a spec that SANY has not parsed and TLC has not run is prose.
2.1. Tooling
Linux Foundation press release dated 2023-04-21 names AWS, Oracle and Microsoft as founding members; lamport.azurewebsites.net/tla/tla.html says TLA+ "is now in the hands of the TLA+ Foundation" and names TLC and TLAPS as the primary tools
Stewardship passed to the TLA+ Foundation, a Linux Foundation project (press release, 21 April 2023) with AWS, Oracle and Microsoft as founding members.
The TLA+ home page now points there, names TLC and the TLAPS proof system as the primary tools, and lists a VS Code extension alongside the Toolbox.
Apalache is a symbolic model checker for TLA+ that translates to SMT (Z3, CVC5), which helps when explicit-state TLC runs out of room. For specs this size, a jar and a JVM are still all that is needed.
2.2. On this site
TLA+ for System Design is the long treatment: PlusCal, industry case studies, and an order state machine. Modeling Apache Redirect Rules in TLA+ and TLA+ for Build Pipeline Verification apply TLC to small, real configuration problems on this site. Alloy Specification Language covers the relational alternative and models the same redirect-termination question; Logic for Programmers: a tools comparison places TLA+ among nine verification tools by install cost and guarantee. The Four-Boundary Spec Mapping files TLA+ under the composition boundary, next to session types and Dafny; Dafny spec to clojure.spec Transpiler is the nearest route from a checked spec to runtime checks.