TerminalLine is a small railway safety model written in Promela for the SPIN model checker.
The top layer is the physical railway and clockwise train flow. The lower layer is the signalling network: each signal box controls its local station and sends tunnel-release notifications to the box behind it.
flowchart TB
subgraph Railway["Physical railway: clockwise train flow"]
direction LR
S1["Station 1"] -->|"Tunnel_1_2"| S2["Station 2"]
S2 -->|"Tunnel_2_3"| S3["Station 3"]
S3 -->|"Tunnel_3_4"| S4["Station 4"]
S4 -->|"Tunnel_4_1"| S1
end
subgraph Signalling["Signalling: local control and rearward release"]
direction LR
B1["Signal box 1"] -->|"signalbox4_1"| B4["Signal box 4"]
B4 -->|"signalbox3_4"| B3["Signal box 3"]
B3 -->|"signalbox2_3"| B2["Signal box 2"]
B2 -->|"signalbox1_2"| B1
end
S1 -.->|"local request / response"| B1
S2 -.->|"local request / response"| B2
S3 -.->|"local request / response"| B3
S4 -.->|"local request / response"| B4
classDef station fill:#dbeafe,stroke:#2563eb,color:#111827;
classDef box fill:#dcfce7,stroke:#16a34a,color:#111827;
class S1,S2,S3,S4 station;
class B1,B2,B3,B4 box;
The railway has:
- four stations, numbered 1 to 4;
- four one-way tunnels forming a circle;
- trains that always move clockwise;
- one signal box controlling each station's departure signal.
The model starts with two trains: one at station 2 and one at station 4. Stations can hold trains. The safety rule concerns tunnels: at most one train may be inside any tunnel at any time.
In the Promela model, a tunnel is a buffered channel. A TRAIN message means
that a train is inside that tunnel. Tunnel capacity is deliberately two. If
the capacity were one, the channel itself would prevent SPIN from reaching the
collision state that we want to detect.
The model is in Source/TerminalLine.pml.
Each station observes only two physical events:
- Receiving
TRAINfrom its rear tunnel means that a train has arrived. - Sending
TRAINto its forward tunnel means that a train has departed.
When a station has a train but its signal is red, it sends REQUEST to its
local signal box. The box replies with either:
PROCEED, if the forward tunnel is free; orDO_NOT_PROCEED, if the forward tunnel is occupied.
When a train departs, the station sends DEPARTURE to its signal box. When a
train arrives, it sends ARRIVAL. The signal boxes pass a
TUNNEL_IS_EMPTY notification backwards around the circle so that the box
controlling the rear of that tunnel can release its reservation.
Local station/box links are rendezvous channels. Signal-box links are
two-place buffered channels so that an arrival notification does not create an
unintended circular communication blockage. The model has no atomic,
d_step, or timeout statements.
The current version keeps the original railway idea but makes its model and verification more reliable.
- The original one-shot assertion was replaced by a continuous monitor that asserts only when the unsafe condition is reached.
- The monitor does not remain continuously enabled in safe states, so it does not hide deadlocks.
- Safety is checked both by the monitor assertion and by LTL claim
p1.
p1is the direct safety property: every tunnel always contains fewer than two trains.p2is a directly observable progress property: eventually some train enters a tunnel.p4records requests explicitly and checks that every pending request is eventually answered, under weak process fairness.- The old idea of comparing transient channel contents for a liveness claim was removed. Channels are communication mechanisms, not stable request history variables.
- Inter-box channels are buffered with capacity two. This matches the two-train model and prevents notification traffic from forming an accidental rendezvous deadlock.
- The forbidden
atomicinitialization block was removed. SPIN now explores process creation as ordinary interleaved behavior. - Channel direction and the meaning of every message are documented directly in the model.
Compile with EXPANDED_VERIFY to enable additional verification-only state and
assertions:
block_reserved[4]records the reservation associated with an authorized departure;- station occupancy is checked against the two-train bound;
- duplicate pending requests are rejected;
- invalid station/box message types are rejected;
- departure and release events are checked for valid reservation order;
- LTL claim
p3checks that every occupied tunnel has a corresponding rear signal-box reservation.
This instrumentation does not choose protocol actions. It is intentionally optional because shared observation variables make the state space much larger and reduce the benefit of partial-order reduction.
These steps work on an Apple Silicon Mac such as an M1, M2, M3, or M4. For this project, macOS and Linux are the recommended platforms. Windows is supported, but WSL2 is the easiest Windows setup.
-
Install Git. If macOS asks for the Xcode Command Line Tools, choose Install. You can also start that installer from Terminal with
xcode-select --install. -
Install Homebrew if it is not already installed.
-
Open Terminal and install SPIN:
brew install spin
In Terminal, run:
cd ~/Documents
git clone https://github.com/nlintas/Verified-Safety-Railway-Promela-Spin-Model.git
cd Verified-Safety-Railway-Promela-Spin-Model
The repository contains the model at Source/TerminalLine.pml and the
clickable verifier at verify_on_macos.command.
Make the script executable once:
chmod +x verify_on_macos.command
Then either double-click verify_on_macos.command in Finder or run:
open verify_on_macos.command
macOS opens a Terminal window and runs the short verification suite. It checks
normal safety, deadlock/assertion behavior, initial movement, fair request
response, and bounded reservation consistency. The window reports ALL CHECKS PASSED when every check returns successfully. Before the verifier exits, it
also writes a timestamped Markdown report and the full log for every check to:
Verification Reports/YYYY-MM-DD_HH-MM-SS-PID/verification-report.md
The report records the model revision, SPIN version, host, commands, run times,
search method, states stored, transitions explored, maximum depth, error count,
memory use, the scope of each result, and a railway topology diagram. It
distinguishes the exhaustive checks from the deliberately sampled p4 bitstate
smoke test and the depth-bounded p3 consistency check. Generated reports are
ignored by Git so repeated local verification does not dirty the working tree.
The script automatically searches Apple Silicon Homebrew's usual location
/opt/homebrew/bin, so it also works when Finder does not load your normal
shell PATH. It removes temporary SPIN compiler files when it finishes.
If macOS blocks a script downloaded as a ZIP file, right-click it, choose Open, and confirm. A project cloned with Git normally avoids that warning.
Linux is the other recommended platform because SPIN and the generated C verifier fit naturally into the standard command-line toolchain.
The following commands are for Ubuntu or Debian:
-
Install Git, SPIN, and a C compiler:
sudo apt update sudo apt install git spin build-essential -
Download the project:
cd ~/Documents git clone https://github.com/nlintas/Verified-Safety-Railway-Promela-Spin-Model.git cd Verified-Safety-Railway-Promela-Spin-Model -
Confirm that SPIN is available:
spin -V -
Run the normal safety check:
spin -search -safety -bfs -ltl p1 Source/TerminalLine.pml -
Run the deadlock and assertion check:
spin -search -noclaim -m2000000 Source/TerminalLine.pml -
Run the initial movement property:
spin -search -ltl p2 Source/TerminalLine.pml -
Run the fair request-response regression:
spin -search -ltl p4 -f -DNOREDUCE -DBITSTATE -DNFAIR=4 -w20 -m20000 Source/TerminalLine.pml -
Run the bounded reservation-consistency check:
spin -DEXPANDED_VERIFY -search -safety -bfs -ltl p3 -m80 Source/TerminalLine.pml
Other Linux distributions can use their own package manager to install git,
spin, and a C compiler such as gcc. The verification commands after step 3
are the same.
Windows can run this project. The simplest Windows setup is WSL2, which gives you an Ubuntu terminal inside Windows. This is recommended over native PowerShell because the model's commands and SPIN's generated C verifier use a Unix-like shell and compiler. See Microsoft's WSL installation guide.
-
Open PowerShell as Administrator.
-
Install WSL and Ubuntu:
wsl --install -
Restart Windows if requested, then open Ubuntu from the Start menu.
-
Inside Ubuntu, install the required tools:
sudo apt update sudo apt install git spin build-essential -
Download the project inside Ubuntu:
cd ~ git clone https://github.com/nlintas/Verified-Safety-Railway-Promela-Spin-Model.git cd Verified-Safety-Railway-Promela-Spin-Model -
Confirm SPIN:
spin -V -
Run the checks manually, in this order:
spin -search -safety -bfs -ltl p1 Source/TerminalLine.pml spin -search -noclaim -m2000000 Source/TerminalLine.pml spin -search -ltl p2 Source/TerminalLine.pml spin -search -ltl p4 -f -DNOREDUCE -DBITSTATE -DNFAIR=4 -w20 -m20000 Source/TerminalLine.pml spin -DEXPANDED_VERIFY -search -safety -bfs -ltl p3 -m80 Source/TerminalLine.pml
Native Windows execution is also possible with the official Windows SPIN executable and a Unix-like build environment such as Cygwin or MSYS2 with a C compiler. The official SPIN installation documentation describes Windows executables and Cygwin workflows. Native setup is more variable, so WSL2 is the recommended Windows route for this repository.
Install SPIN on macOS, for example:
brew install spin
spin -V
Run the exhaustive safety check:
spin -search -safety -bfs -ltl p1 Source/TerminalLine.pml
Run the full DFS deadlock/assertion check:
spin -search -noclaim -m2000000 Source/TerminalLine.pml
The increased DFS depth is intentional. SPIN warns that BFS does not preserve all invalid end states when rendezvous channels are present.
Run the initial movement property:
spin -search -ltl p2 Source/TerminalLine.pml
Run a quick request/response fairness regression:
spin -search -ltl p4 -f -DNOREDUCE -DBITSTATE -DNFAIR=4 -w20 -m20000 Source/TerminalLine.pml
This is a fast bitstate bug-finding run, not an exhaustive proof. -f enables
weak fairness. -DNOREDUCE is required because SPIN cannot combine weak
fairness, partial-order reduction, and this model's rendezvous channels.
Run a short reservation-consistency regression:
spin -DEXPANDED_VERIFY -search -safety -bfs -ltl p3 -m80 Source/TerminalLine.pml
This command is intentionally bounded. The expected max search depth too small message means that the run stopped at depth 80; the important result is
that it reported zero errors before stopping. Remove -m80 for an exhaustive
p3 search when more time and memory are available.
Inspect one bounded execution trace:
spin -p -g -l -u120 -n7 Source/TerminalLine.pml
The final verify_on_macos.command run reported:
p1: 1,057,180 stored states, zero errors;- DFS deadlock/assertion search: 1,825,044 stored states, zero errors;
p2: 184 stored states, zero errors;- bounded
p3: 63,798 states through depth 80, zero errors; p4bitstate regression: 311,557 stored states, zero errors.
The p3 and p4 results are regression evidence, not exhaustive proof
results. The p1 and DFS results are the exhaustive checks for the normal
two-train model.
Number the stations modulo four. Let:
S_ibe stationi;T_ibe the tunnel fromS_itoS_{i+1};x_ibe the number ofTRAINmessages inT_i;n_ibe the number of trains currently counted at stationS_i;g_ibe the local signal value, wheretruemeans proceed;r_ibe the optional reservation observation forT_i.
The physical tunnel channels have capacity two, so:
[ 0 \leq x_i \leq 2. ]
The value x_i = 2 represents the collision state. Capacity two is therefore
part of the test design, not a safety guarantee.
The main safety predicate is:
[ SAFE \equiv (x_1 < 2) \land (x_2 < 2) \land (x_3 < 2) \land (x_4 < 2). ]
In the source this is the SAFE macro based on len(Tunnel_...).
The required safety statement is the invariant:
[ \Box SAFE ]
which means “SAFE is true in every reachable state.” SPIN checks this in two ways:
- The active monitor enables
assert(SAFE)only whenSAFEis false. - LTL claim
p1is[] SAFE.
The two checks are useful for different reasons. The assertion catches a bad state directly in the Promela transition system. The LTL claim checks the same invariant through SPIN's never-claim machinery.
For each tunnel T_i, the rear signal box keeps a logical free/occupied
state. A departure follows this protocol:
- A station requests permission.
- The signal box grants only when its outgoing block is free.
- The station reports
DEPARTURE. - The signal box marks the block occupied and returns
DO_NOT_PROCEED. - The station sends one
TRAINmessage into the tunnel. - The downstream station receives the message and reports
ARRIVAL. - The rear signal box receives
TUNNEL_IS_EMPTYand releases the block.
The station process is serialized: it cannot start a second departure while it is waiting for the response to the first one. Therefore, in the current two-train model, one station cannot issue two simultaneous authorities for the same tunnel.
SPIN still explores interleavings between individual Promela statements. This
is important: no atomic block hides intermediate states. The safety claim
must therefore hold not only at convenient protocol boundaries but throughout
all reachable interleavings.
When EXPANDED_VERIFY is enabled, p3 checks:
[ \Box \bigwedge_{i=1}^{4}(x_i=0 \lor r_i). ]
In words: if tunnel T_i is physically occupied, its rear signal box must
have a matching reservation observation. The property does not claim the
reverse implication because a reservation can legitimately exist briefly
while a departure is being completed or while a release notification is being
processed.
The expanded assertions also check local protocol obligations, such as:
- a station does not create a second pending request;
- a departure has a free block before claiming it;
- a release is not accepted for an unreserved block;
- only the message types valid on each channel are accepted.
Safety says that something bad never happens. Liveness says that something good eventually happens.
The initial progress claim p2 is:
[ \Diamond (x_1>0 \lor x_2>0 \lor x_3>0 \lor x_4>0). ]
It only proves that some train eventually enters a tunnel from the initial state. It does not prove that every train eventually moves or that trains circulate forever.
The request-response claim p4 is, for every station (i):
[ \Box(requestPending_i \Rightarrow \Diamond\neg requestPending_i). ]
The flag is set before a request and cleared after either PROCEED or
DO_NOT_PROCEED. Thus p4 proves eventual response, not eventual permission
to enter a tunnel. A stronger future property would distinguish denial from
service and require eventual PROCEED for a continuously waiting train.
Fairness matters for liveness. Without fairness, a scheduler could postpone
an enabled response forever even though the protocol itself is ready to send
it. The -f option adds weak process fairness for the fairness regression.
The bitstate result is useful for finding errors quickly but is not an
exhaustive proof.
For the current finite model, the exhaustive safety search proves tunnel collision freedom over all explored reachable states. The DFS search checks for assertion violations and invalid end states without an LTL claim.
The model does not yet prove all properties of a real railway. In particular, it does not model train identity, timing, braking distance, multiple blocks in one tunnel, bidirectional traffic, junction routes, signal failures, sensor failures, lost messages, or a strict station-capacity limit. Those require additional state variables, assumptions, and properties.
When adding trains or increasing channel capacities, re-check the inter-box buffer bound, fairness assumptions, and state-space size. For larger models, use exhaustive runs for small configurations and treat bitstate/swarm runs as bug-finding evidence unless their coverage is independently justified.