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:
smarm-agent
2026-06-20 13:02:15 +00:00
parent acf67fef06
commit 531571bfa5
4 changed files with 439 additions and 66 deletions
+212 -50
View File
@@ -57,13 +57,23 @@
//! the corresponding internal event and runs it through the normal `handle`
//! dispatch.
//!
//! [`Resolution::Postpone`] is part of the type surface but is not yet wired up.
//! ## Postpone
//!
//! A `=> postpone` row defers the current event untouched, to be replayed after
//! the next **real transition**. The deferred event — a `cast`, `call`, or
//! `info` — moves whole onto a loop-side FIFO queue; a postponed `call` keeps
//! its [`Reply`] handle and is answered by whichever later state handles the
//! replay. On a transition the queue drains in order through the normal
//! [`handle`](Machine::handle) dispatch in the new state, ahead of any further
//! inbox or timer event; a replayed event may postpone again (it re-queues for
//! the next transition). See the macro docs for the row surface and [`Step`]
//! for how a postpone surfaces to the loop.
use crate::channel::{channel, select, Receiver, Sender};
use crate::pid::Pid;
use crate::scheduler::{cancel_timer, send_after_to, spawn as spawn_actor};
use crate::timer::TimerId;
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -100,20 +110,26 @@ pub trait Machine: Send + 'static {
/// runs the `enter` arm for the initial state.
fn on_start(&mut self, cx: &mut Cx<Self::Ev>);
/// React to one event. The body matches `(state, event)`, performs side
/// effects / replies, and ends each arm in a [`Resolution`] — typically a
/// state tag via `Tag.into()` (transition, or "stay" when it equals the
/// current tag). On a transition the body sets the state cell and runs the
/// new state's `enter`.
fn handle(&mut self, ev: Self::Ev, cx: &mut Cx<Self::Ev>);
/// React to one event, returning a [`Step`] the loop acts on. The body
/// first routes a `postpone` row (handing the event back untouched as
/// [`Step::Postponed`]); otherwise it matches `(state, event)`, performs
/// side effects / replies, and ends each arm in a [`Resolution`] — typically
/// a state tag via `Tag.into()` (transition, or "stay" when it equals the
/// current tag). On a transition the body sets the state cell, runs the new
/// state's `enter`, and returns [`Step::Transitioned`]; a stay or unmatched
/// event returns [`Step::Stayed`].
fn handle(&mut self, ev: Self::Ev, cx: &mut Cx<Self::Ev>) -> Step<Self::Ev>;
}
// ---------------------------------------------------------------------------
// Resolution
// ---------------------------------------------------------------------------
/// The outcome of handling one event, before the loop/handler acts on it:
/// dispatch reduces to `(state, event) -> Resolution<State>`.
/// The outcome of the **consuming** dispatch — the by-value `match (state,
/// event)` — before the apply-tail acts on it: `(state, event) ->
/// Resolution<State>`. Postpone is *not* here: a deferred event never reaches
/// this match (it is routed out first; see [`Step`] and the macro's two-phase
/// `handle`), so the only outcomes are a target state or "unhandled".
///
/// [`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
@@ -122,10 +138,6 @@ pub enum Resolution<S> {
/// End in state `s`. A **transition** when `s != current` (set the cell,
/// 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()`; present
/// here for forward-compatibility but not yet generated.
Postpone,
/// No arm matched `(state, event)`: log-and-drop via
/// [`Cx::on_unhandled`], mirroring `gen_server`'s handling of unexpected
/// messages.
@@ -138,6 +150,24 @@ impl<S> From<S> for Resolution<S> {
}
}
/// What one [`handle`](Machine::handle) call did, as the loop needs to see it.
/// Unlike [`Resolution`] (internal to the consuming match), this is the
/// loop-visible result, because the loop must know two things the match alone
/// doesn't surface: whether an event was **deferred** (so the loop owns it for
/// the postpone queue) and whether a **real transition** happened (so the loop
/// replays that queue). The three cases are mutually exclusive.
pub enum Step<Ev> {
/// The event was deferred by a `postpone` row and handed back untouched.
/// The loop pushes it onto the postpone queue; no state change occurred.
Postponed(Ev),
/// A real transition happened (`enter` for the new state has already run).
/// The loop replays the postpone queue in the new state.
Transitioned,
/// Handled with no transition — a stay or an unmatched event. Nothing is
/// deferred and the postpone queue is left as-is.
Stayed,
}
// ---------------------------------------------------------------------------
// Cx — the per-handler context
// ---------------------------------------------------------------------------
@@ -403,12 +433,19 @@ pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
/// turned into the matching internal event (`state_timeout` / `timeout(name)`)
/// and run through the same `handle` dispatch as an inbox event — the
/// gen_statem model, where timeouts surface as ordinary events.
///
/// The loop owns the **postpone queue**: a `handle` that defers its event hands
/// it back ([`Step::Postponed`]) for the queue; a `handle` that transitions
/// ([`Step::Transitioned`]) triggers a [`replay`] of the queue in the new
/// state, ahead of the next intake.
fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
let (sys_tx, sys_rx) = channel::<Sys>();
let reg = Arc::new(Mutex::new(Timers::new()));
// The loop owns `cx` (and through it a `sys_tx` clone) for its whole life,
// so the sys arm never closes from under us — no auto-close dance needed.
let mut cx = Cx::new(sys_tx, reg.clone());
// Events deferred by `postpone` rows, replayed FIFO on the next transition.
let mut postpone: VecDeque<M::Ev> = VecDeque::new();
machine.on_start(&mut cx);
loop {
// Timer arm first: a ready fire is taken in preference to the inbox.
@@ -442,7 +479,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
}
};
if let Some(ev) = ev {
machine.handle(ev, &mut cx);
dispatch(&mut machine, &mut cx, &mut postpone, ev);
}
}
// Single-receiver: nothing can drain the arm between select's
@@ -454,7 +491,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
}
} else {
match rx.try_recv() {
Ok(Some(ev)) => machine.handle(ev, &mut cx),
Ok(Some(ev)) => dispatch(&mut machine, &mut cx, &mut postpone, ev),
Ok(None) => debug_assert!(false, "ready inbox was empty"),
// All GenStatemRefs dropped → inbox closed → shutdown.
Err(_) => break,
@@ -466,6 +503,51 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
}
}
/// Run one event through `handle` and act on its [`Step`]: stash a deferred
/// event on the postpone queue, or — on a real transition — [`replay`] the
/// queue in the new state. A stay/unmatched event needs nothing further.
fn dispatch<M: Machine>(
machine: &mut M,
cx: &mut Cx<M::Ev>,
postpone: &mut VecDeque<M::Ev>,
ev: M::Ev,
) {
match machine.handle(ev, cx) {
Step::Postponed(ev) => postpone.push_back(ev),
Step::Stayed => {}
Step::Transitioned => replay(machine, cx, postpone),
}
}
/// Replay deferred events after a real transition: each goes back through
/// `handle` in FIFO order, in the now-current state. An event that postpones
/// again re-queues (to wait for the *next* transition); one that transitions
/// re-arms the replay, so a later state can in turn drain what is still pending.
/// Subsequent events in a batch already see the post-transition state, since
/// `handle` reads the live state cell — the outer loop only re-runs to give
/// re-queued events another pass once a transition has occurred within a batch.
fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeque<M::Ev>) {
loop {
if postpone.is_empty() {
return;
}
// Take the current backlog; anything deferred during this pass lands in
// the now-empty queue and is only retried if this pass also transitioned.
let batch = std::mem::take(postpone);
let mut transitioned = false;
for ev in batch {
match machine.handle(ev, cx) {
Step::Postponed(ev) => postpone.push_back(ev),
Step::Stayed => {}
Step::Transitioned => transitioned = true,
}
}
if !transitioned {
return;
}
}
}
// ---------------------------------------------------------------------------
// gen_statem! — the authoring macro (total-match dispatch)
// ---------------------------------------------------------------------------
@@ -567,6 +649,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
/// // * a block ending in one `{ data.enters += 1; Door::Closed }`
/// // * a successor-enum value `on_unlock(key)` (branching row)
/// // * the keyword `unhandled` (explicit refusal)
/// // * the keyword `postpone` (defer until next
/// // transition; cast/call/
/// // info only)
/// on Door::Open => {
/// cast DoorCast::Push => Door::Closed,
/// // An armed state-timeout surfaces as an ordinary event:
@@ -625,6 +710,16 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
/// trip `E0004`.
/// * **Branching rows** return a successor enum that `impl`s `From<_>` for the
/// state type; the macro supplies the outer `.into()`.
/// * **`postpone`** defers the current event untouched, to be replayed after the
/// next *real transition* (a stay does not trigger replay). It is available
/// for `cast`, `call`, and `info` rows — not the timeout events. The deferred
/// event keeps any `Reply` it carries, so a postponed `call` is answered by
/// whichever later state handles the replay. The body runs *no* code (the
/// event is untouched), so a `postpone` row is just `cast Foo(_) => postpone,`;
/// prefer a non-binding pattern, and if you guard it, the guard must not depend
/// on the event's payload (it is evaluated in a borrow pre-pass). On a
/// transition the queue drains FIFO, ahead of further inbox/timer events; a
/// replayed event may postpone again (it re-queues for the next transition).
///
/// # What it emits
///
@@ -704,138 +799,205 @@ macro_rules! gen_statem {
#[allow(unused_variables)]
#[deny(unreachable_patterns)] // conflicting rows must fail even though
// this match is external-macro-expanded
fn handle(&mut self, ev: $Ev, $cx: &mut $crate::gen_statem::Cx<$Ev>) {
fn handle(&mut self, ev: $Ev, $cx: &mut $crate::gen_statem::Cx<$Ev>)
-> $crate::gen_statem::Step<$Ev>
{
// Caller-named bindings (shared call-site hygiene, so row bodies
// can see them): `$cur` = current state tag, `$data` = &mut Data.
let $cur = self.state;
let $data = &mut self.data;
// @arms emits a block: a postpone pre-pass that `return`s
// `Step::Postponed(ev)` for a deferred event (handing it back
// untouched), then the consuming `match (state, event)` whose
// value is this `Resolution`.
let next: $crate::gen_statem::Resolution<$State> =
$crate::gen_statem!(@arms ($Ev) ($cur, ev) [ ]
$crate::gen_statem!(@arms ($Ev) ($cur, ev) [ ] [ ]
$( on $st => { $($rows)* } )+);
match next {
$crate::gen_statem::Resolution::To(s) if s == $cur => {}
$crate::gen_statem::Resolution::To(s) if s == $cur => {
$crate::gen_statem::Step::Stayed
}
$crate::gen_statem::Resolution::To(s) => {
self.state = s; // <- sole writer of the state cell
// A real transition auto-resets the state-timeout: the
// new state re-arms in its `enter` if it wants one.
$cx.__reset_state_timeout();
self.enter(s, $cx);
$crate::gen_statem::Step::Transitioned
}
$crate::gen_statem::Resolution::Postpone => {
unreachable!("postpone is not generated yet")
$crate::gen_statem::Resolution::Unhandled => {
$cx.on_unhandled();
$crate::gen_statem::Step::Stayed
}
$crate::gen_statem::Resolution::Unhandled => $cx.on_unhandled(),
}
}
}
};
// ===== @arms: build the dispatch match ==================================
// No more on-blocks: emit the (catch-all-free for cast/call/timeouts) match.
// The one macro-injected arm is the `Info` silent-drop fallback (3c): info
// is out-of-band and defaults to a drop, matching `gen_server`. It is last
// and broadest, so per-state `info` rows above it stay reachable. Cast,
// call, and both timeout events get NO fallback — a forgotten pair is still
// a non-exhaustive-match error.
(@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ]) => {
match ($ss, $se) {
$($arms)*
(_, $Ev::Info(_)) => $crate::gen_statem::Resolution::Unhandled,
// ===== @arms / @rows: build the two-phase dispatch ======================
// Two accumulators are threaded: `[ $($arms)* ]` is the phase-2 consuming
// match (by value); `[ $($post)* ]` is the phase-1 postpone router (by ref).
// A normal row feeds only phase-2; a `postpone` row feeds both — a `=> true`
// router and a `=> unreachable!` phase-2 filler that keeps the consuming
// match total.
//
// Terminal (no on-blocks left): emit the block the `handle` body assigns to
// `next`. Phase 1 routes a deferred event back out as `Step::Postponed`;
// phase 2 is the catch-all-free consuming match (the one macro-injected arm
// is the `Info` silent-drop — last and broadest, so per-state `info` rows
// stay reachable; cast/call/timeouts get no fallback, so a forgotten pair is
// still E0004).
(@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ]) => {
{
// Phase 1 — postpone routing (borrow-only). A guard on a postpone
// row runs here, against by-ref bindings, so it must not depend on
// the event's payload.
let __defer = match ($ss, &$se) {
$($post)*
_ => false,
};
if __defer {
return $crate::gen_statem::Step::Postponed($se);
}
// Phase 2 — the consuming dispatch. Each postpone pair reappears here
// as `unreachable!` so the match stays total; phase 1 already
// returned for it.
match ($ss, $se) {
$($arms)*
(_, $Ev::Info(_)) => $crate::gen_statem::Resolution::Unhandled,
}
}
};
// Open an on-block: remember its state pat, drain its rows, then continue.
(@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ]
(@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ]
on $st:pat => { $($rows:tt)* } $($more:tt)*
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se) [ $($arms)* ] ($st)
$crate::gen_statem!(@rows ($Ev) ($ss, $se) [ $($arms)* ] [ $($post)* ] ($st)
{ $($rows)* } { $($more)* })
};
// ===== @rows: drain one on-block's rows, threading the global acc ========
// ===== @rows: drain one on-block's rows, threading both accs =============
// cast, explicit refusal
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ cast $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// cast, postpone (defer the event; the replay in a later state handles it)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ cast $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
[ $($post)* ($st, $Ev::Cast($ev)) $(if $g)? => true, ]
($st) { $($rows)* } { $($more)* })
};
// cast, transition / stay / branch
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ cast $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// call, explicit refusal
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ call $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// call, postpone (the Reply rides inside the event onto the queue)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ call $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
[ $($post)* ($st, $Ev::Call($ev)) $(if $g)? => true, ]
($st) { $($rows)* } { $($more)* })
};
// call, transition / stay / branch
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ call $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// info, explicit refusal
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ info $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// info, postpone
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ info $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
[ $($post)* ($st, $Ev::Info($ev)) $(if $g)? => true, ]
($st) { $($rows)* } { $($more)* })
};
// info, transition / stay / branch
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ info $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// state_timeout, explicit refusal (unit event — no pattern)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
// state_timeout, explicit refusal (unit event — no pattern; not postponable)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ state_timeout $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// state_timeout, transition / stay / branch
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ state_timeout $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// timeout, explicit refusal (pattern matches the name)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
// timeout, explicit refusal (pattern matches the name; not postponable)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ timeout $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// timeout, transition / stay / branch
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ timeout $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
) => {
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
[ $($post)* ]
($st) { $($rows)* } { $($more)* })
};
// this block is drained: hand the remaining on-blocks back to @arms
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] ($st:pat)
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
{ } { $($more:tt)* }
) => {
$crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] $($more)*)
$crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] [ $($post)* ] $($more)*)
};
}