//! gen_statem — generic finite state machine behaviour. //! //! The sibling of [`gen_server`](crate::gen_server): where a `gen_server` //! carries one undifferentiated blob of state and a single `handle` that //! re-derives "what mode am I in" on every message, a `gen_statem` makes the //! state an explicit **tag** and routes event handling by it. //! //! ## What this layer is //! //! This module is the **runtime support** a state machine runs on. A machine is //! any type implementing [`Machine`]: it owns its state tag and data, and its //! [`handle`](Machine::handle) reduces a `(state, event)` pair to a //! [`Resolution`]. The loop here drives it — spawn, //! [`on_start`](Machine::on_start), then one [`handle`](Machine::handle) per //! inbox event — mirroring `gen_server`'s spawn/teardown idioms. //! //! The [`gen_statem!`](crate::gen_statem) macro is the authoring surface: it //! *generates* a `Machine` impl from per-state handler blocks. Edge-validity is //! not a separate check — it falls out of the generated total `match (state, //! event)` under denied lints (a forgotten or duplicated pair is a compile //! error), so no proc-macro is needed. See `examples/gen_statem_macro.rs` for //! the macro form and `examples/gen_statem_fused.rs` for the hand-written shape //! it expands to. //! //! ## The unified event //! //! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries. By //! convention (and in the macro's desugaring) it folds the user's `cast` and //! `call` enums together: //! //! ```ignore //! enum Ev { Cast(MyCast), Call(MyCall) } //! ``` //! //! [`GenStatemRef::send`] pushes any event (a cast is just a `send`); //! [`GenStatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a //! `call` variant, and parks until the machine answers — exactly the //! `gen_server` call round-trip, but with the reply handle riding *inside* the //! user's own event so a handler can answer it. //! //! [`Resolution::Postpone`] and the timeout arming on [`Cx`] are part of the //! type surface but are not yet wired up. use crate::channel::{channel, Receiver, Sender}; use crate::pid::Pid; use crate::scheduler::spawn as spawn_actor; use std::marker::PhantomData; // --------------------------------------------------------------------------- // Machine // --------------------------------------------------------------------------- /// A finite state machine driven by the [`statem`](crate::gen_statem) loop. /// /// The implementor owns its current state tag and its persistent data. It is /// the **sole writer** of the state cell (the loop never touches it): both /// transition legality and, later, observer reads have a single source of /// truth. The loop only calls [`on_start`](Self::on_start) once and /// [`handle`](Self::handle) per event. pub trait Machine: Send + 'static { /// The single payload this machine's inbox carries — the user's `cast` and /// `call` enums folded together with the runtime's internal events. See the /// module docs. type Ev: Send + 'static; /// Runs once inside the actor before the first event. The canonical body /// runs the `enter` arm for the initial state. fn on_start(&mut self, cx: &mut Cx); /// React to one event. The body matches `(state, event)`, performs side /// effects / replies, and ends each arm in a [`Resolution`] — typically a /// state tag via `Tag.into()` (transition, or "stay" when it equals the /// current tag). On a transition the body sets the state cell and runs the /// new state's `enter`. fn handle(&mut self, ev: Self::Ev, cx: &mut Cx); } // --------------------------------------------------------------------------- // Resolution // --------------------------------------------------------------------------- /// The outcome of handling one event, before the loop/handler acts on it: /// dispatch reduces to `(state, event) -> Resolution`. /// /// [`From`](From) is why a bare state tag works as an arm tail and why /// "stay" needs no keyword — `Tag.into()` is `To(Tag)`, and the handler treats /// `To(s)` with `s == current` as stay. pub enum Resolution { /// End in state `s`. A **transition** when `s != current` (set the cell, /// run `enter`); a **stay** when `s == current` (no `enter`). To(S), /// Defer the current event onto the postpone queue, to be replayed after /// the next real transition. Produced by `cx.postpone()`; present /// here for forward-compatibility but not yet generated. Postpone, /// No arm matched `(state, event)`: log-and-drop via /// [`Cx::on_unhandled`], mirroring `gen_server`'s handling of unexpected /// messages. Unhandled, } impl From for Resolution { fn from(s: S) -> Self { Resolution::To(s) } } // --------------------------------------------------------------------------- // Cx — the per-handler context // --------------------------------------------------------------------------- /// The context handle injected into [`Machine::on_start`] and /// [`Machine::handle`]. Non-state outcomes (postpone, timeout arming) live here /// as method calls rather than keywords. /// /// For now it carries only the [`on_unhandled`](Self::on_unhandled) hook; /// `cx.state_timeout(d)` / `cx.timeout(name, d)` and `cx.postpone()` will attach /// here when implemented. It lives only on the actor's own /// stack and is never sent. pub struct Cx { _ev: PhantomData Ev>, } impl Cx { fn new() -> Self { Cx { _ev: PhantomData } } /// The default for an event no arm matched: **log-and-drop**. Overridable /// hook wiring is a follow-on; for now an unmatched event is /// silently dropped, as `gen_server` does with unexpected messages. pub fn on_unhandled(&mut self) {} } // --------------------------------------------------------------------------- // Reply — the move-only reply handle a `call` variant carries // --------------------------------------------------------------------------- /// The reply side of a synchronous `call`, carried *inside* the machine's own /// `call` event variant (`GetCount(Reply)`). Move-only: answering consumes /// it, so a handler replies at most once. Built by [`GenStatemRef::call`]; the /// caller parks on the matching receiver until `reply` is invoked (or the /// machine dies, closing the channel). /// /// Carrying the handle in the event — rather than the loop owning a reply slot /// — is what lets a later chunk **postpone a call**: the whole event, reply /// handle included, moves onto the postpone queue and is answered by a later /// state. pub struct Reply { tx: Sender, } impl Reply { /// Answer the call. A dropped/abandoned caller (e.g. one that timed out) /// makes the send fail harmlessly — the machine's reply is simply /// discarded, exactly as `gen_server`'s reply send behaves. pub fn reply(self, value: T) { let _ = self.tx.send(value); } } // --------------------------------------------------------------------------- // Client handle // --------------------------------------------------------------------------- /// Returned by [`GenStatemRef::call`] when the machine is unreachable. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum CallError { /// The machine was already gone, or died before replying. Down, } /// Returned by [`GenStatemRef::send`] when the machine's inbox is closed. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum SendError { /// The machine is gone (its inbox is closed). Down, } /// A clonable handle to a running machine. Cloning yields another sender to the /// same inbox; the machine lives until the last `GenStatemRef` is dropped, at which /// point its inbox closes and the loop exits. pub struct GenStatemRef { tx: Sender, pid: Pid, } impl Clone for GenStatemRef { fn clone(&self) -> Self { GenStatemRef { tx: self.tx.clone(), pid: self.pid } } } impl GenStatemRef { /// The machine actor's pid — usable with `monitor`, `request_stop`, `link`. pub fn pid(&self) -> Pid { self.pid } /// Push one event into the inbox and return immediately (fire-and-forget). /// A cast is just a `send` of the cast-tagged event. [`SendError::Down`] if /// the inbox is already closed. pub fn send(&self, ev: M::Ev) -> Result<(), SendError> { self.tx.send(ev).map_err(|_| SendError::Down) } /// Synchronous request-reply. Builds a one-shot [`Reply`] channel, hands it /// to `make` to construct the call-tagged event, sends it, and parks until /// the machine replies — or returns [`CallError::Down`] if the machine is or /// becomes unreachable first (the reply sender is dropped as the machine's /// stack unwinds, closing the channel and waking the caller). /// /// `make` wraps the [`Reply`] all the way to `M::Ev` — typically /// `|r| Ev::Call(MyCall::GetCount(r))`. (The deferred macro would generate a /// thinner `call` that hides the `Ev::Call` wrap and takes the bare variant /// constructor.) pub fn call(&self, make: F) -> Result where T: Send + 'static, F: FnOnce(Reply) -> M::Ev, { let (tx, rx) = channel::(); let ev = make(Reply { tx }); self.send(ev).map_err(|_| CallError::Down)?; rx.recv().map_err(|_| CallError::Down) } } // --------------------------------------------------------------------------- // Spawn + loop // --------------------------------------------------------------------------- /// Spawn `machine` as an actor and hand back its [`GenStatemRef`]. Shape mirrors /// `gen_server::start`: make the inbox, spawn the loop, return the ref; the /// backing join handle is dropped (lifetime is governed by refs, not joining). /// /// Panics if called outside `Runtime::run()`. pub fn spawn(machine: M) -> GenStatemRef { let (tx, rx) = channel::(); let handle = spawn_actor(move || statem_loop(rx, machine)); GenStatemRef { tx, pid: handle.pid() } } /// The machine actor body: `on_start`, then one `handle` per inbox event until /// the inbox closes (all refs dropped → graceful shutdown). Chunk 1 parks on /// the inbox alone — no system arm, no timers — the analogue of `gen_server`'s /// plain-inbox park. fn statem_loop(rx: Receiver, mut machine: M) { let mut cx = Cx::new(); machine.on_start(&mut cx); loop { match rx.recv() { Ok(ev) => machine.handle(ev, &mut cx), // All StatemRefs dropped → inbox closed → shutdown. Err(_) => break, } // Observation point so a machine fed a hot inbox stays preemptible and // cancellable. crate::check!(); } } // --------------------------------------------------------------------------- // gen_statem! — the authoring macro (fused total-match 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) -> GenStatemRef<$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::gen_statem::GenStatemRef<$sm> { $crate::gen_statem::spawn($sm { state: init, data }) } #[allow(unused_variables)] #[deny(unreachable_patterns)] fn enter(&mut self, state: $State, $cx: &mut $crate::gen_statem::Cx<$Ev>) { let $data = &mut self.data; match state { $( $est => { $ebody } ),+ } } } impl $crate::gen_statem::Machine for $sm { type Ev = $Ev; fn on_start(&mut self, $cx: &mut $crate::gen_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::gen_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::gen_statem::Resolution<$State> = $crate::gen_statem!(@arms ($Ev) ($cur, ev) [ ] $( on $st => { $($rows)* } )+); match next { $crate::gen_statem::Resolution::To(s) if s == $cur => {} $crate::gen_statem::Resolution::To(s) => { self.state = s; // <- sole writer of the state cell self.enter(s, $cx); } $crate::gen_statem::Resolution::Postpone => { unreachable!("postpone is not generated yet") } $crate::gen_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::gen_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::gen_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::gen_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::gen_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)*) }; }