soma.cell

Dialysis — fluid removal is capped, and a session always ends with the

patient reconnected to their own blood In hemodialysis the machine pulls a patient's blood through a filter and returns it, removing excess fluid (ultrafiltration) along the way. Two ways it harms: removing too much fluid too fast crashes blood pressure (intradialytic hypotension — nausea, cramps, cardiac arrest in the frail); and a session ABORTED without rinse-back strands the patient's blood in the extracorporeal circuit — a real loss of ~200+ mL. Here the ultrafiltration total is a bounded ledger: it can never exceed the prescribed fluid removal goal, enforced by the memory invariant — the pump cannot pull more than the body can spare. And the session machine has NO path from `running` straight to `complete`: it must pass through `rinse_back`, so the blood is always returned. `soma verify` proves that path is the only way to finish.

A Soma program from the medical part of the verified corpus: 148 lines, cells Dialysis, DialysisTests. It passes soma check with soma 3.2.4.

How it is built

The cell Dialysis declares 7 signals in its face: begin(session: String, goal_ml: Int) -> Map, ultrafiltrate(session: String, ml: Int) -> Map, rinse_back(session: String) -> Map, finish(session: String) -> Map, removed(session: String) -> Int, session_state(session: String) -> String and demo() -> String; keeps 2 memory slots: uf_total (Map<String, Int>, persistent and consistent) under the invariant uf_total >= 0 && uf_total <= 5000 and uf_goal (Map<String, Int>, persistent and consistent) under the invariant uf_goal >= 0 && uf_goal <= 5000. Its state machine session starts in prepped and allows 4 transitions: prepped -> running, running -> running, running -> rinse_back and rinse_back -> complete. complete is terminal. Handlers: begin (line 58), ultrafiltrate (line 69), rinse_back (line 87), finish (line 97), removed (line 105), session_state (line 106) and demo (line 108).

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

Generated from soma describe on this file.

The source

dialysis.cell

// ============================================================================
// DIALYSIS — fluid removal is capped, and a session always ends with the
// patient reconnected to their own blood
// ============================================================================
//
// In hemodialysis the machine pulls a patient's blood through a filter and
// returns it, removing excess fluid (ultrafiltration) along the way. Two
// ways it harms: removing too much fluid too fast crashes blood pressure
// (intradialytic hypotension — nausea, cramps, cardiac arrest in the
// frail); and a session ABORTED without rinse-back strands the patient's
// blood in the extracorporeal circuit — a real loss of ~200+ mL.
//
// Here the ultrafiltration total is a bounded ledger: it can never exceed
// the prescribed fluid removal goal, enforced by the memory invariant —
// the pump cannot pull more than the body can spare. And the session
// machine has NO path from `running` straight to `complete`: it must pass
// through `rinse_back`, so the blood is always returned. `soma verify`
// proves that path is the only way to finish.
//
//   soma verify examples/corpus/medical/dialysis.cell
//   soma test   examples/corpus/medical/dialysis.cell
//   soma run    examples/corpus/medical/dialysis.cell demo
// ============================================================================

cell Dialysis {
    face {
        signal begin(session: String, goal_ml: Int) -> Map
        signal ultrafiltrate(session: String, ml: Int) -> Map
        signal rinse_back(session: String) -> Map
        signal finish(session: String) -> Map
        signal removed(session: String) -> Int
        signal session_state(session: String) -> String
        signal demo() -> String
    }

    memory {
        // Cumulative fluid removed, in mL. The hard ceiling is the safe
        // ultrafiltration envelope; the pump physically cannot be told to
        // exceed it because the ledger refuses the write.
        uf_total: Map<String, Int> [persistent, consistent]
        invariant uf_total >= 0 && uf_total <= 5000

        // Per-session prescribed goal.
        uf_goal: Map<String, Int> [persistent, consistent]
        invariant uf_goal >= 0 && uf_goal <= 5000
    }

    // A session must end through `rinse_back` — there is no running ->
    // complete edge. The blood is always returned. `complete` is terminal.
    state session {
        initial: prepped
        prepped    -> running
        running    -> running          // each UF increment
        running    -> rinse_back       // returning the patient's blood
        rinse_back -> complete         // safe end — terminal
    }

    on begin(session: String, goal_ml: Int) {
        let w = try { uf_goal.set(session, goal_ml) }
        if w.error != () { return map("ok", false, "reason", "{w.error}") }
        uf_total.set(session, 0)
        let r = try { transition(session, "running") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "goal", goal_ml, "state", get_status(session))
    }

    // Remove fluid in increments. Refused past the prescribed goal AND, as
    // an independent wall, refused by the ledger past the 5 L safety cap.
    on ultrafiltrate(session: String, ml: Int) {
        let st = get_status(session)
        if st != "running" {
            return map("ok", false, "reason", "session is {st}, not running")
        }
        if ml <= 0 { return map("ok", false, "reason", "removal must be positive") }
        let sofar = uf_total.get(session) ?? 0
        let goal = uf_goal.get(session) ?? 0
        if sofar + ml > goal {
            return map("ok", false, "reason",
                "would exceed UF goal: {sofar} + {ml} > {goal} mL")
        }
        let w = try { uf_total.set(session, sofar + ml) }
        if w.error != () { return map("ok", false, "reason", "ledger refused: {w.error}") }
        transition(session, "running")
        return map("ok", true, "removed", sofar + ml, "goal", goal)
    }

    on rinse_back(session: String) {
        let r = try { transition(session, "rinse_back") }
        if r.error != () {
            return map("ok", false, "reason", "cannot rinse back: {r.error}")
        }
        return map("ok", true, "state", get_status(session))
    }

    // Finishing requires having rinsed back — the machine refuses to
    // complete a session whose blood is still in the circuit.
    on finish(session: String) {
        let r = try { transition(session, "complete") }
        if r.error != () {
            return map("ok", false, "reason", "blood not returned: {r.error}")
        }
        return map("ok", true, "state", get_status(session))
    }

    on removed(session: String) { return uf_total.get(session) ?? 0 }
    on session_state(session: String) { return get_status(session) }

    on demo() {
        begin("s-1", 2000)
        ultrafiltrate("s-1", 1500)
        let over = ultrafiltrate("s-1", 800)   // 1500+800 > 2000 — REFUSED
        let early = finish("s-1")              // blood not rinsed — REFUSED
        rinse_back("s-1")
        let done = finish("s-1")
        let total = removed("s-1")
        print("over-pull:  {over}")
        print("early end:  {early}")
        print("clean end:  {done}")
        print("removed mL: {total}")
        return "demo done"
    }
}

