A two-road traffic-light interlock proven safe by construction
The single machine cycles ns_go -> ns_caution -> ew_go -> ew_caution and back, with an all_red emergency reachable from anywhere. Because no state ever has both roads green, the model checker proves the safety property `never = []` over a machine that can never deadlock.
A Soma program from the state_machines part of the verified corpus: 62 lines, cells TrafficInterlock, TrafficTests. It passes soma check with soma 3.2.4.
How it is built
The cell TrafficInterlock declares 5 signals in its face: tick(id: String) -> String, emergency(id: String) -> String, clear(id: String) -> String, phase(id: String) -> String and next(id: String) -> List<String>. Its state machine lights starts in ns_go and allows 6 transitions: ns_go -> ns_caution, ns_caution -> ew_go, ew_go -> ew_caution, ew_caution -> ns_go, * -> all_red and all_red -> ns_go. Handlers: tick (line 27), emergency (line 40), clear (line 41), phase (line 42) and next (line 43).
The file also holds 1 test cell (TrafficTests) with 7 assertions, run by soma test.
The source
traffic_interlock.cell
// A two-road traffic-light interlock proven safe by construction.
// The single machine cycles ns_go -> ns_caution -> ew_go -> ew_caution
// and back, with an all_red emergency reachable from anywhere. Because no
// state ever has both roads green, the model checker proves the safety
// property `never = []` over a machine that can never deadlock.
cell TrafficInterlock {
face {
signal tick(id: String) -> String
signal emergency(id: String) -> String
signal clear(id: String) -> String
signal phase(id: String) -> String
signal next(id: String) -> List<String>
}
state lights {
initial: ns_go
ns_go -> ns_caution
ns_caution -> ew_go
ew_go -> ew_caution
ew_caution -> ns_go
* -> all_red
all_red -> ns_go
}
// advance one phase in the normal cycle
on tick(id: String) {
let cur = get_status(id)
let nxt = match cur {
"ns_go" -> "ns_caution"
"ns_caution" -> "ew_go"
"ew_go" -> "ew_caution"
"ew_caution" -> "ns_go"
_ -> "ns_go"
}
transition(id, nxt)
return get_status(id)
}
on emergency(id: String) { transition(id, "all_red") return get_status(id) }
on clear(id: String) { transition(id, "ns_go") return get_status(id) }
on phase(id: String) { return get_status(id) }
on next(id: String) { return valid_transitions(id) }
}
cell test TrafficTests {
rules {
// full normal cycle returns to the start
assert tick("x") == "ns_caution"
assert tick("x") == "ew_go"
assert tick("x") == "ew_caution"
assert tick("x") == "ns_go"
// emergency from any phase, then clear back to a green
assert emergency("y") == "all_red"
assert clear("y") == "ns_go"
// illegal: cannot jump green-to-green across roads (skips caution)
assert_fails transition("z", "ew_go")
}
}What the toolchain says
$ soma check traffic_interlock.cell
✓ All checks passed.$ soma test traffic_interlock.cell
test TrafficTests ...
✓ assert tick("x") == "ns_caution"
✓ assert tick("x") == "ew_go"
✓ assert tick("x") == "ew_caution"
✓ assert tick("x") == "ns_go"
✓ assert emergency("y") == "all_red"
✓ assert clear("y") == "ns_go"
✓ assert_fails transition("z", "ew_go") — raised invalid transition: ns_go → ew_go. Current state: 'ns_go'. Valid targets: [ns_caution, all_red]
7 tests: 7 passed, 0 failed$ soma verify traffic_interlock.cell
Verifying traffic_interlock.cell...
State machine 'lights': 5 states, initial 'ns_go'
States: [all_red, ew_caution, ew_go, ns_caution, ns_go]
✓ 5 states, 9 transitions
✓ all states reachable from 'ns_go'
✓ reactive machine: no terminal state, and every reachable state can return to 'ns_go'
✓ no deadlocks
✓ liveness: every reachable state can return to 'ns_go' (reactive machine)
✓ wildcard transitions: * -> [all_red]
⚠ dynamic transition targets: handler `tick` uses a dynamic (computed) transition target — the state these handlers move to is computed at run time (from input or an LLM answer), so the machine's safety is checked at run time for them, not proven
✓ termination: all 5 handlers structurally terminate
⚠ refinement (partial — dynamic targets fall back to runtime check): handler `tick` calls transition() with a computed target — which edge it takes is checked at run time (invalid_transition), not proven; write the target as a literal state to have it verified
✓ refinement (partial — dynamic targets fall back to runtime check): handler `tick` ⟶ <dynamic>
✓ refinement (partial — dynamic targets fall back to runtime check): handler `emergency` ⟶ {all_red}
✓ refinement (partial — dynamic targets fall back to runtime check): handler `clear` ⟶ {ns_go}
· transition sources: `emergency`, `clear` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
10 passed, 2 warnings, 0 failures
Temporal properties for 'lights':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK