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:
smarm-agent
2026-06-20 10:48:33 +00:00
parent 3e316066c3
commit 0cf6b80396
6 changed files with 177 additions and 288 deletions
@@ -1,11 +1,9 @@
//! SPIKE (throwaway) — RFC 017 "fused" approach. //! The hand-written **expansion target** of the `gen_statem!` macro: the same
//! machine as `examples/gen_statem_macro.rs`, written out in full so the
//! primitives can be judged standing on their own. The macro generates exactly
//! this shape; nothing here needs the macro to be correct or safe.
//! //!
//! This is the hand-written *expansion target* of the eventual `statem!` macro, //! The design:
//! 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; //! * States and events are real enums. An invalid state is unrepresentable;
//! there are no bitflags, no `u32` superpositions, no unsafe unions. //! there are no bitflags, no `u32` superpositions, no unsafe unions.
@@ -27,18 +25,18 @@
//! wired in is dead code — and dead_code is denied below, making an orphan //! wired in is dead code — and dead_code is denied below, making an orphan
//! handler a compile error. //! handler a compile error.
//! //!
//! * Drops onto the committed `Machine` / `Resolution` / `Cx` primitives with //! * Drops onto the `Machine` / `Resolution` / `Cx` primitives with no change
//! no change to `src/statem.rs`. //! to `src/gen_statem.rs`.
//! //!
//! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents //! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents
//! how to make each of the four guarantees fire. //! how to make each of the four guarantees fire.
//! //!
//! Run: `cargo run --example statem_fused` //! Run: `cargo run --example gen_statem_fused`
#![deny(dead_code, unreachable_patterns)] #![deny(dead_code, unreachable_patterns)]
use smarm::run; use smarm::run;
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, StatemRef}; use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, GenStatemRef};
// === user types ============================================================ // === user types ============================================================
@@ -114,7 +112,7 @@ struct DoorSm {
} }
impl DoorSm { impl DoorSm {
fn start(init: Door) -> StatemRef<DoorSm> { fn start(init: Door) -> GenStatemRef<DoorSm> {
spawn(DoorSm { spawn(DoorSm {
state: init, state: init,
data: Data { enters: 0, pushes: 0 }, data: Data { enters: 0, pushes: 0 },
@@ -184,7 +182,7 @@ impl Machine for DoorSm {
self.state = s; // sole writer of the state cell self.state = s; // sole writer of the state cell
self.enter(cx); self.enter(cx);
} }
Resolution::Postpone => unreachable!("postpone lands in chunk 3"), Resolution::Postpone => unreachable!("postpone is not generated yet"),
Resolution::Unhandled => cx.on_unhandled(), Resolution::Unhandled => cx.on_unhandled(),
} }
} }
@@ -1,18 +1,18 @@
//! RFC 017 — the **same** machine as `examples/statem_fused.rs`, written through //! The **same** machine as `examples/gen_statem_fused.rs`, written through the
//! the `gen_statem!` macro. Diff this file against that one to see exactly what //! `gen_statem!` macro. Diff this file against that one to see exactly what the
//! the macro buys: every `// ===` section there that was boilerplate (the `Ev` //! macro buys: every `// ===` section there that was boilerplate (the `Ev`
//! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter` //! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter`
//! dispatch, the stay/transition apply-tail) collapses into the invocation //! dispatch, the stay/transition apply-tail) collapses into the invocation
//! below. What stays hand-written is what carries meaning: the four types, the //! below. What stays hand-written is what carries meaning: the four types, the
//! per-state successor enum, and the handler fns. //! per-state successor enum, and the handler fns.
//! //!
//! The point of the exercise is that the macro is *pure sugar*: the four //! 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 //! compile-time guarantees the hand-written form demonstrates are properties of
//! emitted code, not of the macro, so they survive expansion unchanged. The //! 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 //! 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. //! the macro surface — flip any one on and the compiler fires identically.
//! //!
//! Run: `cargo run --example statem_macro` //! Run: `cargo run --example gen_statem_macro`
#![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the #![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the
// dispatch's own unreachable_patterns internally. // dispatch's own unreachable_patterns internally.
@@ -21,7 +21,7 @@ use smarm::gen_statem;
use smarm::run; use smarm::run;
use smarm::gen_statem::Reply; use smarm::gen_statem::Reply;
// === user types (identical to statem_fused.rs) ============================= // === user types (identical to gen_statem_fused.rs) =========================
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Door { enum Door {
@@ -91,7 +91,7 @@ gen_statem! {
// You name the bindings the bodies use; the macro can't lend you its own // 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 // `self`/`cx` across macro hygiene. `data` = &mut Data, `prev` = current
// state tag, `cx` = context handle (unused in chunk 1). // state tag, `cx` = context handle (unused here).
context(data, prev, cx); context(data, prev, cx);
enter { enter {
@@ -145,7 +145,7 @@ fn main() {
// =========================================================================== // ===========================================================================
// BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly // BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly
// as it does in the hand-written statem_fused.rs. // as it does in the hand-written gen_statem_fused.rs.
// //
// 1. ORPHAN HANDLER (dead_code -> error): // 1. ORPHAN HANDLER (dead_code -> error):
// add `fn on_slam(_d: &mut Data) {}` and don't reference it. // add `fn on_slam(_d: &mut Data) {}` and don't reference it.
@@ -157,8 +157,8 @@ fn main() {
// NOTE: this example is a separate crate from `smarm`, so rustc's // NOTE: this example is a separate crate from `smarm`, so rustc's
// in_external_macro rule SILENCES this lint here even though the macro // in_external_macro rule SILENCES this lint here even though the macro
// denies it — the duplicate compiles. The guarantee is real only for // denies it — the duplicate compiles. The guarantee is real only for
// machines defined inside the smarm crate (see the unit test in // machines defined inside the `smarm` crate itself. This is the documented
// src/statem.rs). This is the documented macro_rules! limitation. // macro_rules! limitation.
// //
// 3. MISSING PAIR (non-exhaustive match, E0004): // 3. MISSING PAIR (non-exhaustive match, E0004):
// delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,` // delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,`
+45 -120
View File
@@ -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` //! The sibling of [`gen_server`](crate::gen_server): where a `gen_server`
//! carries one undifferentiated blob of state and a single `handle` that //! 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 //! 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 //! state an explicit **tag** and routes event handling by it.
//! chunks) adds the machinery state machines need — state-entry callbacks, a
//! timeout taxonomy, and event postponement.
//! //!
//! ## What this layer is //! ## What this layer is
//! //!
//! This module is the **runtime support** a state machine runs on, *not* the //! This module is the **runtime support** a state machine runs on. A machine is
//! authoring surface. A machine is any type implementing [`Machine`]: it owns //! any type implementing [`Machine`]: it owns its state tag and data, and its
//! its state tag and data, and its [`handle`](Machine::handle) reduces a //! [`handle`](Machine::handle) reduces a `(state, event)` pair to a
//! `(state, event)` pair to a [`Resolution`]. The loop here drives it — spawn, //! [`Resolution`]. The loop here drives it — spawn,
//! [`on_start`](Machine::on_start), then one [`handle`](Machine::handle) per //! [`on_start`](Machine::on_start), then one [`handle`](Machine::handle) per
//! inbox event — mirroring `gen_server`'s spawn/teardown idioms. //! inbox event — mirroring `gen_server`'s spawn/teardown idioms.
//! //!
//! The RFC's `statem!` macro (deferred) would *generate* a `Machine` impl from //! The [`gen_statem!`](crate::gen_statem) macro is the authoring surface: it
//! a declarative `transitions { … }` graph plus per-state handler blocks, and //! *generates* a `Machine` impl from per-state handler blocks. Edge-validity is
//! add the expansion-time edge-lint. Until then a machine is hand-written //! not a separate check — it falls out of the generated total `match (state,
//! against these primitives; see `examples/statem_switch.rs` for the shape the //! event)` under denied lints (a forgotten or duplicated pair is a compile
//! macro would target. //! 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 //! ## The unified event
//! //!
//! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries. //! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries. By
//! By convention (and in the macro's desugaring) it folds the user's `cast` //! convention (and in the macro's desugaring) it folds the user's `cast` and
//! and `call` enums together with the runtime's own internal events: //! `call` enums together:
//! //!
//! ```ignore //! ```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`); //! [`GenStatemRef::send`] pushes any event (a cast is just a `send`);
//! [`StatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a //! [`GenStatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a
//! `call` variant, and parks until the machine answers — exactly the //! `call` variant, and parks until the machine answers — exactly the
//! `gen_server` call round-trip, but with the reply handle riding *inside* 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) //! [`Resolution::Postpone`] and the timeout arming on [`Cx`] are part of the
//! //! type surface but are not yet wired up.
//! 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::channel::{channel, Receiver, Sender};
use crate::pid::Pid; 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: /// 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 /// [`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 /// "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`). /// run `enter`); a **stay** when `s == current` (no `enter`).
To(S), To(S),
/// Defer the current event onto the postpone queue, to be replayed after /// 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. /// here for forward-compatibility but not yet generated.
Postpone, Postpone,
/// No arm matched `(state, event)`: log-and-drop via /// 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 /// The context handle injected into [`Machine::on_start`] and
/// [`Machine::handle`]. Non-state outcomes (postpone, timeout arming) live here /// [`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; /// For now it carries only the [`on_unhandled`](Self::on_unhandled) hook;
/// `cx.state_timeout(d)` / `cx.timeout(name, d)` (chunk 2) and `cx.postpone()` /// `cx.state_timeout(d)` / `cx.timeout(name, d)` and `cx.postpone()` will attach
/// (chunk 3) attach here as those chunks land. It lives only on the actor's own /// here when implemented. It lives only on the actor's own
/// stack and is never sent. /// stack and is never sent.
pub struct Cx<Ev> { pub struct Cx<Ev> {
_ev: PhantomData<fn() -> 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 /// 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. /// silently dropped, as `gen_server` does with unexpected messages.
pub fn on_unhandled(&mut self) {} 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 /// The reply side of a synchronous `call`, carried *inside* the machine's own
/// `call` event variant (`GetCount(Reply<u32>)`). Move-only: answering consumes /// `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 /// caller parks on the matching receiver until `reply` is invoked (or the
/// machine dies, closing the channel). /// machine dies, closing the channel).
/// ///
/// Carrying the handle in the event — rather than the loop owning a reply slot /// 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 /// — 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 /// handle included, moves onto the postpone queue and is answered by a later
/// state (RFC §Postpone). /// state.
pub struct Reply<T> { pub struct Reply<T> {
tx: Sender<T>, tx: Sender<T>,
} }
@@ -168,14 +163,14 @@ impl<T> Reply<T> {
// Client handle // 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)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum CallError { pub enum CallError {
/// The machine was already gone, or died before replying. /// The machine was already gone, or died before replying.
Down, 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)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SendError { pub enum SendError {
/// The machine is gone (its inbox is closed). /// 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 /// 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. /// point its inbox closes and the loop exits.
pub struct StatemRef<M: Machine> { pub struct GenStatemRef<M: Machine> {
tx: Sender<M::Ev>, tx: Sender<M::Ev>,
pid: Pid, pid: Pid,
} }
impl<M: Machine> Clone for StatemRef<M> { impl<M: Machine> Clone for GenStatemRef<M> {
fn clone(&self) -> Self { 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`. /// The machine actor's pid — usable with `monitor`, `request_stop`, `link`.
pub fn pid(&self) -> Pid { pub fn pid(&self) -> Pid {
self.pid self.pid
@@ -235,15 +230,15 @@ impl<M: Machine> StatemRef<M> {
// Spawn + loop // 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 /// `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). /// backing join handle is dropped (lifetime is governed by refs, not joining).
/// ///
/// Panics if called outside `Runtime::run()`. /// 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 (tx, rx) = channel::<M::Ev>();
let handle = spawn_actor(move || statem_loop(rx, machine)); 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 /// 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 /// 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 /// # What it emits
/// ///
/// `enum $Ev { Cast($Cast), Call($Call) }`, `struct $Sm { state, data }`, /// `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 /// running the initial `enter`; `handle` = the dispatch match + the
/// stay/transition/unhandled apply-tail, the cell's sole writer), and the /// stay/transition/unhandled apply-tail, the cell's sole writer), and the
/// `enter` dispatch. Chunk 1: real time, no timers, no postpone. /// `enter` dispatch. Chunk 1: real time, no timers, no postpone.
@@ -426,7 +421,7 @@ macro_rules! gen_statem {
} }
impl $sm { 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 }) $crate::gen_statem::spawn($sm { state: init, data })
} }
@@ -466,7 +461,7 @@ macro_rules! gen_statem {
self.enter(s, $cx); self.enter(s, $cx);
} }
$crate::gen_statem::Resolution::Postpone => { $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(), $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)*) $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
});
}
}
+2 -2
View File
@@ -59,8 +59,8 @@ pub use gen_server::{
NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher, NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher,
}; };
pub use gen_statem::{ pub use gen_statem::{
CallError as StatemCallError, Cx, Machine, Reply, Resolution, SendError as StatemSendError, CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError,
StatemRef, GenStatemRef,
}; };
pub use introspect::{ pub use introspect::{
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree, actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
+108
View File
@@ -0,0 +1,108 @@
//! gen_statem behaviour tests, driven through the `gen_statem!` macro: cast/call
//! round-trip, `enter` firing on start and on every real transition (but not on
//! a stay), and the machine-down path when a handler panics.
use smarm::gen_statem;
use smarm::gen_statem::{CallError, Reply};
use smarm::run;
use std::sync::{Arc, Mutex};
// A two-state machine: Flip toggles, calls read counters, Boom panics.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Switch {
Off,
On,
}
struct Counts {
flips: u32,
enters: u32,
}
enum Cast {
Flip,
}
enum Call {
GetFlips(Reply<u32>),
GetEnters(Reply<u32>),
Boom(Reply<u32>),
}
gen_statem! {
machine: Sm { state: Switch, data: Counts };
event: Ev { cast: Cast, call: Call };
context(data, prev, cx);
enter {
_ => data.enters += 1,
}
on Switch::Off => {
cast Cast::Flip => { data.flips += 1; Switch::On },
}
on Switch::On => {
cast Cast::Flip => Switch::Off,
}
// State-independent queries: reply, then stay via `prev`. Boom panics
// (`boom()` is typed as a state tag so the arm stays well-formed).
on _ => {
call Call::GetFlips(r) => { r.reply(data.flips); prev },
call Call::GetEnters(r) => { r.reply(data.enters); prev },
call Call::Boom(_r) => boom(),
}
}
fn boom() -> Switch {
panic!("boom")
}
// Casts are applied in order and a later call observes the accumulated data.
#[test]
fn cast_then_call_roundtrip() {
let got = Arc::new(Mutex::new(0u32));
let got2 = got.clone();
run(move || {
let sw = Sm::start(Switch::Off, Counts { flips: 0, enters: 0 });
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On (flips = 1)
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off (no flip count)
let flips = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
*got2.lock().unwrap() = flips;
});
assert_eq!(*got.lock().unwrap(), 1, "turned On once across the two flips");
}
// `enter` fires once on start and once per *real* transition; a stay (a call
// that returns the current tag) does not re-enter.
#[test]
fn enter_on_start_and_each_transition_but_not_stay() {
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
let got2 = got.clone();
run(move || {
let sw = Sm::start(Switch::Off, Counts { flips: 0, enters: 0 }); // enter -> 1
let after_start = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
// Two stays (the reads) must not bump enters.
let _ = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
let still = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On -> enter -> 2
let after_flip = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
*got2.lock().unwrap() = (after_start, still, after_flip);
});
assert_eq!(*got.lock().unwrap(), (1, 1, 2));
}
// A handler that panics tears the loop down; the in-flight call's reply channel
// closes as the stack unwinds, so the parked caller wakes with Down (mirrors
// gen_server's panicking-handler path).
#[test]
fn call_to_panicking_handler_is_down() {
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
let got2 = got.clone();
run(move || {
let sw = Sm::start(Switch::Off, Counts { flips: 0, enters: 0 });
let r = sw.call(|rep| Ev::Call(Call::Boom(rep)));
*got2.lock().unwrap() = Some(r);
});
assert_eq!(*got.lock().unwrap(), Some(Err(CallError::Down)));
}
-142
View File
@@ -1,142 +0,0 @@
//! gen_statem (RFC 017) chunk-1 tests: call/cast round-trip against a
//! hand-written machine, `enter` firing on start and on every real transition
//! (but not on a stay), and the machine-down path when a handler panics.
//!
//! These drive the `smarm::gen_statem` primitives directly, the same way a
//! `statem!`-generated machine eventually will.
use smarm::run;
use smarm::gen_statem::{self, CallError, Cx, Machine, Reply, Resolution, StatemRef};
use std::sync::{Arc, Mutex};
// ---------------------------------------------------------------------------
// A hand-written two-state machine: Flip toggles, calls read, Boom panics.
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Switch {
Off,
On,
}
struct Counts {
flips: u32,
enters: u32,
}
enum Cast {
Flip,
}
enum Call {
GetFlips(Reply<u32>),
GetEnters(Reply<u32>),
Boom(Reply<u32>),
}
enum Ev {
Cast(Cast),
Call(Call),
}
struct Sm {
state: Switch,
data: Counts,
}
impl Sm {
fn start(init: Switch) -> StatemRef<Sm> {
gen_statem::spawn(Sm { state: init, data: Counts { flips: 0, enters: 0 } })
}
fn enter(&mut self, _s: Switch, _cx: &mut Cx<Ev>) {
self.data.enters += 1;
}
}
impl Machine for Sm {
type Ev = Ev;
fn on_start(&mut self, cx: &mut Cx<Ev>) {
let s = self.state;
self.enter(s, cx);
}
fn handle(&mut self, ev: Ev, cx: &mut Cx<Ev>) {
let prev = self.state;
let next: Resolution<Switch> = match (self.state, ev) {
(Switch::Off, Ev::Cast(Cast::Flip)) => {
self.data.flips += 1;
Switch::On.into()
}
(Switch::On, Ev::Cast(Cast::Flip)) => Switch::Off.into(),
(s, Ev::Call(Call::GetFlips(r))) => {
r.reply(self.data.flips);
s.into() // stay
}
(s, Ev::Call(Call::GetEnters(r))) => {
r.reply(self.data.enters);
s.into() // stay
}
(_, Ev::Call(Call::Boom(_r))) => panic!("boom"),
};
match next {
Resolution::To(s) if s == prev => {}
Resolution::To(s) => {
self.state = s;
self.enter(s, cx);
}
Resolution::Postpone => {}
Resolution::Unhandled => cx.on_unhandled(),
}
}
}
// Casts are applied in order and a later call observes the accumulated data.
#[test]
fn cast_then_call_roundtrip() {
let got = Arc::new(Mutex::new(0u32));
let got2 = got.clone();
run(move || {
let sw = Sm::start(Switch::Off);
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off
let flips = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
*got2.lock().unwrap() = flips;
});
assert_eq!(*got.lock().unwrap(), 1, "turned On once across the two flips");
}
// `enter` fires once on start and once per *real* transition; a stay (a call
// that returns the current tag) does not re-enter.
#[test]
fn enter_on_start_and_each_transition_but_not_stay() {
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
let got2 = got.clone();
run(move || {
let sw = Sm::start(Switch::Off); // on_start enter -> enters = 1
let after_start = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
// Two stays (the reads above + below) must not bump enters.
let _ = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
let still = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On -> enter -> enters = 2
let after_flip = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
*got2.lock().unwrap() = (after_start, still, after_flip);
});
assert_eq!(*got.lock().unwrap(), (1, 1, 2));
}
// A handler that panics tears the loop down; the in-flight call's reply channel
// closes as the stack unwinds, so the parked caller wakes with Down (mirrors
// gen_server's panicking-handler path).
#[test]
fn call_to_panicking_handler_is_down() {
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
let got2 = got.clone();
run(move || {
let sw = Sm::start(Switch::Off);
let r = sw.call(|rep| Ev::Call(Call::Boom(rep)));
*got2.lock().unwrap() = Some(r);
});
assert_eq!(*got.lock().unwrap(), Some(Err(CallError::Down)));
}