soma.cell

Hazmat bay segregation — Beirut, as a state that cannot exist

On 4 August 2020, 2,750 tonnes of ammonium nitrate stored for six years in Beirut's Hangar 12 — alongside a cache of FIREWORKS — detonated and killed more than 200 people. Tianjin 2015 was the same story: oxidizers warehoused next to fuels, 173 dead. Every dangerous-goods code on earth (IMDG, NFPA 400) says the same thing: oxidizers and flammables never share a bay. And in both cities, the paperwork said they did anyway. Here the bay is a state machine with NO mixed state: a bay holds oxidizer, or flammables, or nothing. The state "both" is not refused — it is unrepresentable, and soma verify proves the reachable set. A tonnage invariant caps any single bay at 25 t of oxidizer: a 2,750-tonne Hangar 12 cannot be written into this ledger at all.

A Soma program from the logistics part of the verified corpus: 120 lines, cells HazmatBay, Hangar12CannotHappen. It passes soma check with soma 3.2.4.

How it is built

The cell HazmatBay declares 4 signals in its face: stow_oxidizer(bay: String, tonnes: Int) -> Map, stow_flammable(bay: String, tonnes: Int) -> Map, clear_bay(bay: String) -> Map and bay_contents(bay: String) -> Map; keeps 2 memory slots: oxidizer_t (Map<String, Int>, persistent and consistent) under the invariant oxidizer_t >= 0 && oxidizer_t <= 25 and flammable_t (Map<String, Int>, persistent and consistent) under the invariant flammable_t >= 0 && flammable_t <= 25. Its state machine bay_class starts in empty and allows 4 transitions: empty -> holds_oxidizer, empty -> holds_flammable, holds_oxidizer -> empty and holds_flammable -> empty. Handlers: stow_oxidizer (line 45), stow_flammable (line 62), clear_bay (line 79) and bay_contents (line 87).

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

Generated from soma describe on this file.

The source

hazmat_bay_segregation.cell

// ============================================================================
// HAZMAT BAY SEGREGATION — Beirut, as a state that cannot exist
// ============================================================================
//
// On 4 August 2020, 2,750 tonnes of ammonium nitrate stored for six years
// in Beirut's Hangar 12 — alongside a cache of FIREWORKS — detonated and
// killed more than 200 people. Tianjin 2015 was the same story: oxidizers
// warehoused next to fuels, 173 dead. Every dangerous-goods code on earth
// (IMDG, NFPA 400) says the same thing: oxidizers and flammables never
// share a bay. And in both cities, the paperwork said they did anyway.
//
// Here the bay is a state machine with NO mixed state: a bay holds
// oxidizer, or flammables, or nothing. The state "both" is not refused —
// it is unrepresentable, and soma verify proves the reachable set.
// A tonnage invariant caps any single bay at 25 t of oxidizer: a
// 2,750-tonne Hangar 12 cannot be written into this ledger at all.
// ============================================================================

cell HazmatBay {
    face {
        signal stow_oxidizer(bay: String, tonnes: Int) -> Map
        signal stow_flammable(bay: String, tonnes: Int) -> Map
        signal clear_bay(bay: String) -> Map
        signal bay_contents(bay: String) -> Map
    }

    memory {
        // tonnes of oxidizer per bay — NFPA-style cap per storage bay
        oxidizer_t: Map<String, Int> [persistent, consistent]
        invariant oxidizer_t >= 0 && oxidizer_t <= 25

        flammable_t: Map<String, Int> [persistent, consistent]
        invariant flammable_t >= 0 && flammable_t <= 25
    }

    // There is no holds_both. That absence is the whole point.
    state bay_class {
        initial: empty
        empty           -> holds_oxidizer
        empty           -> holds_flammable
        holds_oxidizer  -> empty
        holds_flammable -> empty
    }

    on stow_oxidizer(bay: String, tonnes: Int) {
        if tonnes <= 0 { return map("ok", false, "reason", "tonnage must be positive") }
        if get_status(bay) != "holds_oxidizer" {
            let r = try { transition(bay, "holds_oxidizer") }
            if r.error != () {
                return map("ok", false, "reason", "SEGREGATION: bay {bay} holds flammables")
            }
        }
        let cur = oxidizer_t.get(bay) ?? 0
        let w = try { oxidizer_t.set(bay, cur + tonnes) }
        if w.error != () {
            if cur == 0 { transition(bay, "empty") }
            return map("ok", false, "reason", "bay cap is 25 t of oxidizer")
        }
        return map("ok", true, "bay", bay, "oxidizer_t", cur + tonnes)
    }

    on stow_flammable(bay: String, tonnes: Int) {
        if tonnes <= 0 { return map("ok", false, "reason", "tonnage must be positive") }
        if get_status(bay) != "holds_flammable" {
            let r = try { transition(bay, "holds_flammable") }
            if r.error != () {
                return map("ok", false, "reason", "SEGREGATION: bay {bay} holds an oxidizer")
            }
        }
        let cur = flammable_t.get(bay) ?? 0
        let w = try { flammable_t.set(bay, cur + tonnes) }
        if w.error != () {
            if cur == 0 { transition(bay, "empty") }
            return map("ok", false, "reason", "bay cap is 25 t of flammables")
        }
        return map("ok", true, "bay", bay, "flammable_t", cur + tonnes)
    }

    on clear_bay(bay: String) {
        oxidizer_t.set(bay, 0)
        flammable_t.set(bay, 0)
        let r = try { transition(bay, "empty") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "bay", bay)
    }

    on bay_contents(bay: String) {
        return map("bay", bay, "class", get_status(bay),
                   "oxidizer_t", oxidizer_t.get(bay) ?? 0,
                   "flammable_t", flammable_t.get(bay) ?? 0)
    }
}

