gen_statem: state and named timeouts, info events
Add the two timeout flavours, both surfacing as ordinary events matched in on-state arms: - cx.state_timeout(d): fires a state_timeout event after d in the current state, auto-reset by the loop on every real transition. - cx.timeout(name, d): fires a timeout(name) event after d, surviving state changes, keyed by name, with cx.cancel_timeout(name). Both ride the existing timer min-heap via send_after_to onto a new per-loop system channel, selected above the inbox so a fire can't be starved by inbox traffic. A local-id stamp on each fire lets a reset/cancel that loses the race discard a stale fire. The macro grows an info: clause and folds three internal Ev variants (Info, StateTimeout, Timeout) alongside cast/call, with new row keywords info / state_timeout / timeout. Unmatched info silently drops (the gen_server default); state/named timeouts have no default, so a state that can see one must handle it or the match is non-exhaustive. Rename the hand-written expansion-target example fused -> expanded, retire the deprecated Switch demo machine (its round-trip / enter / panic-down coverage moves onto the timer machine), and refresh the macro docs to the door machine. cargo build --all-targets warning-free; cargo test green.
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
//! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents
|
||||
//! how to make each of the four guarantees fire.
|
||||
//!
|
||||
//! Run: `cargo run --example gen_statem_fused`
|
||||
//! Run: `cargo run --example gen_statem_expanded`
|
||||
|
||||
#![deny(dead_code, unreachable_patterns)]
|
||||
|
||||
@@ -68,6 +68,11 @@ enum Call {
|
||||
enum Ev {
|
||||
Cast(Cast),
|
||||
Call(Call),
|
||||
// The runtime's internal events. `Info` is out-of-band (here unused, so
|
||||
// `()`); `StateTimeout` / `Timeout` are timer fires the loop feeds back in.
|
||||
Info(()),
|
||||
StateTimeout,
|
||||
Timeout(&'static str),
|
||||
}
|
||||
|
||||
const CODE: u32 = 1234;
|
||||
@@ -119,14 +124,28 @@ impl DoorSm {
|
||||
})
|
||||
}
|
||||
|
||||
fn enter(&mut self, _cx: &mut Cx<Ev>) {
|
||||
fn enter(&mut self, cx: &mut Cx<Ev>) {
|
||||
self.data.enters += 1;
|
||||
// A state-timeout: an Open door auto-closes after a quiet window. The
|
||||
// loop auto-resets it on any transition, so it fires only if the door is
|
||||
// still Open when it elapses.
|
||||
if self.state == Door::Open {
|
||||
cx.state_timeout(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for DoorSm {
|
||||
type Ev = Ev;
|
||||
|
||||
fn state_timeout_ev() -> Ev {
|
||||
Ev::StateTimeout
|
||||
}
|
||||
|
||||
fn timeout_ev(name: &'static str) -> Ev {
|
||||
Ev::Timeout(name)
|
||||
}
|
||||
|
||||
fn on_start(&mut self, cx: &mut Cx<Ev>) {
|
||||
self.enter(cx);
|
||||
}
|
||||
@@ -173,6 +192,19 @@ impl Machine for DoorSm {
|
||||
r.reply(self.data.pushes);
|
||||
Resolution::To(prev)
|
||||
}
|
||||
|
||||
// --- timeouts: an Open door auto-closes; others have none armed --
|
||||
(Door::Open, Ev::StateTimeout) => Resolution::To(Door::Closed),
|
||||
(_, Ev::StateTimeout) => Resolution::Unhandled,
|
||||
(_, Ev::Timeout(name)) => {
|
||||
// No named timeout is armed in this run; a real handler would
|
||||
// dispatch on `name`. Acknowledge it to exercise the field.
|
||||
let _ = name;
|
||||
Resolution::Unhandled
|
||||
}
|
||||
|
||||
// --- out-of-band info: silent drop (the gen_server default) ------
|
||||
(_, Ev::Info(_)) => Resolution::Unhandled,
|
||||
};
|
||||
|
||||
// ---- apply the resolution -----------------------------------------
|
||||
@@ -180,6 +212,7 @@ impl Machine for DoorSm {
|
||||
Resolution::To(s) if s == prev => {} // stay: no enter
|
||||
Resolution::To(s) => {
|
||||
self.state = s; // sole writer of the state cell
|
||||
cx.__reset_state_timeout(); // auto-reset across transitions
|
||||
self.enter(cx);
|
||||
}
|
||||
Resolution::Postpone => unreachable!("postpone is not generated yet"),
|
||||
@@ -196,17 +229,23 @@ fn main() {
|
||||
door.send(Ev::Cast(Cast::Push)).unwrap(); // Locked: Push invalid -> Unhandled
|
||||
door.send(Ev::Cast(Cast::Unlock(0))).unwrap(); // Locked: wrong key -> stay
|
||||
door.send(Ev::Cast(Cast::Unlock(CODE))).unwrap(); // Locked -> Closed
|
||||
door.send(Ev::Cast(Cast::Pull)).unwrap(); // Closed -> Open
|
||||
door.send(Ev::Cast(Cast::Push)).unwrap(); // Open -> Closed (pushes=1)
|
||||
door.send(Ev::Cast(Cast::Push)).unwrap(); // Closed: Push invalid -> Unhandled
|
||||
door.send(Ev::Cast(Cast::Pull)).unwrap(); // Closed -> Open (arms 5ms auto-close)
|
||||
door.send(Ev::Info(())).unwrap(); // out-of-band: silently dropped
|
||||
|
||||
// Wait past the auto-close window: the state-timeout fires and the door
|
||||
// closes itself, with no further input. (`smarm::sleep` parks the actor
|
||||
// without blocking a worker thread, so the timer wheel keeps turning.)
|
||||
smarm::sleep(std::time::Duration::from_millis(40));
|
||||
|
||||
let st = door.call(|r| Ev::Call(Call::GetState(r))).unwrap();
|
||||
let enters = door.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
||||
let pushes = door.call(|r| Ev::Call(Call::GetPushes(r))).unwrap();
|
||||
|
||||
println!("state={st:?} enters={enters} pushes={pushes}");
|
||||
assert_eq!(st, Door::Closed);
|
||||
assert_eq!(st, Door::Closed); // auto-closed by the state-timeout
|
||||
assert_eq!(enters, 5); // Closed(start) + Locked + Closed + Open + Closed
|
||||
assert_eq!(pushes, 1);
|
||||
assert_eq!(pushes, 0); // no push ever closed it this run
|
||||
println!("ok");
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The **same** machine as `examples/gen_statem_fused.rs`, written through the
|
||||
//! The **same** machine as `examples/gen_statem_expanded.rs`, written through the
|
||||
//! `gen_statem!` macro. Diff this file against that one to see exactly what the
|
||||
//! macro buys: every `// ===` section there that was boilerplate (the `Ev`
|
||||
//! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter`
|
||||
@@ -21,7 +21,7 @@ use smarm::gen_statem;
|
||||
use smarm::run;
|
||||
use smarm::gen_statem::Reply;
|
||||
|
||||
// === user types (identical to gen_statem_fused.rs) =========================
|
||||
// === user types (identical to gen_statem_expanded.rs) =========================
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Door {
|
||||
@@ -87,7 +87,7 @@ fn on_unlock(key: u32) -> UnlockOutcome {
|
||||
|
||||
gen_statem! {
|
||||
machine: DoorSm { state: Door, data: Data };
|
||||
event: Ev { cast: Cast, call: Call };
|
||||
event: Ev { cast: Cast, call: Call, info: () };
|
||||
|
||||
// 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
|
||||
@@ -117,6 +117,10 @@ gen_statem! {
|
||||
call Call::GetState(r) => { r.reply(prev); prev },
|
||||
call Call::GetEnters(r) => { r.reply(data.enters); prev },
|
||||
call Call::GetPushes(r) => { r.reply(data.pushes); prev },
|
||||
// This machine arms no timeouts, so refuse them everywhere. (Info has a
|
||||
// built-in silent-drop default, so it needs no row.)
|
||||
state_timeout => unhandled,
|
||||
timeout _ => unhandled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +149,7 @@ fn main() {
|
||||
|
||||
// ===========================================================================
|
||||
// BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly
|
||||
// as it does in the hand-written gen_statem_fused.rs.
|
||||
// as it does in the hand-written gen_statem_expanded.rs.
|
||||
//
|
||||
// 1. ORPHAN HANDLER (dead_code -> error):
|
||||
// add `fn on_slam(_d: &mut Data) {}` and don't reference it.
|
||||
|
||||
Reference in New Issue
Block a user