cell test DialysisTests {
    rules {
        assert begin("a", 1000).ok == true
        assert ultrafiltrate("a", 600).ok == true
        assert ultrafiltrate("a", 600).ok == false     // 600+600 > 1000
        assert removed("a") == 600

        // cannot finish without rinse-back — blood would be stranded
        assert_fails transition("a", "complete")
        assert finish("a").ok == false
        assert rinse_back("a").ok == true
        assert finish("a").ok == true
        assert session_state("a") == "complete"

        // complete is terminal — no second run on the same circuit
        assert_fails transition("a", "running")
        assert_fails transition("a", "rinse_back")

        // the UF ledger refuses an impossible total even bypassing logic
        assert begin("b", 3000).ok == true
        assert_fails uf_total.set("b", 5001)
        assert_fails uf_total.set("b", -1)
    }
}

What the toolchain says

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

$ soma check dialysis.cell
✓ All checks passed.
$ soma test dialysis.cell
test DialysisTests ...
  ✓ assert begin("a", 1000).ok == true
  ✓ assert ultrafiltrate("a", 600).ok == true
  ✓ assert ultrafiltrate("a", 600).ok == false
  ✓ assert removed("a") == 600
  ✓ assert_fails transition("a", "complete") — raised invalid transition: running → complete. Current state: 'running'. Valid targets: [running, rinse_back]
  ✓ assert finish("a").ok == false
  ✓ assert rinse_back("a").ok == true
  ✓ assert finish("a").ok == true
  ✓ assert session_state("a") == "complete"
  ✓ assert_fails transition("a", "running") — raised invalid transition: complete → running. Current state: 'complete'. Valid targets: []
  ✓ assert_fails transition("a", "rinse_back") — raised invalid transition: complete → rinse_back. Current state: 'complete'. Valid targets: []
  ✓ assert begin("b", 3000).ok == true
  ✓ assert_fails uf_total.set("b", 5001) — raised memory invariant violated on 'uf_total': uf_total >= 0 && uf_total <= 5000 — rejected write of 5001 (key "b"); the slot is unchanged
  ✓ assert_fails uf_total.set("b", -1) — raised memory invariant violated on 'uf_total': uf_total >= 0 && uf_total <= 5000 — rejected write of -1 (key "b"); the slot is unchanged

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

State machine 'session': 4 states, initial 'prepped'
  States: [complete, prepped, rinse_back, running]

  ✓ 4 states, 4 transitions
  ✓ all states reachable from 'prepped'
  ✓ terminal states: [complete]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 4 literal transitions, 0 dynamic)
  ✓ termination: all 7 handlers structurally terminate
  ✓ refinement: handler `begin` ⟶ {running}
  ✓ refinement: handler `ultrafiltrate` ⟶ {running → running}
  ✓ refinement: handler `rinse_back` ⟶ {rinse_back}
  ✓ refinement: handler `finish` ⟶ {complete}
  · transition sources: `begin`, `rinse_back`, `finish` 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)

Dialysis/invariants:

  ✓ invariant uf_total >= 0 && uf_total <= 5000 — writer 'begin' proven (writes 0)
  ✓ invariant uf_total >= 0 && uf_total <= 5000 — writer 'ultrafiltrate' proven (linear arithmetic over the handler's requires and conditions) (writes sofar + ml)
  ⚠ invariant uf_goal >= 0 && uf_goal <= 5000 — runtime-checked (computed values): begin → uf_goal because `goal_ml` is a parameter (narrow it: `require goal_ml >= 0 && goal_ml <= 5000 else …`)

13 passed, 1 warnings, 0 failures

  Temporal properties for 'session':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK
$ soma run dialysis.cell demo
over-pull:  {"ok": false, "reason": "would exceed UF goal: 1500 + 800 > 2000 mL"}
early end:  {"ok": false, "reason": "blood not returned: invalid transition: running → complete. Current state: 'running'. Valid targets: [running, rinse_back]"}
clean end:  {"ok": true, "state": "complete"}
removed mL: 1500
demo done

Builtins used

get_status, map, print, transition

Source file: /corpus/medical/dialysis.cell · examples/corpus/medical/dialysis.cell