RFC 017 chunk 1: gen_statem primitives (no macro yet)
Runtime support layer for gen_statem, built against existing public API (channel + scheduler::spawn), sibling to gen_server. No macro: per review, build the primitives first and hand-write the Switch example to evaluate whether a statem! macro earns its place before committing to one. - src/statem.rs: Machine trait (on_start/handle), Resolution<S> with From<S>, Cx (on_unhandled), Reply<T> move-only reply handle, StatemRef (send/call), spawn + inbox loop. Real time only; Postpone + Cx timeout arming are in the type surface but not yet acted on (chunks 2-3). - examples/statem_switch.rs: the RFC Switch machine hand-written against the primitives, tagged USER vs MACRO to mark what a macro would generate. Asserts the RFC end-state (flips=1, enters=3). - tests/statem.rs: call/cast round-trip, enter-on-start/transition-not-stay, panicking-handler -> Down. Reply<T> included (the call helper needs a handle type; keeps the example true to the RFC surface) but isolated and trivially removable if we drop it.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
//! A hand-written `gen_statem`, written directly against the chunk-1 primitives
|
||||
//! in `smarm::statem` — no macro. This is the RFC 017 `Switch` example, and its
|
||||
//! purpose is to be the **evaluation artifact**: it shows exactly the shape the
|
||||
//! deferred `statem!` macro would have to generate, so we can judge what the
|
||||
//! macro actually buys before committing to building one.
|
||||
//!
|
||||
//! Run with: `cargo run --example statem_switch`
|
||||
//!
|
||||
//! Sections are tagged // USER (you'd hand-write this with or without a macro)
|
||||
//! and // MACRO (the boilerplate a `statem!` would emit for you).
|
||||
|
||||
use smarm::run;
|
||||
use smarm::statem::{self, Cx, Machine, Reply, Resolution, StatemRef};
|
||||
|
||||
// ---- USER: the four hand-written types ------------------------------------
|
||||
// In the macro world these are unchanged — the macro never generates them.
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Switch {
|
||||
Off,
|
||||
On,
|
||||
}
|
||||
|
||||
struct Counts {
|
||||
flips: u32,
|
||||
enters: u32,
|
||||
}
|
||||
|
||||
enum SwitchCast {
|
||||
Flip,
|
||||
}
|
||||
|
||||
enum SwitchCall {
|
||||
GetCount(Reply<u32>),
|
||||
GetEnters(Reply<u32>),
|
||||
}
|
||||
|
||||
// ---- MACRO: the unified event ---------------------------------------------
|
||||
// The user's two message enums folded together. Chunk 1 has no internal
|
||||
// variants yet; chunks 2–3 add `StateTimeout` / `Timeout(name)` here.
|
||||
|
||||
enum Ev {
|
||||
Cast(SwitchCast),
|
||||
Call(SwitchCall),
|
||||
}
|
||||
|
||||
// ---- MACRO: the machine struct + state cell -------------------------------
|
||||
// `state` is the private cell; the `handle` body below is its sole writer.
|
||||
|
||||
struct SwitchSm {
|
||||
state: Switch,
|
||||
data: Counts,
|
||||
}
|
||||
|
||||
impl SwitchSm {
|
||||
// MACRO: `Switch::start` in the RFC; spawns the actor, hands back a ref.
|
||||
fn start(init: Switch, data: Counts) -> StatemRef<SwitchSm> {
|
||||
statem::spawn(SwitchSm { state: init, data })
|
||||
}
|
||||
|
||||
// MACRO: the `enter` dispatch, assembled from the per-state `enter` arms.
|
||||
// USER wrote the arm bodies (`data.enters += 1`); the macro wrote the match
|
||||
// and the `()` return. `enter` runs side effects only — it cannot transition.
|
||||
fn enter(&mut self, s: Switch, _cx: &mut Cx<Ev>) {
|
||||
match s {
|
||||
Switch::Off => self.data.enters += 1,
|
||||
Switch::On => self.data.enters += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for SwitchSm {
|
||||
type Ev = Ev;
|
||||
|
||||
// MACRO: run the initial state's enter.
|
||||
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;
|
||||
|
||||
// MACRO: the `(state, event)` dispatch table. USER wrote each arm tail
|
||||
// (the body + the ending state tag); the macro qualified bare tags
|
||||
// (`On` -> `Switch::On`), wrapped them in `.into()`, assembled the match,
|
||||
// and — in the real macro — would have edge-checked each tail tag
|
||||
// against the `transitions { Off => On, On => Off }` table at expand
|
||||
// time. Here that check is on the honour system.
|
||||
let next: Resolution<Switch> = match (self.state, ev) {
|
||||
(Switch::Off, Ev::Cast(SwitchCast::Flip)) => {
|
||||
self.data.flips += 1;
|
||||
Switch::On.into()
|
||||
}
|
||||
(Switch::On, Ev::Cast(SwitchCast::Flip)) => Switch::Off.into(),
|
||||
(Switch::Off, Ev::Call(SwitchCall::GetCount(r))) => {
|
||||
r.reply(self.data.flips);
|
||||
Switch::Off.into() // stay = return the current tag
|
||||
}
|
||||
(Switch::On, Ev::Call(SwitchCall::GetCount(r))) => {
|
||||
r.reply(self.data.flips);
|
||||
Switch::On.into()
|
||||
}
|
||||
(Switch::Off, Ev::Call(SwitchCall::GetEnters(r))) => {
|
||||
r.reply(self.data.enters);
|
||||
Switch::Off.into()
|
||||
}
|
||||
(Switch::On, Ev::Call(SwitchCall::GetEnters(r))) => {
|
||||
r.reply(self.data.enters);
|
||||
Switch::On.into()
|
||||
}
|
||||
// The macro would emit `_ => Resolution::Unhandled` here for a
|
||||
// machine whose table is non-exhaustive; this one covers every
|
||||
// (state, event) pair, so an added arm would be unreachable.
|
||||
};
|
||||
|
||||
// MACRO: the resolution dispatch — identical in every generated machine.
|
||||
match next {
|
||||
Resolution::To(s) if s == prev => {} // stay: no enter, no reset
|
||||
Resolution::To(s) => {
|
||||
self.state = s; // <- sole writer of the state cell
|
||||
// chunk 2: self.timers.clear_state_timeout();
|
||||
self.enter(s, cx);
|
||||
// chunk 3: cx.replay(&mut self.postponed);
|
||||
}
|
||||
Resolution::Postpone => {} // chunk 3
|
||||
Resolution::Unhandled => cx.on_unhandled(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
run(|| {
|
||||
// USER: the client side. With a macro these would be `sw.cast(..)` /
|
||||
// `sw.call(SwitchCall::GetCount)`; without it, the `Ev::Cast` / `Ev::Call`
|
||||
// wrap is explicit — note how mechanical it is.
|
||||
let sw = SwitchSm::start(Switch::Off, Counts { flips: 0, enters: 0 });
|
||||
|
||||
sw.send(Ev::Cast(SwitchCast::Flip)).unwrap(); // Off -> On
|
||||
sw.send(Ev::Cast(SwitchCast::Flip)).unwrap(); // On -> Off
|
||||
|
||||
let flips = sw.call(|r| Ev::Call(SwitchCall::GetCount(r))).unwrap();
|
||||
let enters = sw.call(|r| Ev::Call(SwitchCall::GetEnters(r))).unwrap();
|
||||
|
||||
// RFC's stated end state after two flips: Counts { flips: 1, enters: 3 }
|
||||
// (initial Off entry + On entry + Off entry).
|
||||
println!("after two flips: flips={flips}, enters={enters}");
|
||||
assert_eq!(flips, 1, "turned On once across the two flips");
|
||||
assert_eq!(enters, 3, "initial Off + On + Off entries");
|
||||
println!("ok");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user