cleanup some LLM crud

This commit is contained in:
smarm
2026-06-20 12:22:33 +02:00
parent 07867b91f6
commit f646c5cd72
6 changed files with 24 additions and 176 deletions
+599
View File
@@ -0,0 +1,599 @@
//! gen_statem — generic finite state machine behaviour (RFC 017).
//!
//! 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**, routes event handling by it, and (in later
//! chunks) adds the machinery state machines need — state-entry callbacks, a
//! timeout taxonomy, and event postponement.
//!
//! ## What this layer is
//!
//! This module is the **runtime support** a state machine runs on, *not* the
//! authoring surface. 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 RFC's `statem!` macro (deferred) would *generate* a `Machine` impl from
//! a declarative `transitions { … }` graph plus per-state handler blocks, and
//! add the expansion-time edge-lint. Until then a machine is hand-written
//! against these primitives; see `examples/statem_switch.rs` for the shape the
//! macro would target.
//!
//! ## 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 with the runtime's own internal events:
//!
//! ```ignore
//! enum Ev { Cast(MyCast), Call(MyCall) /* later: StateTimeout, Timeout(name) */ }
//! ```
//!
//! [`StatemRef::send`] pushes any event (a cast is just a `send`);
//! [`StatemRef::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 (or, later, postpone) it.
//!
//! ## Chunk status (RFC 017 §Sequencing)
//!
//! This is **chunk 1**: macro-free dispatch against real time — spawn,
//! `on_start`, stay/transition, and the `enter` callback (a method on the
//! machine, run on entry to a state). [`Resolution::Postpone`] and the timeout
//! arming on [`Cx`] are part of the type surface but are not yet acted on; they
//! land in chunks 23.
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>` (RFC §Semantics).
///
/// [`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()` (chunk 3); 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 (RFC §"Non-state outcomes live on cx").
///
/// In chunk 1 it carries only the [`on_unhandled`](Self::on_unhandled) hook;
/// `cx.state_timeout(d)` / `cx.timeout(name, d)` (chunk 2) and `cx.postpone()`
/// (chunk 3) attach here as those chunks land. 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 (RFC Open Q5); 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 [`StatemRef::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 (RFC §Postpone).
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 [`StatemRef::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 [`StatemRef::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 `StatemRef` is dropped, at which
/// point its inbox closes and the loop exits.
pub struct StatemRef<M: Machine> {
tx: Sender<M::Ev>,
pid: Pid,
}
impl<M: Machine> Clone for StatemRef<M> {
fn clone(&self) -> Self {
StatemRef { tx: self.tx.clone(), pid: self.pid }
}
}
impl<M: Machine> StatemRef<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 [`StatemRef`]. 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) -> StatemRef<M> {
let (tx, rx) = channel::<M::Ev>();
let handle = spawn_actor(move || statem_loop(rx, machine));
StatemRef { 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 (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<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
/// 23) 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 23 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::StatemRef<$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 unreachable until chunk 3")
}
$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)*)
};
}
#[cfg(test)]
mod gen_statem_tests {
use crate::gen_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<u32>),
GetEnters(Reply<u32>),
}
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
});
}
}