diff --git a/examples/statem_fused.rs b/examples/statem_fused.rs new file mode 100644 index 0000000..1a95f6f --- /dev/null +++ b/examples/statem_fused.rs @@ -0,0 +1,236 @@ +//! 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` +// =========================================================================== diff --git a/examples/statem_macro.rs b/examples/statem_macro.rs new file mode 100644 index 0000000..2a6cca0 --- /dev/null +++ b/examples/statem_macro.rs @@ -0,0 +1,171 @@ +//! 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` +// =========================================================================== diff --git a/src/statem.rs b/src/statem.rs index 81e58a7..033845a 100644 --- a/src/statem.rs +++ b/src/statem.rs @@ -264,3 +264,336 @@ fn statem_loop(rx: Receiver, mut machine: M) { crate::check!(); } } + +// --------------------------------------------------------------------------- +// gen_statem! — the authoring macro (RFC 017 §Surface, fused variant) +// --------------------------------------------------------------------------- + +/// Assemble a complete [`Machine`] from hand-written types, a glanceable +/// transition table, and free handler functions. +/// +/// This is the **fused** authoring surface (see `examples/statem_fused.rs` for +/// the same machine written out by hand). You keep ownership of every type that +/// carries meaning — the state enum, the data struct, the `cast`/`call` enums, +/// and any per-row successor enums — and the macro emits only the mechanical +/// scaffolding: the unified event enum, the machine struct and its private +/// state cell, `start`, the `Machine` impl, and the `enter` dispatch. +/// +/// The macro is **pure sugar with no semantic analysis of its own** — it never +/// inspects your types and never validates the graph. Every safety property is +/// a property of the code it emits, enforced by the *compiler* — which is what +/// lets a plain `macro_rules!` carry almost the whole guarantee set a proc-macro +/// would (the one exception, conflicting rows across a crate boundary, is +/// spelled out below). +/// +/// # The four guarantees (and exactly how strong each is) +/// +/// Two of these are hard `rustc` errors that fire unconditionally. The other +/// two are *lints*, so they need a deny in force — and lints interact with +/// macros, which is called out below. Put `#![deny(dead_code)]` on the crate +/// using the macro (the dispatch already denies its own pattern lint). +/// +/// 1. **Forgotten `(state, event)` pair → `E0004` (hard error, always).** The +/// dispatch is a *total* `match (state, event)` with **no macro-injected +/// catch-all**: a pair you never wrote makes the match non-exhaustive. +/// Totality is the price — refuse the events you don't handle with explicit +/// `=> unhandled` rows (OR-patterns keep that to one row per state). +/// 4. **Out-of-set branch target → `E0599` (hard error, always).** A branching +/// row returns a per-state *successor enum* (its variants are that state's +/// declared targets); naming a tag outside it is a missing variant. +/// 3. **Orphan handler → `dead_code` (lint; needs your `#![deny(dead_code)]`).** +/// Handlers are ordinary module-private `fn`s in *your* crate, reachable only +/// through the table, so this lint sees them normally. +/// 2. **Duplicated / conflicting row → `unreachable_patterns` (lint; with a +/// caveat).** Your patterns are emitted verbatim into one match, so a second +/// arm for the same pair is unreachable. The macro applies +/// `#[deny(unreachable_patterns)]` to the dispatch itself — **but** `rustc` +/// silences this lint for code expanded from a macro defined in *another* +/// crate (`in_external_macro`). So a conflicting row is a hard error when the +/// machine lives in the same crate as this macro, and is **silently dropped +/// when you call `gen_statem!` from a downstream crate** — neither a crate +/// `#![deny]` nor `#![forbid]` overrides the suppression. This is the one +/// guarantee a `macro_rules!` cannot carry across the crate boundary; the +/// RFC's proc-macro could (it would stamp the arms with call-site spans). +/// Guard against it in review, or with an in-crate test of the machine. +/// +/// There is deliberately **no separate `transitions { … }` adjacency block**: +/// in this variant the `match` *is* the table and the successor enums *are* the +/// per-state target sets, so a second listing would be redundant and +/// un-cross-checkable without a proc-macro. The graph is read off the `on` +/// blocks and the successor enums at the top of the file. +/// +/// # Surface +/// +/// ```ignore +/// // ── your types (the macro never generates or inspects these) ─────────── +/// #[derive(Clone, Copy, PartialEq, Eq, Debug)] +/// enum Switch { Off, On } +/// struct Counts { flips: u32, enters: u32 } +/// enum SwitchCast { Flip } +/// enum SwitchCall { GetCount(Reply) } +/// +/// gen_statem! { +/// machine: SwitchSm { state: Switch, data: Counts }; +/// event: Ev { cast: SwitchCast, call: SwitchCall }; +/// +/// // Name the bindings your handler bodies use. A declarative macro can't +/// // hand you its own `self`/`cx` (hygiene), so you choose the identifiers +/// // and the macro binds them: `data` = &mut your Data, `prev` = the +/// // current state tag, `cx` = the context handle. +/// context(data, prev, cx); +/// +/// // `enter` runs on entry to a state; side effects only, returns (). +/// // Match the state tag (or `_`); the arm body is statements. +/// enter { +/// _ => data.enters += 1, +/// } +/// +/// // The transition table. Group rows by current state with `on `. +/// // A row is: cast|call [if ] => , +/// // and the tail is one of: +/// // * a state tag `Switch::On` (transition, or "stay" +/// // if it equals current) +/// // * a block ending in one `{ data.flips += 1; Switch::On }` +/// // * a successor-enum value `unlock(key)` (branching row) +/// // * the keyword `unhandled` (explicit refusal) +/// on Switch::Off => { +/// cast SwitchCast::Flip => { data.flips += 1; Switch::On }, +/// call SwitchCall::GetCount(r) => { r.reply(data.flips); prev }, +/// } +/// on Switch::On => { +/// cast SwitchCast::Flip => Switch::Off, +/// call SwitchCall::GetCount(r) => { r.reply(data.flips); prev }, +/// } +/// } +/// ``` +/// +/// # Writing the rows +/// +/// * **Every row ends in a comma** (block-bodied ones too) and every `on` block +/// is `on => { … }`. These two are macro-grammar requirements, not +/// style: a declarative matcher can't otherwise tell where a row's tail ends. +/// * **Qualify event patterns** (`SwitchCast::Flip`) and **state tags** +/// (`Switch::On`). A declarative macro can't prepend the enum name inside an +/// opaque pattern fragment, so the `cast`/`call` keyword only selects the +/// event wrapper — it does not qualify for you. The keyword also reads as a +/// sync/async marker at a glance. +/// * **Stay** = return the current tag. The `prev` you named in `context` is +/// bound to the pre-handler state for exactly this — handy in any-state +/// (`on _`) rows where there is no single literal tag to write. +/// * **`data` and `cx`** (the names you chose) are in scope in every body: +/// mutate `data`, reply through a `Reply` bound in the pattern, and (chunks +/// 2–3) arm timeouts or postpone via `cx`. You never write `self`. +/// * **Guards** are plain match guards. A guarded arm does not count toward +/// exhaustiveness, so pair it with an unguarded fallback or you'll (correctly) +/// trip `E0004`. +/// * **Branching rows** return a successor enum that `impl`s `From<_>` for the +/// state type; the macro supplies the outer `.into()`. +/// +/// # What it emits +/// +/// `enum $Ev { Cast($Cast), Call($Call) }`, `struct $Sm { state, data }`, +/// `$Sm::start(init, data) -> StatemRef<$Sm>`, the `Machine` impl (`on_start` +/// running the initial `enter`; `handle` = the dispatch match + the +/// stay/transition/unhandled apply-tail, the cell's sole writer), and the +/// `enter` dispatch. Chunk 1: real time, no timers, no postpone. +/// +/// # Limitation +/// +/// Diagnostics point at the macro expansion, not the offending source row — +/// the cost of staying a `macro_rules!`. The errors are the standard `E0004` / +/// `E0599` / lint messages, just sited at the invocation. +#[macro_export] +macro_rules! gen_statem { + // ===== public entry ===================================================== + ( + machine: $sm:ident { state: $State:ty, data: $Data:ty } ; + event: $Ev:ident { cast: $Cast:ty, call: $Call:ty } ; + context ( $data:ident , $cur:ident , $cx:ident ) ; + enter { $( $est:pat => $ebody:expr ),+ $(,)? } + $( on $st:pat => { $($rows:tt)* } )+ + ) => { + /// Unified inbox payload: the user's `cast`/`call` enums folded together + /// (chunks 2–3 add the runtime's internal timeout variants here). + enum $Ev { + Cast($Cast), + Call($Call), + } + + struct $sm { + state: $State, + data: $Data, + } + + impl $sm { + fn start(init: $State, data: $Data) -> $crate::statem::StatemRef<$sm> { + $crate::statem::spawn($sm { state: init, data }) + } + + #[allow(unused_variables)] + #[deny(unreachable_patterns)] + fn enter(&mut self, state: $State, $cx: &mut $crate::statem::Cx<$Ev>) { + let $data = &mut self.data; + match state { + $( $est => { $ebody } ),+ + } + } + } + + impl $crate::statem::Machine for $sm { + type Ev = $Ev; + + fn on_start(&mut self, $cx: &mut $crate::statem::Cx<$Ev>) { + let s = self.state; + self.enter(s, $cx); + } + + #[allow(unused_variables)] + #[deny(unreachable_patterns)] // conflicting rows must fail even though + // this match is external-macro-expanded + fn handle(&mut self, ev: $Ev, $cx: &mut $crate::statem::Cx<$Ev>) { + // Caller-named bindings (shared call-site hygiene, so row bodies + // can see them): `$cur` = current state tag, `$data` = &mut Data. + let $cur = self.state; + let $data = &mut self.data; + let next: $crate::statem::Resolution<$State> = + $crate::gen_statem!(@arms ($Ev) ($cur, ev) [ ] + $( on $st => { $($rows)* } )+); + match next { + $crate::statem::Resolution::To(s) if s == $cur => {} + $crate::statem::Resolution::To(s) => { + self.state = s; // <- sole writer of the state cell + self.enter(s, $cx); + } + $crate::statem::Resolution::Postpone => { + unreachable!("postpone is unreachable until chunk 3") + } + $crate::statem::Resolution::Unhandled => $cx.on_unhandled(), + } + } + } + }; + + // ===== @arms: build the dispatch match ================================== + // No more on-blocks: emit the (total, catch-all-free) match. + (@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ]) => { + match ($ss, $se) { $($arms)* } + }; + // Open an on-block: remember its state pat, drain its rows, then continue. + (@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] + on $st:pat => { $($rows:tt)* } $($more:tt)* + ) => { + $crate::gen_statem!(@rows ($Ev) ($ss, $se) [ $($arms)* ] ($st) + { $($rows)* } { $($more)* }) + }; + + // ===== @rows: drain one on-block's rows, threading the global acc ======== + // cast, explicit refusal + (@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat) + { cast $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* } + ) => { + $crate::gen_statem!(@rows ($Ev) ($ss, $se) + [ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::statem::Resolution::Unhandled, ] + ($st) { $($rows)* } { $($more)* }) + }; + // cast, transition / stay / branch + (@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat) + { cast $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* } + ) => { + $crate::gen_statem!(@rows ($Ev) ($ss, $se) + [ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::statem::Resolution::To($tail.into()), ] + ($st) { $($rows)* } { $($more)* }) + }; + // call, explicit refusal + (@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat) + { call $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* } + ) => { + $crate::gen_statem!(@rows ($Ev) ($ss, $se) + [ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::statem::Resolution::Unhandled, ] + ($st) { $($rows)* } { $($more)* }) + }; + // call, transition / stay / branch + (@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat) + { call $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* } + ) => { + $crate::gen_statem!(@rows ($Ev) ($ss, $se) + [ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::statem::Resolution::To($tail.into()), ] + ($st) { $($rows)* } { $($more)* }) + }; + // this block is drained: hand the remaining on-blocks back to @arms + (@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat) + { } { $($more:tt)* } + ) => { + $crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] $($more)*) + }; +} + +#[cfg(test)] +mod gen_statem_tests { + use crate::statem::Reply; + + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + enum Switch { + Off, + On, + } + struct Counts { + flips: u32, + enters: u32, + } + enum Cast { + Flip, + Nudge, + } + enum Call { + GetFlips(Reply), + GetEnters(Reply), + } + + fn on_flip(data: &mut Counts) { + data.flips += 1; + } + + crate::gen_statem! { + machine: SwitchSm { state: Switch, data: Counts }; + event: Ev { cast: Cast, call: Call }; + context(data, prev, cx); + + enter { + _ => data.enters += 1, + } + + on Switch::Off => { + cast Cast::Flip => { on_flip(data); Switch::On }, + cast Cast::Nudge => unhandled, + } + on Switch::On => { + cast Cast::Flip => { on_flip(data); Switch::Off }, + cast Cast::Nudge => prev, // explicit stay + } + on _ => { + call Call::GetFlips(r) => { r.reply(data.flips); prev }, + call Call::GetEnters(r) => { r.reply(data.enters); prev }, + } + } + + // NOTE (guarantee #2): because this machine lives in the SAME crate as + // `gen_statem!`, adding a duplicate row here — e.g. a second + // `cast Cast::Nudge => unhandled,` under `on Switch::Off` — is a hard + // `unreachable pattern` error. (From a downstream crate it is silently + // suppressed by rustc's in_external_macro rule; see the macro docs.) + #[test] + fn macro_machine_drives_and_counts() { + crate::run(|| { + let sm = SwitchSm::start(Switch::Off, Counts { flips: 0, enters: 0 }); + sm.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On (flip=1, enter) + sm.send(Ev::Cast(Cast::Nudge)).unwrap(); // On: stay (no enter) + sm.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off (flip=2, enter) + + let flips = sm.call(|r| Ev::Call(Call::GetFlips(r))).unwrap(); + let enters = sm.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); + assert_eq!(flips, 2); + assert_eq!(enters, 3); // Off(start) + On + Off + }); + } +}