soma.cell

Ev charging — no current before the handshake, no session past the cap

Two real failure modes, one charging post. 1. Current before negotiation. The J1772/CCS pilot handshake exists so the cable, the car, and the post agree on a current the weakest link can carry. Skip it — a bent pin, a counterfeit adapter — and a 16 A cable carries 48 A until the insulation melts. Garage fires from uncertified EVSE gear are a documented pattern. 2. Charging past the cell limit. GM's 2021 recall of every Chevy Bolt came down to defective cells that became dangerous near full charge; the interim fix was SOFTWARE: cap the session. A cap that lives in an if-statement is a cap a firmware update can delete. Both are structural here: `charging` is reachable ONLY through `authorized` (the handshake state — soma verify proves it), and the session energy ledger refuses any write past 60 kWh at the memory.

A Soma program from the energy part of the verified corpus: 124 lines, cells ChargePost, ChargeTests. It passes soma check with soma 3.2.4.

How it is built

The cell ChargePost declares 8 signals in its face: plug_in(session: String) -> Map, handshake_done(session: String) -> Map, start_charge(session: String) -> Map, add_energy(session: String, kwh: Int) -> Map, stop_charge(session: String) -> Map, unplug(session: String) -> Map, session_state(session: String) -> String and delivered_kwh(session: String) -> Int; keeps 1 memory slot: delivered (Map<String, Int>, persistent and consistent) under the invariant delivered >= 0 && delivered <= 60. Its state machine session_fsm starts in idle and allows 5 transitions: idle -> plugged, plugged -> authorized, authorized -> charging, charging -> finished and finished -> idle. Handlers: plug_in (line 49), handshake_done (line 55), start_charge (line 61), add_energy (line 67), stop_charge (line 80), unplug (line 86), session_state (line 92) and delivered_kwh (line 93).

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

Generated from soma describe on this file.

The source

ev_charging.cell

// ============================================================================
// EV CHARGING — no current before the handshake, no session past the cap
// ============================================================================
//
// Two real failure modes, one charging post.
//
// 1. Current before negotiation. The J1772/CCS pilot handshake exists so
//    the cable, the car, and the post agree on a current the weakest link
//    can carry. Skip it — a bent pin, a counterfeit adapter — and a 16 A
//    cable carries 48 A until the insulation melts. Garage fires from
//    uncertified EVSE gear are a documented pattern.
// 2. Charging past the cell limit. GM's 2021 recall of every Chevy Bolt
//    came down to defective cells that became dangerous near full charge;
//    the interim fix was SOFTWARE: cap the session. A cap that lives in
//    an if-statement is a cap a firmware update can delete.
//
// Both are structural here: `charging` is reachable ONLY through
// `authorized` (the handshake state — soma verify proves it), and the
// session energy ledger refuses any write past 60 kWh at the memory.
// ============================================================================

cell ChargePost {
    face {
        signal plug_in(session: String) -> Map
        signal handshake_done(session: String) -> Map
        signal start_charge(session: String) -> Map
        signal add_energy(session: String, kwh: Int) -> Map
        signal stop_charge(session: String) -> Map
        signal unplug(session: String) -> Map
        signal session_state(session: String) -> String
        signal delivered_kwh(session: String) -> Int
    }

    memory {
        // kWh delivered this session — the cap is the wall
        delivered: Map<String, Int> [persistent, consistent]
        invariant delivered >= 0 && delivered <= 60
    }

    state session_fsm {
        initial: idle
        idle -> plugged            // cable seated, pilot signal present
        plugged -> authorized      // handshake: current limit + payment agreed
        authorized -> charging     // contactor closes
        charging -> finished       // target reached or user stop
        finished -> idle           // unplug
    }

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

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

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

    on add_energy(session: String, kwh: Int) {
        if kwh <= 0 { return map("ok", false, "reason", "energy must be positive") }
        if get_status(session) != "charging" {
            return map("ok", false, "reason", "contactor open (state: {get_status(session)})")
        }
        let total = delivered_kwh(session) + kwh
        let w = try { delivered.set(session, total) }
        if w.error != () {
            return map("ok", false, "reason", "session cap: {w.error}")
        }
        return map("ok", true, "delivered", total)
    }

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

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

    on session_state(session: String) { return get_status(session) }
    on delivered_kwh(session: String) { return delivered.get(session) ?? 0 }
}

