|
fsm 0.4.1
Header-only C++20 hierarchical table-driven FSM
|
The state machine engine interpreting a Traits' constexpr tables. More...
#include <fsm.hpp>
Public Types | |
| using | state_type = typename Traits::State |
| using | event_type = typename Traits::Event |
| using | context_type = typename Traits::Context |
| using | rows_array = std::remove_cvref_t< decltype(Traits::rows)> |
| using | specs_array = std::remove_cvref_t< decltype(Traits::states)> |
| using | row_type = typename rows_array::value_type |
| using | spec_type = typename specs_array::value_type |
| using | payload_type = typename row_type::payload_type |
| void if none declared | |
| using | reason_type = typename row_type::reason_type |
| using | time_type = typename detail::time_of< Traits >::type |
| using | result_type = result< state_type, event_type, reason_type > |
| using | observer_type = observer< state_type, event_type, reason_type > |
Public Member Functions | |
| constexpr | machine (context_type &ctx) |
| Bind the machine to a caller-owned context. Does not enter any state — call start() with the current time. | |
| machine (const machine &)=delete | |
| machine & | operator= (const machine &)=delete |
| void | start (time_type now) |
| Enter the initial state chain (entry actions run, deadlines arm). Idempotent: a second call is a no-op. Entry hooks run under the reentrancy guard: a dispatch() from inside one is refused (refused_reentrant), exactly as during a normal transition. | |
| bool | post (event_type e) |
| Queue an event for delivery after the current dispatch completes (run-to-completion), instead of dispatching immediately. | |
| size_t | posts_pending () const |
| Number of post()ed events still queued (not yet drained). | |
| template<event_type E, class Arg > requires (!std::is_void_v<payload_type> && requires(const Arg& a) { { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>; }) | |
| bool | post (const Arg &arg) |
| Typed post(): same factory-checked construction as the typed dispatch() wrapper, for run-to-completion follow-ups. | |
| template<event_type E> requires (!std::is_void_v<payload_type> && requires { { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>; }) | |
| bool | post () |
| Typed post() for events whose factory takes no data. | |
| template<class P = payload_type> requires (!std::is_void_v<P> && std::is_same_v<P, payload_type>) | |
| bool | post (event_type e, const P &p) |
| post() carrying input data (machines with a Payload). | |
| result_type | dispatch (event_type e, time_type now) |
| Deliver an event and return the verdict. | |
| template<class P = payload_type> requires (!std::is_void_v<P> && std::is_same_v<P, payload_type>) | |
| result_type | dispatch (event_type e, const P &p, time_type now) |
| Deliver an event carrying input data (machines with a Payload). | |
| template<event_type E, class Arg > requires (!std::is_void_v<payload_type> && requires(const Arg& a) { { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>; }) | |
| result_type | dispatch (const Arg &arg, time_type now) |
| Typed dispatch: the event is a compile-time value, the payload is built by the traits' factory — wrong data for the event is a compile error. | |
| template<event_type E> requires (!std::is_void_v<payload_type> && requires { { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>; }) | |
| result_type | dispatch (time_type now) |
| Typed dispatch for events whose factory takes no data. | |
| bool | next_deadline (time_type &out) const |
| Earliest armed deadline across the machine's own timeout slots. | |
| unsigned | service (time_type now) |
| Fire every expired deadline, earliest first. | |
| state_type | state () const |
| Current leaf state. | |
| bool | in (state_type s) const |
True if s is the current leaf or one of its active ancestors. | |
| bool | started () const |
| True once start() has run. | |
| void | set_observer (const observer_type *o) |
| Attach a debug observer (nullptr detaches). Not owned — the observer must outlive its attachment. Compiled out entirely when FSM_ENABLE_OBSERVER is 0. | |
| template<class W > | |
| void | dump (W &&write) const |
Stream a human-readable description of the machine — the live table, current leaf, and armed deadlines — through write. | |
Static Public Member Functions | |
| template<class F > | |
| static constexpr void | for_each_row (F &&f) |
Visit every transition row as f(row, index) (constexpr-capable). | |
| template<class F > | |
| static constexpr void | for_each_state (F &&f) |
Visit every state spec as f(spec, index) (constexpr-capable). | |
| static constexpr void | put_reason_name (detail::line_buf &lb, reason_type r) |
Append the (hooked or numeric) name of refusal reason r to lb. | |
| static constexpr void | put_state_name (detail::line_buf &lb, state_type s) |
Append the (hooked or numeric) name of s to lb. | |
| static constexpr void | put_event_name (detail::line_buf &lb, event_type e) |
Append the (hooked or numeric) name of e to lb. | |
Static Public Attributes | |
| static constexpr size_t | state_count = static_cast<size_t>(state_type::Count) |
| static constexpr size_t | event_count = static_cast<size_t>(event_type::Count) |
| static constexpr size_t | row_count = Traits::rows.size() |
| static constexpr bool | strict = detail::strict_of<Traits>() |
The state machine engine interpreting a Traits' constexpr tables.
| Traits | struct providing: State/Event enum classes (dense, Count sentinel), Context, states (std::array of fsm::state_spec), rows (std::array of fsm::row), initial; optionally Time (default uint32_t), strict (default true), state_name/event_name hooks. Payload and Reason are deduced from the rows' element type. |
Instantiation static_asserts the full validator suite; a malformed table is a compile error, not a runtime surprise.
The machine owns no clock: start, dispatch and service take the current monotonic time. Arm, fire and transit therefore all happen on the caller's task — the design assumes (and only supports) single-task use.
| using fsm::machine< Traits >::context_type = typename Traits::Context |
| using fsm::machine< Traits >::event_type = typename Traits::Event |
| using fsm::machine< Traits >::observer_type = observer<state_type, event_type, reason_type> |
| using fsm::machine< Traits >::payload_type = typename row_type::payload_type |
void if none declared
| using fsm::machine< Traits >::reason_type = typename row_type::reason_type |
| using fsm::machine< Traits >::result_type = result<state_type, event_type, reason_type> |
| using fsm::machine< Traits >::row_type = typename rows_array::value_type |
| using fsm::machine< Traits >::rows_array = std::remove_cvref_t<decltype(Traits::rows)> |
| using fsm::machine< Traits >::spec_type = typename specs_array::value_type |
| using fsm::machine< Traits >::specs_array = std::remove_cvref_t<decltype(Traits::states)> |
| using fsm::machine< Traits >::state_type = typename Traits::State |
| using fsm::machine< Traits >::time_type = typename detail::time_of<Traits>::type |
|
inlineexplicitconstexpr |
Bind the machine to a caller-owned context. Does not enter any state — call start() with the current time.
|
delete |
Not copyable: a machine is a unique stateful device bound to one context — a copy would alias the context and duplicate armed deadlines. Construct machines in place.
|
inline |
Typed dispatch: the event is a compile-time value, the payload is built by the traits' factory — wrong data for the event is a compile error.
Enabled per event by declaring an overload in the traits:
Then m.dispatch<Ev::Call>(FloorCall{.floor = 4}, now) constructs the machine Payload through that factory; passing anything the factory does not accept for Ev::Call fails overload resolution. Rows, guards and actions are unchanged — they still receive the machine-wide Payload.
|
inline |
Deliver an event carrying input data (machines with a Payload).
The payload is forwarded by const reference to every guard and action evaluated for this event. Mixed per-event input types live inside the Payload as a tagged union / variant. The payload-less dispatch() overload forwards a value-initialized Payload{}, as do timeout-fired events from service().
|
inline |
Deliver an event and return the verdict.
Matching walks the active chain leaf → ancestors, then fsm::any rows; within a level, table order; the first row whose guard passes decides. With no deciding row the event is refused: status::refused_guard if something matched but declined, status::unhandled if nothing considered the pair at all.
|
inline |
Typed dispatch for events whose factory takes no data.
|
inline |
Stream a human-readable description of the machine — the live table, current leaf, and armed deadlines — through write.
write is called once per line with a NUL-terminated const char* (no trailing newline). No allocation. Names come from the traits' state_name/event_name hooks when present, else s<N>/e<N>. Useful for describing a machine over a control channel or log.
|
inlinestaticconstexpr |
Visit every transition row as f(row, index) (constexpr-capable).
|
inlinestaticconstexpr |
Visit every state spec as f(spec, index) (constexpr-capable).
|
inline |
True if s is the current leaf or one of its active ancestors.
|
inline |
Earliest armed deadline across the machine's own timeout slots.
The machine iterates its own storage, so a caller cannot forget a deadline source when computing how long to block. Typical loop: wait = next_deadline(dl) ? dl - now : forever; block(wait); service(now);
| [out] | out | the earliest deadline, valid when true is returned |
|
delete |
|
inline |
Typed post() for events whose factory takes no data.
|
inline |
Typed post(): same factory-checked construction as the typed dispatch() wrapper, for run-to-completion follow-ups.
|
inline |
Queue an event for delivery after the current dispatch completes (run-to-completion), instead of dispatching immediately.
Callable from guards, actions and entry/exit hooks — the places where dispatch() is refused as reentrant. Posted events are drained FIFO before the outermost dispatch()/service()/start() call returns to the caller; each drained event goes through the full table with its own verdict (visible to the observer). Called outside any dispatch, the events are delivered on the next dispatch()/service() call.
This is an ordered outbox, not deferred-event semantics: a posted event that no row accepts is refused like any other, never parked for a later state. Bounds: the queue holds Traits::post_queue_depth entries (default 4) — post() returns false (and drops the event) when full or before start(); a drain processes at most 4 * depth events per top-level call as livelock protection (a self-sustaining post cycle is a table bug), leaving any excess queued for the next call.
|
inline |
post() carrying input data (machines with a Payload).
|
inline |
Number of post()ed events still queued (not yet drained).
Zero after any top-level dispatch()/service()/start() returns, unless the livelock fuse truncated a self-sustaining post chain — which makes this the direct settle check: m.dispatch(e, now); m.posts_pending() == 0 proves the cascade completed within the fuse bound. Also useful as a debug assertion in the event loop.
|
inlinestaticconstexpr |
Append the (hooked or numeric) name of e to lb.
|
inlinestaticconstexpr |
Append the (hooked or numeric) name of refusal reason r to lb.
|
inlinestaticconstexpr |
Append the (hooked or numeric) name of s to lb.
|
inline |
Fire every expired deadline, earliest first.
Each expired slot is disarmed and its declared event dispatched through the normal table (full verdict, observer, refusal semantics — a refused or unhandled timeout event does not re-fire). Transitions caused by one firing can arm or disarm others; the scan repeats until nothing armed is expired. An iteration fuse of 4 * state_count bounds pathological zero-delay self-loops.
Reentrancy: service() called from inside a guard/action/hook is a no-op returning 0 — the expired slots stay armed and fire on the next top-level service() call (disarming them here would lose their events, since the nested dispatch would be refused as reentrant).
| now | current monotonic time |
|
inline |
Attach a debug observer (nullptr detaches). Not owned — the observer must outlive its attachment. Compiled out entirely when FSM_ENABLE_OBSERVER is 0.
|
inline |
Enter the initial state chain (entry actions run, deadlines arm). Idempotent: a second call is a no-op. Entry hooks run under the reentrancy guard: a dispatch() from inside one is refused (refused_reentrant), exactly as during a normal transition.
| now | current monotonic time |
|
inline |
True once start() has run.
|
inline |
Current leaf state.
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |