soma.cell

An HVAC thermostat control loop

A single machine cycles between idle, heating, and cooling based on the measured temperature versus a target band. This is a reactive (cyclic) system with no terminal state — the proof obligation is deadlock-freedom: the controller always has a legal next action.

A Soma program from the state_machines part of the verified corpus: 75 lines, cells Thermostat, ThermostatTests. It passes soma check with soma 3.2.4.

How it is built

The cell Thermostat declares 3 signals in its face: set_target(lo: Int, hi: Int) -> Map, reading(id: String, temp: Int) -> Map and mode(id: String) -> String; keeps 1 memory slot: band (Map<String, Int>, persistent and consistent). Its state machine hvac starts in idle and allows 4 transitions: idle -> heating, idle -> cooling, heating -> idle and cooling -> idle. Handlers: set_target (line 26), reading (line 33) and mode (line 54).

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

Generated from soma describe on this file.

The source

thermostat.cell

// An HVAC thermostat control loop.
// A single machine cycles between idle, heating, and cooling based on the
// measured temperature versus a target band. This is a reactive (cyclic)
// system with no terminal state — the proof obligation is deadlock-freedom:
// the controller always has a legal next action.

cell Thermostat {
    face {
        signal set_target(lo: Int, hi: Int) -> Map
        signal reading(id: String, temp: Int) -> Map
        signal mode(id: String) -> String
    }

    memory {
        band: Map<String, Int> [persistent, consistent]
    }

    state hvac {
        initial: idle
        idle    -> heating
        idle    -> cooling
        heating -> idle
        cooling -> idle
    }

    on set_target(lo: Int, hi: Int) {
        require lo < hi else bad_band
        band.set("lo", lo)
        band.set("hi", hi)
        return map("lo", lo, "hi", hi)
    }

    on reading(id: String, temp: Int) {
        let lo = band.get("lo") ?? 18
        let hi = band.get("hi") ?? 24
        let cur = get_status(id)
        // decide the target mode for this temperature
        let want = if temp < lo { "heating" } else if temp > hi { "cooling" } else { "idle" }
        // return to idle first if we're actively conditioning and the
        // desired mode differs (the machine only allows idle<->active)
        if cur != "idle" && cur != want {
            transition(id, "idle")
            cur = "idle"
        }
        if cur == "idle" && want != "idle" {
            transition(id, want)
        }
        if cur != "idle" && want == "idle" {
            transition(id, "idle")
        }
        return map("temp", temp, "mode", get_status(id))
    }

    on mode(id: String) { return get_status(id) }
}

cell test ThermostatTests {
    rules {
        // configure a comfortable band
        assert set_target(18, 24).lo == 18

        // cold reading -> heating
        assert reading("h1", 15).mode == "heating"
        // warming into the band -> idle
        assert reading("h1", 21).mode == "idle"
        // hot reading -> cooling
        assert reading("h1", 28).mode == "cooling"
        // back into band -> idle
        assert reading("h1", 22).mode == "idle"

        // an inverted band is rejected
        assert_fails set_target(30, 10)
    }
}

What the toolchain says

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

$ soma check thermostat.cell
✓ All checks passed.
$ soma test thermostat.cell
test ThermostatTests ...
  ✓ assert set_target(18, 24).lo == 18
  ✓ assert reading("h1", 15).mode == "heating"
  ✓ assert reading("h1", 21).mode == "idle"
  ✓ assert reading("h1", 28).mode == "cooling"
  ✓ assert reading("h1", 22).mode == "idle"
  ✓ assert_fails set_target(30, 10) — raised require failed: bad_band: constraint violated

6 tests: 6 passed, 0 failed
$ soma verify thermostat.cell
Verifying thermostat.cell...

State machine 'hvac': 3 states, initial 'idle'
  States: [cooling, heating, idle]

  ✓ 3 states, 4 transitions
  ✓ all states reachable from 'idle'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'idle'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'idle' (reactive machine)
  ⚠ dynamic transition targets: handler `reading` 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 3 handlers structurally terminate
  ⚠ refinement (partial — dynamic targets fall back to runtime check): handler `reading` 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 `reading` ⟶ {idle [if cur != "idle" && cur != want], idle [if cur != "idle" && want == "idle"]} + <dynamic>
  · transition sources: `reading` calls 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)

7 passed, 2 warnings, 0 failures

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

Builtins used

get_status, map, transition

Source file: /corpus/state_machines/thermostat.cell · examples/corpus/state_machines/thermostat.cell