cell test ChargeTests {
    rules {
        // plug in — but current BEFORE the handshake does not exist
        assert plug_in("s1").ok == true
        assert_fails transition("s1", "charging")
        assert add_energy("s1", 5).ok == false       // contactor stays open

        // handshake, then charge
        assert handshake_done("s1").ok == true
        assert start_charge("s1").ok == true
        assert add_energy("s1", 30).ok == true
        assert add_energy("s1", 25).ok == true       // 55 kWh

        // the interval that would cross the cap is refused; ledger intact
        assert add_energy("s1", 10).ok == false      // 55 + 10 > 60
        assert delivered_kwh("s1") == 55

        // THE WALL itself, with every handler check deleted
        assert_fails delivered.set("s1", 90)
        assert_fails delivered.set("s1", -2)

        // orderly finish
        assert stop_charge("s1").ok == true
        assert add_energy("s1", 1).ok == false       // no energy after stop
        assert unplug("s1").ok == true
        assert session_state("s1") == "idle"
    }
}

What the toolchain says

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

$ soma check ev_charging.cell
✓ All checks passed.
$ soma test ev_charging.cell
test ChargeTests ...
  ✓ assert plug_in("s1").ok == true
  ✓ assert_fails transition("s1", "charging") — raised invalid transition: plugged → charging. Current state: 'plugged'. Valid targets: [authorized]
  ✓ assert add_energy("s1", 5).ok == false
  ✓ assert handshake_done("s1").ok == true
  ✓ assert start_charge("s1").ok == true
  ✓ assert add_energy("s1", 30).ok == true
  ✓ assert add_energy("s1", 25).ok == true
  ✓ assert add_energy("s1", 10).ok == false
  ✓ assert delivered_kwh("s1") == 55
  ✓ assert_fails delivered.set("s1", 90) — raised memory invariant violated on 'delivered': delivered >= 0 && delivered <= 60 — rejected write of 90 (key "s1"); the slot is unchanged
  ✓ assert_fails delivered.set("s1", -2) — raised memory invariant violated on 'delivered': delivered >= 0 && delivered <= 60 — rejected write of -2 (key "s1"); the slot is unchanged
  ✓ assert stop_charge("s1").ok == true
  ✓ assert add_energy("s1", 1).ok == false
  ✓ assert unplug("s1").ok == true
  ✓ assert session_state("s1") == "idle"

15 tests: 15 passed, 0 failed
$ soma verify ev_charging.cell
Verifying ev_charging.cell...

State machine 'session_fsm': 5 states, initial 'idle'
  States: [authorized, charging, finished, idle, plugged]

  ✓ 5 states, 5 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)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 5 literal transitions, 0 dynamic)
  ✓ termination: all 8 handlers structurally terminate
  ✓ refinement: handler `plug_in` ⟶ {plugged}
  ✓ refinement: handler `handshake_done` ⟶ {authorized}
  ✓ refinement: handler `start_charge` ⟶ {charging}
  ✓ refinement: handler `stop_charge` ⟶ {finished}
  ✓ refinement: handler `unplug` ⟶ {idle}
  · transition sources: `plug_in`, `handshake_done`, `start_charge`, `stop_charge`, `unplug` 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)

ChargePost/invariants:

  ✓ invariant delivered >= 0 — writer 'add_energy' proven by induction (writes total)
  ⚠ invariant delivered >= 0 && delivered <= 60 — runtime-checked (computed values): add_energy → delivered [delivered <= 60] because `total` is only known to lie in [1, ∞) — narrow it: `require total <= 60 else …`

13 passed, 1 warnings, 0 failures

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

Builtins used

get_status, map, transition

Source file: /corpus/energy/ev_charging.cell · examples/corpus/energy/ev_charging.cell