//! SPIKE (throwaway) — RFC 017 "fused" approach. //! //! This is the hand-written *expansion target* of the eventual `statem!` macro, //! written out in full so the primitives can be judged standing on their own. //! A macro would generate exactly this from a `transitions { … }` block; nothing //! here needs the macro to be correct or safe. //! //! The design, as settled over the prior iterations: //! //! * States and events are real enums. An invalid state is unrepresentable; //! there are no bitflags, no `u32` superpositions, no unsafe unions. //! //! * The dispatch `match (state, event)` IS the transition table. It is //! *total* — no catch-all `_` arm — so: //! - a forgotten (state, event) pair is a non-exhaustive `match` (E0004), //! - a duplicated/conflicting row is `unreachable_patterns` (denied below). //! //! * Single-target rows name their target in the table; the handler (if any) //! is side-effect-only. The table owns the target, so it cannot be wrong. //! //! * Branching rows have the handler return a per-row *successor enum*. A //! target outside that enum is E0599; a missing one is E0004. (See //! `UnlockOutcome` and `on_unlock`.) //! //! * Handlers are module-private. The only way to reach one is through the //! actor's message interface via this table, so a handler that is never //! wired in is dead code — and dead_code is denied below, making an orphan //! handler a compile error. //! //! * Drops onto the committed `Machine` / `Resolution` / `Cx` primitives with //! no change to `src/statem.rs`. //! //! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents //! how to make each of the four guarantees fire. //! //! Run: `cargo run --example statem_fused` #![deny(dead_code, unreachable_patterns)] use smarm::run; use smarm::statem::{spawn, Cx, Machine, Reply, Resolution, StatemRef}; // === user types ============================================================ #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Door { Open, Closed, Locked, } struct Data { enters: u32, // total state entries (incl. initial) pushes: u32, // times a push closed the door } enum Cast { Push, Pull, Lock, Unlock(u32), // carries a key } enum Call { GetState(Reply), GetEnters(Reply), GetPushes(Reply), } enum Ev { Cast(Cast), Call(Call), } const CODE: u32 = 1234; // === per-row successor enum for the one branching row ====================== // `Locked + Unlock` may end in Closed (right key) or Locked (wrong key) — and // nothing else. This type IS that declared set; `on_unlock` cannot name Open. enum UnlockOutcome { Closed, Locked, } impl From for Door { fn from(o: UnlockOutcome) -> Door { match o { UnlockOutcome::Closed => Door::Closed, UnlockOutcome::Locked => Door::Locked, } } } // === module-private handlers (side effects / branch choice only) =========== // Reachable solely through the table below. Orphan one and it is dead_code. fn on_push(data: &mut Data) { data.pushes += 1; } fn on_unlock(key: u32) -> UnlockOutcome { if key == CODE { UnlockOutcome::Closed } else { UnlockOutcome::Locked // wrong key: caller will see this == current -> stay } } // === the machine =========================================================== struct DoorSm { state: Door, data: Data, } impl DoorSm { fn start(init: Door) -> StatemRef { spawn(DoorSm { state: init, data: Data { enters: 0, pushes: 0 }, }) } fn enter(&mut self, _cx: &mut Cx) { self.data.enters += 1; } } impl Machine for DoorSm { type Ev = Ev; fn on_start(&mut self, cx: &mut Cx) { self.enter(cx); } fn handle(&mut self, ev: Ev, cx: &mut Cx) { let prev = self.state; // ---- the transition table ----------------------------------------- // This match is total over (Door, Ev). Read it as the declared graph: // each `=> To(x)` is an edge, each `=> Unhandled` an explicit refusal. let res: Resolution = match (self.state, ev) { // --- Open ------------------------------------------------------- (Door::Open, Ev::Cast(Cast::Push)) => { on_push(&mut self.data); Resolution::To(Door::Closed) } (Door::Open, Ev::Cast(Cast::Pull | Cast::Lock | Cast::Unlock(_))) => { Resolution::Unhandled } // --- Closed ----------------------------------------------------- (Door::Closed, Ev::Cast(Cast::Pull)) => Resolution::To(Door::Open), (Door::Closed, Ev::Cast(Cast::Lock)) => Resolution::To(Door::Locked), (Door::Closed, Ev::Cast(Cast::Push | Cast::Unlock(_))) => Resolution::Unhandled, // --- Locked (branching row: handler picks within UnlockOutcome) - (Door::Locked, Ev::Cast(Cast::Unlock(key))) => { Resolution::To(on_unlock(key).into()) } (Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => { Resolution::Unhandled } // --- state-independent queries (reply, then stay) --------------- (_, Ev::Call(Call::GetState(r))) => { r.reply(prev); Resolution::To(prev) } (_, Ev::Call(Call::GetEnters(r))) => { r.reply(self.data.enters); Resolution::To(prev) } (_, Ev::Call(Call::GetPushes(r))) => { r.reply(self.data.pushes); Resolution::To(prev) } }; // ---- apply the resolution ----------------------------------------- match res { Resolution::To(s) if s == prev => {} // stay: no enter Resolution::To(s) => { self.state = s; // sole writer of the state cell self.enter(cx); } Resolution::Postpone => unreachable!("postpone lands in chunk 3"), Resolution::Unhandled => cx.on_unhandled(), } } } fn main() { run(|| { let door = DoorSm::start(Door::Closed); door.send(Ev::Cast(Cast::Lock)).unwrap(); // Closed -> Locked door.send(Ev::Cast(Cast::Push)).unwrap(); // Locked: Push invalid -> Unhandled door.send(Ev::Cast(Cast::Unlock(0))).unwrap(); // Locked: wrong key -> stay door.send(Ev::Cast(Cast::Unlock(CODE))).unwrap(); // Locked -> Closed door.send(Ev::Cast(Cast::Pull)).unwrap(); // Closed -> Open door.send(Ev::Cast(Cast::Push)).unwrap(); // Open -> Closed (pushes=1) let st = door.call(|r| Ev::Call(Call::GetState(r))).unwrap(); let enters = door.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); let pushes = door.call(|r| Ev::Call(Call::GetPushes(r))).unwrap(); println!("state={st:?} enters={enters} pushes={pushes}"); assert_eq!(st, Door::Closed); assert_eq!(enters, 5); // Closed(start) + Locked + Closed + Open + Closed assert_eq!(pushes, 1); println!("ok"); }); } // =========================================================================== // BREAK-CASE MENU — each makes one compile-time guarantee fire. // // 1. ORPHAN HANDLER (dead_code -> error): // add `fn on_slam(_d: &mut Data) {}` and don't reference it. // => error: function `on_slam` is never used // // 2. CONFLICTING ROW (unreachable_patterns -> error): // duplicate an arm, e.g. add a second // `(Door::Open, Ev::Cast(Cast::Push)) => Resolution::Unhandled,` // => error: unreachable pattern // // 3. MISSING PAIR (non-exhaustive match, E0004): // delete the `(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock))` // arm. // => error[E0004]: non-exhaustive patterns: ... not covered // // 4. OUT-OF-SET TARGET (E0599): // in `on_unlock`, return `UnlockOutcome::Open`. // => error[E0599]: no variant ... named `Open` found for enum `UnlockOutcome` // ===========================================================================