gen_statem: GenStatem* type prefix + cleanup
- 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
This commit is contained in:
+45
-120
@@ -1,50 +1,45 @@
|
||||
//! gen_statem — generic finite state machine behaviour (RFC 017).
|
||||
//! 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**, routes event handling by it, and (in later
|
||||
//! chunks) adds the machinery state machines need — state-entry callbacks, a
|
||||
//! timeout taxonomy, and event postponement.
|
||||
//! 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, *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,
|
||||
//! 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 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 [`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 with the runtime's own internal events:
|
||||
//! 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) /* later: StateTimeout, Timeout(name) */ }
|
||||
//! enum Ev { Cast(MyCast), Call(MyCall) }
|
||||
//! ```
|
||||
//!
|
||||
//! [`StatemRef::send`] pushes any event (a cast is just a `send`);
|
||||
//! [`StatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a
|
||||
//! [`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 (or, later, postpone) it.
|
||||
//! user's own event so a handler can answer 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 2–3.
|
||||
//! [`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;
|
||||
@@ -85,7 +80,7 @@ pub trait Machine: Send + 'static {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The outcome of handling one event, before the loop/handler acts on it:
|
||||
/// dispatch reduces to `(state, event) -> Resolution<State>` (RFC §Semantics).
|
||||
/// 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
|
||||
@@ -95,7 +90,7 @@ pub enum Resolution<S> {
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -116,11 +111,11 @@ impl<S> From<S> for Resolution<S> {
|
||||
|
||||
/// 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").
|
||||
/// as method calls rather than keywords.
|
||||
///
|
||||
/// 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
|
||||
/// 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>,
|
||||
@@ -132,7 +127,7 @@ impl<Ev> Cx<Ev> {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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) {}
|
||||
}
|
||||
@@ -143,14 +138,14 @@ impl<Ev> Cx<Ev> {
|
||||
|
||||
/// 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
|
||||
/// 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 (RFC §Postpone).
|
||||
/// state.
|
||||
pub struct Reply<T> {
|
||||
tx: Sender<T>,
|
||||
}
|
||||
@@ -168,14 +163,14 @@ impl<T> Reply<T> {
|
||||
// Client handle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Returned by [`StatemRef::call`] when the machine is unreachable.
|
||||
/// 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 [`StatemRef::send`] when the machine's inbox is closed.
|
||||
/// 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).
|
||||
@@ -183,20 +178,20 @@ pub enum SendError {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// same inbox; the machine lives until the last `GenStatemRef` is dropped, at which
|
||||
/// point its inbox closes and the loop exits.
|
||||
pub struct StatemRef<M: Machine> {
|
||||
pub struct GenStatemRef<M: Machine> {
|
||||
tx: Sender<M::Ev>,
|
||||
pid: Pid,
|
||||
}
|
||||
|
||||
impl<M: Machine> Clone for StatemRef<M> {
|
||||
impl<M: Machine> Clone for GenStatemRef<M> {
|
||||
fn clone(&self) -> Self {
|
||||
StatemRef { tx: self.tx.clone(), pid: self.pid }
|
||||
GenStatemRef { tx: self.tx.clone(), pid: self.pid }
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine> StatemRef<M> {
|
||||
impl<M: Machine> GenStatemRef<M> {
|
||||
/// The machine actor's pid — usable with `monitor`, `request_stop`, `link`.
|
||||
pub fn pid(&self) -> Pid {
|
||||
self.pid
|
||||
@@ -235,15 +230,15 @@ impl<M: Machine> StatemRef<M> {
|
||||
// Spawn + loop
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Spawn `machine` as an actor and hand back its [`StatemRef`]. Shape mirrors
|
||||
/// 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) -> StatemRef<M> {
|
||||
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||||
let (tx, rx) = channel::<M::Ev>();
|
||||
let handle = spawn_actor(move || statem_loop(rx, machine));
|
||||
StatemRef { tx, pid: handle.pid() }
|
||||
GenStatemRef { tx, pid: handle.pid() }
|
||||
}
|
||||
|
||||
/// The machine actor body: `on_start`, then one `handle` per inbox event until
|
||||
@@ -266,7 +261,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// gen_statem! — the authoring macro (RFC 017 §Surface, fused variant)
|
||||
// gen_statem! — the authoring macro (fused total-match variant)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Assemble a complete [`Machine`] from hand-written types, a glanceable
|
||||
@@ -393,7 +388,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
||||
/// # What it emits
|
||||
///
|
||||
/// `enum $Ev { Cast($Cast), Call($Call) }`, `struct $Sm { state, data }`,
|
||||
/// `$Sm::start(init, data) -> StatemRef<$Sm>`, the `Machine` impl (`on_start`
|
||||
/// `$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.
|
||||
@@ -426,7 +421,7 @@ macro_rules! gen_statem {
|
||||
}
|
||||
|
||||
impl $sm {
|
||||
fn start(init: $State, data: $Data) -> $crate::gen_statem::StatemRef<$sm> {
|
||||
fn start(init: $State, data: $Data) -> $crate::gen_statem::GenStatemRef<$sm> {
|
||||
$crate::gen_statem::spawn($sm { state: init, data })
|
||||
}
|
||||
|
||||
@@ -466,7 +461,7 @@ macro_rules! gen_statem {
|
||||
self.enter(s, $cx);
|
||||
}
|
||||
$crate::gen_statem::Resolution::Postpone => {
|
||||
unreachable!("postpone is unreachable until chunk 3")
|
||||
unreachable!("postpone is not generated yet")
|
||||
}
|
||||
$crate::gen_statem::Resolution::Unhandled => $cx.on_unhandled(),
|
||||
}
|
||||
@@ -527,73 +522,3 @@ macro_rules! gen_statem {
|
||||
$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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user