gen_statem: postpone events for replay after a transition
A `=> postpone` row (cast/call/info) defers the current event untouched, to be replayed after the next real transition. `handle` is now two-phase: a borrow-only postpone pre-pass that hands the event back as `Step::Postponed(ev)`, then the existing consuming `match (state, event)`. The loop owns a FIFO queue, drained in the new state ahead of further intake; a replayed event may postpone again. A postponed `call` keeps its Reply, so a later state answers it. `handle` returns `Step` (Postponed / Transitioned / Stayed) so the loop can see both deferral and transition without reading the state cell. `Resolution::Postpone` is removed: postpone is a pre-dispatch routing decision, not a consuming-dispatch outcome.
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
#![deny(dead_code, unreachable_patterns)]
|
||||
|
||||
use smarm::run;
|
||||
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, GenStatemRef};
|
||||
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, Step, GenStatemRef};
|
||||
|
||||
// === user types ============================================================
|
||||
|
||||
@@ -50,6 +50,7 @@ enum Door {
|
||||
struct Data {
|
||||
enters: u32, // total state entries (incl. initial)
|
||||
pushes: u32, // times a push closed the door
|
||||
knocks: u32, // knocks answered (a Locked knock is postponed, then counted)
|
||||
}
|
||||
|
||||
enum Cast {
|
||||
@@ -57,12 +58,14 @@ enum Cast {
|
||||
Pull,
|
||||
Lock,
|
||||
Unlock(u32), // carries a key
|
||||
Knock, // counted when the door is reachable; postponed while Locked
|
||||
}
|
||||
|
||||
enum Call {
|
||||
GetState(Reply<Door>),
|
||||
GetEnters(Reply<u32>),
|
||||
GetPushes(Reply<u32>),
|
||||
GetKnocks(Reply<u32>),
|
||||
}
|
||||
|
||||
enum Ev {
|
||||
@@ -120,7 +123,7 @@ impl DoorSm {
|
||||
fn start(init: Door) -> GenStatemRef<DoorSm> {
|
||||
spawn(DoorSm {
|
||||
state: init,
|
||||
data: Data { enters: 0, pushes: 0 },
|
||||
data: Data { enters: 0, pushes: 0, knocks: 0 },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,18 +153,32 @@ impl Machine for DoorSm {
|
||||
self.enter(cx);
|
||||
}
|
||||
|
||||
fn handle(&mut self, ev: Ev, cx: &mut Cx<Ev>) {
|
||||
fn handle(&mut self, ev: Ev, cx: &mut Cx<Ev>) -> Step<Ev> {
|
||||
let prev = self.state;
|
||||
|
||||
// ---- the transition table -----------------------------------------
|
||||
// This match is total over (Door, Ev). Read it as the declared graph:
|
||||
// each `=> To(x)` is an edge, each `=> Unhandled` an explicit refusal.
|
||||
// ---- phase 1: postpone routing (borrow-only) ----------------------
|
||||
// A deferred event is handed back untouched for the loop's postpone
|
||||
// queue; no handler code runs on it. Here: a knock at a locked door.
|
||||
// (The macro emits this as a `match (state, &ev)` yielding a bool; the
|
||||
// hand-written form can just test directly.)
|
||||
if let (Door::Locked, Ev::Cast(Cast::Knock)) = (prev, &ev) {
|
||||
return Step::Postponed(ev);
|
||||
}
|
||||
|
||||
// ---- phase 2: the consuming transition table ----------------------
|
||||
// Total over (Door, Ev). Read it as the declared graph: each `=> To(x)`
|
||||
// is an edge, each `=> Unhandled` an explicit refusal. The postponed
|
||||
// pair above reappears as `unreachable!` so the match stays total.
|
||||
let res: Resolution<Door> = match (self.state, ev) {
|
||||
// --- Open -------------------------------------------------------
|
||||
(Door::Open, Ev::Cast(Cast::Push)) => {
|
||||
on_push(&mut self.data);
|
||||
Resolution::To(Door::Closed)
|
||||
}
|
||||
(Door::Open, Ev::Cast(Cast::Knock)) => {
|
||||
self.data.knocks += 1;
|
||||
Resolution::To(prev)
|
||||
}
|
||||
(Door::Open, Ev::Cast(Cast::Pull | Cast::Lock | Cast::Unlock(_))) => {
|
||||
Resolution::Unhandled
|
||||
}
|
||||
@@ -169,12 +186,20 @@ impl Machine for DoorSm {
|
||||
// --- Closed -----------------------------------------------------
|
||||
(Door::Closed, Ev::Cast(Cast::Pull)) => Resolution::To(Door::Open),
|
||||
(Door::Closed, Ev::Cast(Cast::Lock)) => Resolution::To(Door::Locked),
|
||||
(Door::Closed, Ev::Cast(Cast::Knock)) => {
|
||||
self.data.knocks += 1;
|
||||
Resolution::To(prev)
|
||||
}
|
||||
(Door::Closed, Ev::Cast(Cast::Push | Cast::Unlock(_))) => Resolution::Unhandled,
|
||||
|
||||
// --- Locked (branching row: handler picks within UnlockOutcome) -
|
||||
(Door::Locked, Ev::Cast(Cast::Unlock(key))) => {
|
||||
Resolution::To(on_unlock(key).into())
|
||||
}
|
||||
// Routed out in phase 1; listed only to keep this match total.
|
||||
(Door::Locked, Ev::Cast(Cast::Knock)) => {
|
||||
unreachable!("postponed event is replayed, not dispatched here")
|
||||
}
|
||||
(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => {
|
||||
Resolution::Unhandled
|
||||
}
|
||||
@@ -192,6 +217,10 @@ impl Machine for DoorSm {
|
||||
r.reply(self.data.pushes);
|
||||
Resolution::To(prev)
|
||||
}
|
||||
(_, Ev::Call(Call::GetKnocks(r))) => {
|
||||
r.reply(self.data.knocks);
|
||||
Resolution::To(prev)
|
||||
}
|
||||
|
||||
// --- timeouts: an Open door auto-closes; others have none armed --
|
||||
(Door::Open, Ev::StateTimeout) => Resolution::To(Door::Closed),
|
||||
@@ -209,14 +238,17 @@ impl Machine for DoorSm {
|
||||
|
||||
// ---- apply the resolution -----------------------------------------
|
||||
match res {
|
||||
Resolution::To(s) if s == prev => {} // stay: no enter
|
||||
Resolution::To(s) if s == prev => Step::Stayed, // 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);
|
||||
Step::Transitioned
|
||||
}
|
||||
Resolution::Unhandled => {
|
||||
cx.on_unhandled();
|
||||
Step::Stayed
|
||||
}
|
||||
Resolution::Postpone => unreachable!("postpone is not generated yet"),
|
||||
Resolution::Unhandled => cx.on_unhandled(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,9 +258,10 @@ fn main() {
|
||||
let door = DoorSm::start(Door::Closed);
|
||||
|
||||
door.send(Ev::Cast(Cast::Lock)).unwrap(); // Closed -> Locked
|
||||
door.send(Ev::Cast(Cast::Knock)).unwrap(); // Locked: postponed (not yet counted)
|
||||
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::Unlock(0))).unwrap(); // Locked: wrong key -> stay (knock still deferred)
|
||||
door.send(Ev::Cast(Cast::Unlock(CODE))).unwrap(); // Locked -> Closed; deferred Knock replays here
|
||||
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
|
||||
@@ -241,11 +274,13 @@ fn main() {
|
||||
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();
|
||||
let knocks = door.call(|r| Ev::Call(Call::GetKnocks(r))).unwrap();
|
||||
|
||||
println!("state={st:?} enters={enters} pushes={pushes}");
|
||||
println!("state={st:?} enters={enters} pushes={pushes} knocks={knocks}");
|
||||
assert_eq!(st, Door::Closed); // auto-closed by the state-timeout
|
||||
assert_eq!(enters, 5); // Closed(start) + Locked + Closed + Open + Closed
|
||||
assert_eq!(pushes, 0); // no push ever closed it this run
|
||||
assert_eq!(knocks, 1); // the locked-door knock, replayed once unlocked
|
||||
println!("ok");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user