//! RFC 017 — the **same** machine as `examples/statem_fused.rs`, written through //! the `gen_statem!` macro. Diff this file against that one to see exactly what //! the macro buys: every `// ===` section there that was boilerplate (the `Ev` //! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter` //! dispatch, the stay/transition apply-tail) collapses into the invocation //! below. What stays hand-written is what carries meaning: the four types, the //! per-state successor enum, and the handler fns. //! //! The point of the exercise is that the macro is *pure sugar*: the four //! compile-time guarantees the fused spike demonstrates are properties of the //! emitted code, not of the macro, so they survive expansion unchanged. The //! BREAK-CASE MENU at the bottom is the same four cases, re-expressed against //! the macro surface — flip any one on and the compiler fires identically. //! //! Run: `cargo run --example statem_macro` #![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the // dispatch's own unreachable_patterns internally. use smarm::gen_statem; use smarm::run; use smarm::statem::Reply; // === user types (identical to statem_fused.rs) ============================= #[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), } 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 =========================================================== // Everything below — Ev, DoorSm, start, Machine, enter — is generated. gen_statem! { machine: DoorSm { state: Door, data: Data }; event: Ev { cast: Cast, call: Call }; // You name the bindings the bodies use; the macro can't lend you its own // `self`/`cx` across macro hygiene. `data` = &mut Data, `prev` = current // state tag, `cx` = context handle (unused in chunk 1). context(data, prev, cx); enter { _ => data.enters += 1, } on Door::Open => { cast Cast::Push => { on_push(data); Door::Closed }, cast Cast::Pull | Cast::Lock | Cast::Unlock(_) => unhandled, } on Door::Closed => { cast Cast::Pull => Door::Open, cast Cast::Lock => Door::Locked, cast Cast::Push | Cast::Unlock(_) => unhandled, } on Door::Locked => { cast Cast::Unlock(key) => on_unlock(key), // branch -> UnlockOutcome cast Cast::Push | Cast::Pull | Cast::Lock => unhandled, } // state-independent queries (reply, then stay via `prev`) on _ => { call Call::GetState(r) => { r.reply(prev); prev }, call Call::GetEnters(r) => { r.reply(data.enters); prev }, call Call::GetPushes(r) => { r.reply(data.pushes); prev }, } } fn main() { run(|| { let door = DoorSm::start(Door::Closed, Data { enters: 0, pushes: 0 }); 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 — the four guarantees, through the macro. Each fires exactly // as it does in the hand-written statem_fused.rs. // // 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): // duplicate a row, e.g. add a second // `cast Cast::Push => unhandled,` under `on Door::Open`. // NOTE: this example is a separate crate from `smarm`, so rustc's // in_external_macro rule SILENCES this lint here even though the macro // denies it — the duplicate compiles. The guarantee is real only for // machines defined inside the smarm crate (see the unit test in // src/statem.rs). This is the documented macro_rules! limitation. // // 3. MISSING PAIR (non-exhaustive match, E0004): // delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,` // row under `on Door::Locked`. // => 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` // ===========================================================================