//! The hand-written **expansion target** of the `gen_statem!` macro: the same //! machine as `examples/gen_statem_macro.rs`, written out in full so the //! primitives can be judged standing on their own. The macro generates exactly //! this shape; nothing here needs the macro to be correct or safe. //! //! The design: //! //! * 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 `Machine` / `Resolution` / `Cx` primitives with no change //! to `src/gen_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 gen_statem_expanded` #![deny(dead_code, unreachable_patterns)] use smarm::run; use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, GenStatemRef}; // === 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), // The runtime's internal events. `Info` is out-of-band (here unused, so // `()`); `StateTimeout` / `Timeout` are timer fires the loop feeds back in. Info(()), StateTimeout, Timeout(&'static str), } 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) -> GenStatemRef { spawn(DoorSm { state: init, data: Data { enters: 0, pushes: 0 }, }) } fn enter(&mut self, cx: &mut Cx) { self.data.enters += 1; // A state-timeout: an Open door auto-closes after a quiet window. The // loop auto-resets it on any transition, so it fires only if the door is // still Open when it elapses. if self.state == Door::Open { cx.state_timeout(std::time::Duration::from_millis(5)); } } } impl Machine for DoorSm { type Ev = Ev; fn state_timeout_ev() -> Ev { Ev::StateTimeout } fn timeout_ev(name: &'static str) -> Ev { Ev::Timeout(name) } 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) } // --- timeouts: an Open door auto-closes; others have none armed -- (Door::Open, Ev::StateTimeout) => Resolution::To(Door::Closed), (_, Ev::StateTimeout) => Resolution::Unhandled, (_, Ev::Timeout(name)) => { // No named timeout is armed in this run; a real handler would // dispatch on `name`. Acknowledge it to exercise the field. let _ = name; Resolution::Unhandled } // --- out-of-band info: silent drop (the gen_server default) ------ (_, Ev::Info(_)) => Resolution::Unhandled, }; // ---- 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 cx.__reset_state_timeout(); // auto-reset across transitions self.enter(cx); } Resolution::Postpone => unreachable!("postpone is not generated yet"), 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::Push)).unwrap(); // Closed: Push invalid -> Unhandled door.send(Ev::Cast(Cast::Pull)).unwrap(); // Closed -> Open (arms 5ms auto-close) door.send(Ev::Info(())).unwrap(); // out-of-band: silently dropped // Wait past the auto-close window: the state-timeout fires and the door // closes itself, with no further input. (`smarm::sleep` parks the actor // without blocking a worker thread, so the timer wheel keeps turning.) smarm::sleep(std::time::Duration::from_millis(40)); 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); // auto-closed by the state-timeout assert_eq!(enters, 5); // Closed(start) + Locked + Closed + Open + Closed assert_eq!(pushes, 0); // no push ever closed it this run 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` // ===========================================================================