cell test Hangar12CannotHappen {
    rules {
        // ammonium nitrate goes into bay H12 — top-ups of the SAME class are fine
        assert stow_oxidizer("H12", 15).ok == true
        assert stow_oxidizer("H12", 5).ok == true
        assert bay_contents("H12").oxidizer_t == 20
        assert bay_contents("H12").class == "holds_oxidizer"

        // THE BEIRUT WRITE: fireworks into the same bay. The machine has
        // no oxidizer->flammable path; the request is refused.
        assert stow_flammable("H12", 3).ok == false
        assert_fails transition("H12", "holds_flammable")
        assert bay_contents("H12").flammable_t == 0

        // and 2,750 tonnes can never be booked into one bay
        assert stow_oxidizer("H12", 2750).ok == false
        assert_fails oxidizer_t.set("H12", 2750)
        assert bay_contents("H12").oxidizer_t == 20

        // emptied and re-inspected, the bay may change class
        assert clear_bay("H12").ok == true
        assert stow_flammable("H12", 5).ok == true
        assert bay_contents("H12").class == "holds_flammable"
        assert stow_oxidizer("H12", 1).ok == false
    }
}

What the toolchain says

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

$ soma check hazmat_bay_segregation.cell
✓ All checks passed.
$ soma test hazmat_bay_segregation.cell
test Hangar12CannotHappen ...
  ✓ assert stow_oxidizer("H12", 15).ok == true
  ✓ assert stow_oxidizer("H12", 5).ok == true
  ✓ assert bay_contents("H12").oxidizer_t == 20
  ✓ assert bay_contents("H12").class == "holds_oxidizer"
  ✓ assert stow_flammable("H12", 3).ok == false
  ✓ assert_fails transition("H12", "holds_flammable") — raised invalid transition: holds_oxidizer → holds_flammable. Current state: 'holds_oxidizer'. Valid targets: [empty]
  ✓ assert bay_contents("H12").flammable_t == 0
  ✓ assert stow_oxidizer("H12", 2750).ok == false
  ✓ assert_fails oxidizer_t.set("H12", 2750) — raised memory invariant violated on 'oxidizer_t': oxidizer_t >= 0 && oxidizer_t <= 25 — rejected write of 2750 (key "H12"); the slot is unchanged
  ✓ assert bay_contents("H12").oxidizer_t == 20
  ✓ assert clear_bay("H12").ok == true
  ✓ assert stow_flammable("H12", 5).ok == true
  ✓ assert bay_contents("H12").class == "holds_flammable"
  ✓ assert stow_oxidizer("H12", 1).ok == false

14 tests: 14 passed, 0 failed
$ soma verify hazmat_bay_segregation.cell
Verifying hazmat_bay_segregation.cell...

State machine 'bay_class': 3 states, initial 'empty'
  States: [empty, holds_flammable, holds_oxidizer]

  ✓ 3 states, 4 transitions
  ✓ all states reachable from 'empty'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'empty'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'empty' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (3 handlers, 5 literal transitions, 0 dynamic)
  ✓ termination: all 4 handlers structurally terminate
  ✓ refinement: handler `stow_oxidizer` ⟶ {holds_oxidizer [if get_status(bay) != "holds_oxidizer"], empty [if w.error != () ∧ if cur == 0]}
  ✓ refinement: handler `stow_flammable` ⟶ {holds_flammable [if get_status(bay) != "holds_flammable"], empty [if w.error != () ∧ if cur == 0]}
  ✓ refinement: handler `clear_bay` ⟶ {empty}
  · transition sources: `stow_oxidizer`, `stow_flammable`, `clear_bay` 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)

HazmatBay/invariants:

  ✓ invariant oxidizer_t >= 0 && oxidizer_t <= 25 — writer 'clear_bay' proven (writes 0)
  ✓ invariant oxidizer_t >= 0 — writer 'stow_oxidizer' proven by induction (writes cur + tonnes)
  ⚠ invariant oxidizer_t >= 0 && oxidizer_t <= 25 — runtime-checked (computed values): stow_oxidizer → oxidizer_t [oxidizer_t <= 25] because `cur + tonnes` is only known to lie in [1, ∞) — narrow it: `require (cur + tonnes) <= 25 else …`
  ✓ invariant flammable_t >= 0 && flammable_t <= 25 — writer 'clear_bay' proven (writes 0)
  ✓ invariant flammable_t >= 0 — writer 'stow_flammable' proven by induction (writes cur + tonnes)
  ⚠ invariant flammable_t >= 0 && flammable_t <= 25 — runtime-checked (computed values): stow_flammable → flammable_t [flammable_t <= 25] because `cur + tonnes` is only known to lie in [1, ∞) — narrow it: `require (cur + tonnes) <= 25 else …`

14 passed, 2 warnings, 0 failures

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

Builtins used

get_status, map, transition

Source file: /corpus/logistics/hazmat_bay_segregation.cell · examples/corpus/logistics/hazmat_bay_segregation.cell