- rename StatemRef/StatemCallError/StatemSendError -> GenStatem*
- move the inline unit test out of src; consolidate the Switch coverage
onto a single macro-driven harness in tests/gen_statem.rs
- drop the redundant hand-written Switch test machine and the two
untracked rejected-direction probes (succ_enums, typed_edges)
- rename examples statem_{fused,macro}.rs -> gen_statem_{fused,macro}.rs
- strip RFC/chunk/spike provenance and fix the mislabeled "throwaway"
example header and dead cross-references
525 lines
24 KiB
Rust
525 lines
24 KiB
Rust
//! 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<Self::Ev>);
|
||
|
||
/// 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<Self::Ev>);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Resolution
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// The outcome of handling one event, before the loop/handler acts on it:
|
||
/// dispatch reduces to `(state, event) -> Resolution<State>`.
|
||
///
|
||
/// [`From<S>`](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<S> {
|
||
/// 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<S> From<S> for Resolution<S> {
|
||
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> {
|
||
_ev: PhantomData<fn() -> Ev>,
|
||
}
|
||
|
||
impl<Ev> Cx<Ev> {
|
||
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<u32>)`). 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<T> {
|
||
tx: Sender<T>,
|
||
}
|
||
|
||
impl<T> Reply<T> {
|
||
/// 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<M: Machine> {
|
||
tx: Sender<M::Ev>,
|
||
pid: Pid,
|
||
}
|
||
|
||
impl<M: Machine> Clone for GenStatemRef<M> {
|
||
fn clone(&self) -> Self {
|
||
GenStatemRef { tx: self.tx.clone(), pid: self.pid }
|
||
}
|
||
}
|
||
|
||
impl<M: Machine> GenStatemRef<M> {
|
||
/// 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<T, F>(&self, make: F) -> Result<T, CallError>
|
||
where
|
||
T: Send + 'static,
|
||
F: FnOnce(Reply<T>) -> M::Ev,
|
||
{
|
||
let (tx, rx) = channel::<T>();
|
||
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<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||
let (tx, rx) = channel::<M::Ev>();
|
||
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<M: Machine>(rx: Receiver<M::Ev>, 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<u32>) }
|
||
///
|
||
/// 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 <pat>`.
|
||
/// // A row is: cast|call <event-pattern> [if <guard>] => <tail> ,
|
||
/// // 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 <state-pat> => { … }`. 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)*)
|
||
};
|
||
}
|