soma.cell

Railway block signaling — two trains never share a block

Ladbroke Grove (1999, 31 dead) and a century of telescoped carriages before it happen when a second train enters an occupied block. Here the collision state is unrepresentable: a block is a state machine with exactly `clear` and `occupied`, and `occupied -> occupied` does not exist — the second grant RAISES instead of being a flag you forgot to check. A train-count ledger is the independent second wall: the write of a 2 is refused by the memory itself.

A Soma program from the safety_interlocks part of the verified corpus: 80 lines, cells BlockSignal, BlockTests. It passes soma check with soma 3.2.4.

How it is built

The cell BlockSignal declares 3 signals in its face: request_entry(block: String, train: String) -> Map, exit_block(block: String, train: String) -> Map and occupant_of(block: String) -> String; keeps 2 memory slots: train_count (Map<String, Int>, persistent and consistent) under the invariant train_count >= 0 && train_count <= 1 and holder (Map<String, String>, persistent and consistent). Its state machine block starts in clear and allows 2 transitions: clear -> occupied and occupied -> clear. Handlers: request_entry (line 31), exit_block (line 43) and occupant_of (line 53).

The file also holds 1 test cell (BlockTests) with 9 assertions, run by soma test.

Generated from soma describe on this file.

The source

railway_block.cell

// RAILWAY BLOCK SIGNALING — two trains never share a block.
// Ladbroke Grove (1999, 31 dead) and a century of telescoped carriages
// before it happen when a second train enters an occupied block. Here the
// collision state is unrepresentable: a block is a state machine with
// exactly `clear` and `occupied`, and `occupied -> occupied` does not
// exist — the second grant RAISES instead of being a flag you forgot to
// check. A train-count ledger is the independent second wall: the write
// of a 2 is refused by the memory itself.

cell BlockSignal {
    face {
        signal request_entry(block: String, train: String) -> Map
        signal exit_block(block: String, train: String) -> Map
        signal occupant_of(block: String) -> String
    }

    memory {
        // trains physically inside each block — never 0..1 violated
        train_count: Map<String, Int> [persistent, consistent]
        invariant train_count >= 0 && train_count <= 1

        holder: Map<String, String> [persistent, consistent]
    }

    state block {
        initial: clear
        clear -> occupied
        occupied -> clear
    }

    on request_entry(block: String, train: String) {
        // the interlock IS the transition: a held block raises here
        let r = try { transition(block, "occupied") }
        if r.error != () {
            let who = occupant_of(block)
            return map("granted", false, "reason", "block {block} held by {who}")
        }
        train_count.set(block, (train_count.get(block) ?? 0) + 1)
        holder.set(block, train)
        return map("granted", true, "block", block, "train", train)
    }

    on exit_block(block: String, train: String) {
        if occupant_of(block) != train {
            return map("ok", false, "reason", "{train} does not hold {block}")
        }
        transition(block, "clear")
        train_count.set(block, train_count.get(block) - 1)
        holder.delete(block)
        return map("ok", true, "block", block, "state", get_status(block))
    }

    on occupant_of(block: String) {
        return holder.get(block) ?? "nobody"
    }
}

cell test BlockTests {
    rules {
        // first train is granted the block
        assert request_entry("b7", "IC-100").granted == true
        assert occupant_of("b7") == "IC-100"

        // THE collision: a second train into the same block. The signal
        // logic refuses it, and the raw transition itself does not exist.
        assert request_entry("b7", "EXP-9").granted == false
        assert_fails transition("b7", "occupied")

        // the ledger is the second wall: writing 2 trains is refused
        // even if every signaling check above were deleted
        assert_fails train_count.set("b7", 2)
        assert_fails train_count.set("b7", -1)

        // only the holder can release; then the block cycles normally
        assert exit_block("b7", "EXP-9").ok == false
        assert exit_block("b7", "IC-100").ok == true
        assert request_entry("b7", "EXP-9").granted == true
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check railway_block.cell
✓ All checks passed.
$ soma test railway_block.cell
test BlockTests ...
  ✓ assert request_entry("b7", "IC-100").granted == true
  ✓ assert occupant_of("b7") == "IC-100"
  ✓ assert request_entry("b7", "EXP-9").granted == false
  ✓ assert_fails transition("b7", "occupied") — raised invalid transition: occupied → occupied. Current state: 'occupied'. Valid targets: [clear]
  ✓ assert_fails train_count.set("b7", 2) — raised memory invariant violated on 'train_count': train_count >= 0 && train_count <= 1 — rejected write of 2 (key "b7"); the slot is unchanged
  ✓ assert_fails train_count.set("b7", -1) — raised memory invariant violated on 'train_count': train_count >= 0 && train_count <= 1 — rejected write of -1 (key "b7"); the slot is unchanged
  ✓ assert exit_block("b7", "EXP-9").ok == false
  ✓ assert exit_block("b7", "IC-100").ok == true
  ✓ assert request_entry("b7", "EXP-9").granted == true

9 tests: 9 passed, 0 failed
$ soma verify railway_block.cell
Verifying railway_block.cell...

State machine 'block': 2 states, initial 'clear'
  States: [clear, occupied]

  ✓ 2 states, 2 transitions
  ✓ all states reachable from 'clear'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'clear'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'clear' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (2 handlers, 2 literal transitions, 0 dynamic)
  ✓ termination: all 3 handlers structurally terminate
  ✓ refinement: handler `request_entry` ⟶ {occupied}
  ✓ refinement: handler `exit_block` ⟶ {clear}
  · transition sources: `request_entry`, `exit_block` 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)

BlockSignal/invariants:

  ✓ invariant train_count <= 1 — writer 'exit_block' proven by induction (writes train_count.get(block) - 1)
  ✓ invariant train_count >= 0 — writer 'request_entry' proven by induction (writes (train_count.get(block) ?? 0) + 1)
  ⚠ invariant train_count >= 0 && train_count <= 1 — runtime-checked (computed values): exit_block → train_count [train_count >= 0] because `train_count` has no known range, request_entry → train_count [train_count <= 1] because `train_count` has no known range

11 passed, 1 warnings, 0 failures

  Temporal properties for 'block':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/safety_interlocks/railway_block.cell · examples/corpus/safety_interlocks/railway_block.cell