fsm 0.4.1
Header-only C++20 hierarchical table-driven FSM
Loading...
Searching...
No Matches
fsm.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
13#ifndef FSM_FSM_HPP
14#define FSM_FSM_HPP
15
18#define FSM_VERSION_MAJOR 0
19#define FSM_VERSION_MINOR 4
20#define FSM_VERSION_PATCH 1
23/*
24 * Feature gating. Under ESP-IDF, Kconfig options CONFIG_FSM_* (see the
25 * component's Kconfig file) drive the FSM_ENABLE_* macros via sdkconfig.h.
26 * On other platforms the macros can be predefined by the build system
27 * (the CMake options FSM_OBSERVER / FSM_INTROSPECTION do so) and default on.
28 */
29#if defined(ESP_PLATFORM)
30# include "sdkconfig.h"
31# ifndef FSM_ENABLE_OBSERVER
32# ifdef CONFIG_FSM_OBSERVER
33# define FSM_ENABLE_OBSERVER 1
34# else
35# define FSM_ENABLE_OBSERVER 0
36# endif
37# endif
38# ifndef FSM_ENABLE_INTROSPECTION
39# ifdef CONFIG_FSM_INTROSPECTION
40# define FSM_ENABLE_INTROSPECTION 1
41# else
42# define FSM_ENABLE_INTROSPECTION 0
43# endif
44# endif
45#endif
46#ifndef FSM_ENABLE_OBSERVER
47# define FSM_ENABLE_OBSERVER 1
48#endif
49#ifndef FSM_ENABLE_INTROSPECTION
50# define FSM_ENABLE_INTROSPECTION 1
51#endif
52
53/*
54 * Debug payload/event agreement checking: when a Traits declares
55 * payload_matches(Event, const Payload&), the erased dispatch path verifies
56 * it with FSM_PAYLOAD_ASSERT. On by default in debug builds (NDEBUG not
57 * defined); predefine FSM_ENABLE_PAYLOAD_CHECKS to force either way, or
58 * FSM_PAYLOAD_ASSERT to substitute your own handler for assert().
59 */
60#ifndef FSM_ENABLE_PAYLOAD_CHECKS
61# ifdef NDEBUG
62# define FSM_ENABLE_PAYLOAD_CHECKS 0
63# else
64# define FSM_ENABLE_PAYLOAD_CHECKS 1
65# endif
66#endif
67#if FSM_ENABLE_PAYLOAD_CHECKS && !defined(FSM_PAYLOAD_ASSERT)
68# include <cassert>
69# define FSM_PAYLOAD_ASSERT(cond) assert(cond)
70#endif
71
72/*
73 * Debug range checking for dynamic deadlines: a Traits::deadline_ms() return
74 * above half the Time range is clamped there in all builds (a mis-ranged
75 * profile value stays *long* instead of firing instantly) and additionally
76 * asserts in debug builds. Same override story as the payload checks.
77 */
78#ifndef FSM_ENABLE_DEADLINE_CHECKS
79# ifdef NDEBUG
80# define FSM_ENABLE_DEADLINE_CHECKS 0
81# else
82# define FSM_ENABLE_DEADLINE_CHECKS 1
83# endif
84#endif
85#if FSM_ENABLE_DEADLINE_CHECKS && !defined(FSM_DEADLINE_ASSERT)
86# include <cassert>
87# define FSM_DEADLINE_ASSERT(cond) assert(cond)
88#endif
89
90#include <array>
91#include <concepts>
92#include <cstddef>
93#include <cstdint>
94#include <type_traits>
95
96namespace fsm {
97
99inline constexpr uint16_t no_row = 0xFFFF;
100
107enum class status : uint8_t {
112 unhandled,
115};
116
125template <class State, class Event, class Reason>
126struct result {
128 State from;
129 State to;
130 Event event;
131 uint16_t row;
132 Reason reason;
133};
134
136template <class State, class Event, class Reason>
140
142struct any_t {
143 explicit constexpr any_t() = default;
144};
146inline constexpr any_t any{};
147
150 explicit constexpr internal_t() = default;
151};
153inline constexpr internal_t internal{};
154
157 explicit constexpr no_timeout_t() = default;
158};
160inline constexpr no_timeout_t no_timeout{};
161
172template <auto E>
173struct event_tag {
174 explicit constexpr event_tag() = default;
175};
176
178enum class from_kind : uint8_t { unspecified, any, state };
179
186template <class State>
187struct from_spec {
189 State s{};
190 constexpr from_spec() = default;
191 constexpr from_spec(State st) : k(from_kind::state), s(st) {} // NOLINT implicit
192 constexpr from_spec(any_t) : k(from_kind::any) {} // NOLINT implicit
193};
194
196enum class on_kind : uint8_t { unspecified, event };
197
199template <class Event>
200struct on_spec {
202 Event e{};
203 constexpr on_spec() = default;
204 constexpr on_spec(Event ev) : k(on_kind::event), e(ev) {} // NOLINT implicit
205};
206
208enum class target_kind : uint8_t { unspecified, to, internal_, refuse_ };
209
210namespace detail {
211template <class State>
212struct to_t {
213 State s;
214};
215template <class Reason>
216struct refuse_t {
217 Reason r;
218};
220struct empty {};
221
223template <class Context, class Payload>
224struct fn_sigs {
225 using guard = bool (*)(Context&, const Payload&);
226 using action = void (*)(Context&, const Payload&);
227};
228template <class Context>
229struct fn_sigs<Context, void> {
230 using guard = bool (*)(Context&);
231 using action = void (*)(Context&);
232};
233} // namespace detail
234
242template <class State, class Reason>
243struct target {
245 State s{};
246 Reason r{};
247 constexpr target() = default;
248 constexpr target(detail::to_t<State> t) : k(target_kind::to), s(t.s) {} // NOLINT
249 constexpr target(internal_t) : k(target_kind::internal_) {} // NOLINT
250 constexpr target(detail::refuse_t<Reason> f) // NOLINT
251 : k(target_kind::refuse_), r(f.r) {}
252};
253
258template <class State>
259constexpr detail::to_t<State> to(State s) {
260 return {s};
261}
262
270template <class Reason>
271constexpr detail::refuse_t<Reason> refuse(Reason r) {
272 return {r};
273}
274
276enum class id_kind : uint8_t { unspecified, state };
277
279template <class State>
280struct state_id {
282 State s{};
283 constexpr state_id() = default;
284 constexpr state_id(State st) : k(id_kind::state), s(st) {} // NOLINT implicit
285};
286
288enum class parent_kind : uint8_t { unspecified, root, child };
289
291struct root_t {
292 explicit constexpr root_t() = default;
293};
295inline constexpr root_t root{};
296
306template <class State>
309 State s{};
310 constexpr parent_spec() = default;
311 constexpr parent_spec(root_t) : k(parent_kind::root) {} // NOLINT implicit
312 constexpr parent_spec(parent_kind kk, State st) : k(kk), s(st) {}
313};
314
316template <class State>
317constexpr parent_spec<State> child_of(State s) {
318 return {parent_kind::child, s};
319}
320
322enum class initial_kind : uint8_t { leaf, start_at };
323
325template <class State>
330
337template <class State>
338constexpr initial_spec<State> start_at(State s) {
339 return {initial_kind::start_at, s};
340}
341
343enum class timeout_kind : uint8_t { unspecified, none, after_, after_dyn_ };
344
353template <class Event>
356 uint32_t ms = 0;
357 Event event{};
358 constexpr timeout_spec() = default;
359 constexpr timeout_spec(no_timeout_t) : k(timeout_kind::none) {} // NOLINT implicit
360 constexpr timeout_spec(timeout_kind kk, uint32_t m, Event e) : k(kk), ms(m), event(e) {}
361};
362
370template <class Event>
371constexpr timeout_spec<Event> after(uint32_t ms, Event e) {
372 return {timeout_kind::after_, ms, e};
373}
374
397template <class Event>
398constexpr timeout_spec<Event> after_dyn(Event e) {
399 return {timeout_kind::after_dyn_, 0, e};
400}
401
415template <class State, class Event, class Context, class Payload = void, class Reason = uint8_t>
416struct row {
417 using payload_type = Payload;
418 using reason_type = Reason;
420 using guard_fn = typename detail::fn_sigs<Context, Payload>::guard;
422 using action_fn = typename detail::fn_sigs<Context, Payload>::action;
423
426 guard_fn when = nullptr;
427 action_fn then = nullptr;
432 const char* label = nullptr;
433};
434
440template <class State, class Event, class Context>
452
463template <class State, class Event, class Reason>
464struct observer {
465 void* user = nullptr;
467 void (*on_event)(void* user, const result<State, Event, Reason>& r) = nullptr;
469 void (*on_transition)(void* user, State from, State to, Event e) = nullptr;
471 void (*on_timeout_fired)(void* user, State armed, Event e) = nullptr;
472};
473
474namespace detail {
475
477template <class Time>
478constexpr bool time_le(Time a, Time b) {
479 using S = std::make_signed_t<Time>;
480 return static_cast<S>(a - b) <= 0;
481}
482
484template <class Time>
485constexpr bool time_lt(Time a, Time b) {
486 using S = std::make_signed_t<Time>;
487 return static_cast<S>(a - b) < 0;
488}
489
490// ---------------------------------------------------------------------------
491// Constexpr table validators. All are pure loops (no recursion, cycle fuses)
492// so they run both inside machine<> static_asserts and at runtime in tests.
493// ---------------------------------------------------------------------------
494
496template <class Spec, size_t N, class State>
497constexpr size_t spec_index(const std::array<Spec, N>& specs, State s) {
498 for (size_t i = 0; i < N; ++i)
499 if (specs[i].state.k == id_kind::state && specs[i].state.s == s) return i;
500 return N;
501}
502
504template <class Spec, size_t N, class State>
505constexpr bool is_composite(const std::array<Spec, N>& specs, State s) {
506 for (size_t i = 0; i < N; ++i)
507 if (specs[i].parent.k == parent_kind::child && specs[i].parent.s == s) return true;
508 return false;
509}
510
512template <class Spec, size_t N, class State>
513constexpr bool is_ancestor(const std::array<Spec, N>& specs, State a, State x) {
514 State cur = x;
515 for (size_t fuse = 0; fuse <= N; ++fuse) {
516 const size_t i = spec_index(specs, cur);
517 if (i == N || specs[i].parent.k != parent_kind::child) return false;
518 cur = specs[i].parent.s;
519 if (cur == a) return true;
520 }
521 return false;
522}
523
525template <class Spec, size_t N, class State>
526constexpr size_t depth_of(const std::array<Spec, N>& specs, State s) {
527 size_t d = 0;
528 State cur = s;
529 for (size_t fuse = 0; fuse <= N; ++fuse) {
530 const size_t i = spec_index(specs, cur);
531 if (i == N || specs[i].parent.k != parent_kind::child) return d;
532 cur = specs[i].parent.s;
533 ++d;
534 }
535 return d;
536}
537
539template <class Spec, size_t N>
540constexpr size_t max_depth(const std::array<Spec, N>& specs) {
541 size_t m = 0;
542 for (size_t i = 0; i < N; ++i) {
543 if (specs[i].state.k != id_kind::state) continue;
544 const size_t d = depth_of(specs, specs[i].state.s);
545 if (d > m) m = d;
546 }
547 return m;
548}
549
551template <class Spec, size_t N, class State>
552constexpr State leaf_of(const std::array<Spec, N>& specs, State s) {
553 State cur = s;
554 for (size_t fuse = 0; fuse <= N; ++fuse) {
555 const size_t i = spec_index(specs, cur);
556 if (i == N || specs[i].initial.k != initial_kind::start_at) return cur;
557 cur = specs[i].initial.s;
558 }
559 return cur;
560}
561
563template <size_t StateCount, class Spec, size_t N>
564constexpr bool states_complete(const std::array<Spec, N>& specs) {
565 for (size_t i = 0; i < N; ++i)
566 if (specs[i].state.k != id_kind::state) return false;
567 if (N != StateCount) return false;
568 for (size_t v = 0; v < StateCount; ++v) {
569 bool found = false;
570 for (size_t i = 0; i < N; ++i)
571 if (static_cast<size_t>(specs[i].state.s) == v) found = true;
572 if (!found) return false;
573 }
574 return true;
575}
576
578template <class Spec, size_t N>
579constexpr bool no_duplicate_specs(const std::array<Spec, N>& specs) {
580 for (size_t i = 0; i < N; ++i)
581 for (size_t j = i + 1; j < N; ++j)
582 if (specs[i].state.k == id_kind::state && specs[j].state.k == id_kind::state &&
583 specs[i].state.s == specs[j].state.s)
584 return false;
585 return true;
586}
587
589template <class Spec, size_t N>
590constexpr bool parents_valid(const std::array<Spec, N>& specs) {
591 for (size_t i = 0; i < N; ++i) {
592 if (specs[i].parent.k != parent_kind::child) continue;
593 if (spec_index(specs, specs[i].parent.s) == N) return false;
594 // Walk up; a chain longer than N states is a cycle.
595 auto cur = specs[i].state.s;
596 size_t steps = 0;
597 for (; steps <= N; ++steps) {
598 const size_t p = spec_index(specs, cur);
599 if (p == N || specs[p].parent.k != parent_kind::child) break;
600 cur = specs[p].parent.s;
601 }
602 if (steps > N) return false;
603 }
604 return true;
605}
606
608template <class Spec, size_t N>
609constexpr bool composites_declare_initial(const std::array<Spec, N>& specs) {
610 for (size_t i = 0; i < N; ++i) {
611 if (specs[i].state.k != id_kind::state) continue;
612 if (is_composite(specs, specs[i].state.s) &&
613 specs[i].initial.k != initial_kind::start_at)
614 return false;
615 }
616 return true;
617}
618
620template <class Spec, size_t N>
621constexpr bool leaves_have_no_initial(const std::array<Spec, N>& specs) {
622 for (size_t i = 0; i < N; ++i) {
623 if (specs[i].state.k != id_kind::state) continue;
624 if (!is_composite(specs, specs[i].state.s) &&
625 specs[i].initial.k == initial_kind::start_at)
626 return false;
627 }
628 return true;
629}
630
635template <class Spec, size_t N>
636constexpr bool initials_belong(const std::array<Spec, N>& specs) {
637 for (size_t i = 0; i < N; ++i) {
638 if (specs[i].state.k != id_kind::state) continue;
639 if (specs[i].initial.k != initial_kind::start_at) continue;
640 const auto s = specs[i].state.s;
641 const size_t c = spec_index(specs, specs[i].initial.s);
642 if (c == N) return false;
643 if (specs[c].parent.k != parent_kind::child || specs[c].parent.s != s) return false;
644 // Descent must reach a non-start_at spec within N steps.
645 auto cur = s;
646 bool leaf_reached = false;
647 for (size_t fuse = 0; fuse <= N; ++fuse) {
648 const size_t k = spec_index(specs, cur);
649 if (k == N) return false;
650 if (specs[k].initial.k != initial_kind::start_at) {
651 leaf_reached = true;
652 break;
653 }
654 cur = specs[k].initial.s;
655 }
656 if (!leaf_reached) return false;
657 }
658 return true;
659}
660
662template <class Row, size_t M>
663constexpr bool rows_have_targets(const std::array<Row, M>& rows) {
664 for (size_t i = 0; i < M; ++i)
665 if (rows[i].to.k == target_kind::unspecified || rows[i].from.k == from_kind::unspecified ||
666 rows[i].on.k == on_kind::unspecified)
667 return false;
668 return true;
669}
670
672template <size_t StateCount, size_t EventCount, class Row, size_t M, class Spec, size_t N>
673constexpr bool rows_reference_valid(const std::array<Row, M>& rows,
674 const std::array<Spec, N>& specs) {
675 for (size_t i = 0; i < M; ++i) {
676 if (rows[i].from.k == from_kind::state && spec_index(specs, rows[i].from.s) == N)
677 return false;
678 if (rows[i].on.k != on_kind::event || static_cast<size_t>(rows[i].on.e) >= EventCount)
679 return false;
680 if (rows[i].to.k == target_kind::to && spec_index(specs, rows[i].to.s) == N) return false;
681 }
682 return true;
683}
684
686template <class Row>
687constexpr bool same_slot(const Row& a, const Row& b) {
688 if (a.from.k != b.from.k) return false;
689 if (a.from.k == from_kind::state && a.from.s != b.from.s) return false;
690 return a.on.k == on_kind::event && b.on.k == on_kind::event && a.on.e == b.on.e;
691}
692
694template <class Row, size_t M>
695constexpr bool rows_no_duplicate_unguarded(const std::array<Row, M>& rows) {
696 for (size_t i = 0; i < M; ++i)
697 for (size_t j = i + 1; j < M; ++j)
698 if (same_slot(rows[i], rows[j]) && rows[i].when == nullptr && rows[j].when == nullptr)
699 return false;
700 return true;
701}
702
704template <class Row, size_t M>
705constexpr bool rows_no_unreachable(const std::array<Row, M>& rows) {
706 for (size_t i = 0; i < M; ++i)
707 for (size_t j = i + 1; j < M; ++j)
708 if (same_slot(rows[i], rows[j]) && rows[i].when == nullptr && rows[j].when != nullptr)
709 return false;
710 return true;
711}
712
719template <class Row, size_t M>
720constexpr bool rows_refuse_have_no_action(const std::array<Row, M>& rows) {
721 for (size_t i = 0; i < M; ++i)
722 if (rows[i].to.k == target_kind::refuse_ && rows[i].then != nullptr) return false;
723 return true;
724}
725
727template <class Spec, size_t N>
728constexpr bool parents_declared(const std::array<Spec, N>& specs) {
729 for (size_t i = 0; i < N; ++i)
730 if (specs[i].parent.k == parent_kind::unspecified) return false;
731 return true;
732}
733
735template <class Spec, size_t N>
736constexpr bool any_dynamic_deadline(const std::array<Spec, N>& specs) {
737 for (size_t i = 0; i < N; ++i)
738 if (specs[i].deadline.k == timeout_kind::after_dyn_) return true;
739 return false;
740}
741
743template <class Spec, size_t N>
744constexpr bool deadlines_declared(const std::array<Spec, N>& specs) {
745 for (size_t i = 0; i < N; ++i)
746 if (specs[i].deadline.k == timeout_kind::unspecified) return false;
747 return true;
748}
749
755template <class Time, class Spec, size_t N>
756constexpr bool deadlines_fit(const std::array<Spec, N>& specs) {
757 constexpr uint64_t half = static_cast<uint64_t>(static_cast<Time>(-1)) / 2;
758 for (size_t i = 0; i < N; ++i)
759 if (specs[i].deadline.k == timeout_kind::after_ &&
760 static_cast<uint64_t>(specs[i].deadline.ms) > half)
761 return false;
762 return true;
763}
764
769template <class Spec, size_t N, class Row, size_t M>
770constexpr bool timeouts_consumable(const std::array<Spec, N>& specs,
771 const std::array<Row, M>& rows) {
772 for (size_t i = 0; i < N; ++i) {
773 if (specs[i].deadline.k != timeout_kind::after_ &&
774 specs[i].deadline.k != timeout_kind::after_dyn_)
775 continue;
776 const auto s = specs[i].state.s;
777 const auto ev = specs[i].deadline.event;
778 bool ok = false;
779 for (size_t r = 0; r < M && !ok; ++r) {
780 if (rows[r].on.k != on_kind::event || rows[r].on.e != ev) continue;
781 if (rows[r].from.k == from_kind::any) ok = true;
782 else if (rows[r].from.k == from_kind::state &&
783 (rows[r].from.s == s || is_ancestor(specs, rows[r].from.s, s)))
784 ok = true;
785 }
786 if (!ok) return false;
787 }
788 return true;
789}
790
796template <class Row, class State, class Event>
797constexpr bool row_matches_state(const Row& r, State lvl, Event e) {
798 return r.from.k == from_kind::state && r.from.s == lvl &&
799 r.on.k == on_kind::event && r.on.e == e;
800}
801
803template <class Row, class Event>
804constexpr bool row_matches_any(const Row& r, Event e) {
805 return r.from.k == from_kind::any && r.on.k == on_kind::event && r.on.e == e;
806}
807
809template <class Spec, size_t N, class Row, size_t M, class State, class Event>
810constexpr bool matches(const std::array<Row, M>& rows, const std::array<Spec, N>& specs,
811 State leaf, Event e) {
812 State lvl = leaf;
813 for (size_t fuse = 0; fuse <= N; ++fuse) {
814 for (size_t r = 0; r < M; ++r)
815 if (row_matches_state(rows[r], lvl, e)) return true;
816 const size_t i = spec_index(specs, lvl);
817 if (i == N || specs[i].parent.k != parent_kind::child) break;
818 lvl = specs[i].parent.s;
819 }
820 for (size_t r = 0; r < M; ++r)
821 if (row_matches_any(rows[r], e)) return true;
822 return false;
823}
824
829template <size_t EventCount, class Spec, size_t N, class Row, size_t M>
830constexpr bool exhaustive(const std::array<Spec, N>& specs, const std::array<Row, M>& rows) {
831 for (size_t i = 0; i < N; ++i) {
832 if (specs[i].state.k != id_kind::state) continue;
833 const auto s = specs[i].state.s;
834 if (is_composite(specs, s)) continue; // machines rest only at leaves
835 for (size_t e = 0; e < EventCount; ++e)
836 if (!matches(rows, specs, s, static_cast<decltype(specs[i].deadline.event)>(e)))
837 return false;
838 }
839 return true;
840}
841
842#if FSM_ENABLE_INTROSPECTION
847struct line_buf {
848 char b[224] = {};
849 size_t n = 0;
850 constexpr void put(char c) {
851 if (n + 1 < sizeof b) b[n++] = c;
852 b[n] = '\0';
853 }
854 constexpr void str(const char* s) {
855 while (*s != '\0') put(*s++);
856 }
857 constexpr void u64(uint64_t v) {
858 char tmp[20];
859 size_t i = 0;
860 do {
861 tmp[i++] = static_cast<char>('0' + v % 10);
862 v /= 10;
863 } while (v != 0);
864 while (i > 0) put(tmp[--i]);
865 }
866 constexpr void reset() {
867 n = 0;
868 b[0] = '\0';
869 }
870};
871#endif // FSM_ENABLE_INTROSPECTION
872
873// Optional-trait detection (evaluated on the *complete* Traits type at
874// machine instantiation, never inside the user's struct definition).
875template <class T>
876concept has_time_trait = requires { typename T::Time; };
877template <class T>
878concept has_strict_trait = requires {
879 { T::strict } -> std::convertible_to<bool>;
880};
881template <class T, class State>
882concept has_state_names = requires(State s) {
883 { T::state_name(s) } -> std::convertible_to<const char*>;
884};
885template <class T, class Event>
886concept has_event_names = requires(Event e) {
887 { T::event_name(e) } -> std::convertible_to<const char*>;
888};
889template <class T, class Reason>
890concept has_reason_names = requires(Reason r) {
891 { T::reason_name(r) } -> std::convertible_to<const char*>;
892};
893template <class T, class Event, class Payload>
894concept has_payload_matches = requires(Event e, const Payload& p) {
895 { T::payload_matches(e, p) } -> std::convertible_to<bool>;
896};
897template <class T, class State, class Context>
898concept has_deadline_ms = requires(State s, const Context& c) {
899 { T::deadline_ms(s, c) } -> std::convertible_to<uint32_t>;
900};
901template <class T>
902concept has_post_depth = requires {
903 { T::post_queue_depth } -> std::convertible_to<size_t>;
904};
905
906template <class T>
907constexpr size_t post_depth_of() {
908 if constexpr (has_post_depth<T>) return T::post_queue_depth;
909 else return 4;
910}
911
912template <class T>
913struct time_of {
914 using type = uint32_t;
915};
916template <has_time_trait T>
917struct time_of<T> {
918 using type = typename T::Time;
919};
920
921template <class T>
922constexpr bool strict_of() {
923 if constexpr (has_strict_trait<T>) return T::strict;
924 else return true;
925}
926
927} // namespace detail
928
971template <class Traits>
972class machine {
973public:
974 using state_type = typename Traits::State;
975 using event_type = typename Traits::Event;
976 using context_type = typename Traits::Context;
977 using rows_array = std::remove_cvref_t<decltype(Traits::rows)>;
978 using specs_array = std::remove_cvref_t<decltype(Traits::states)>;
979 using row_type = typename rows_array::value_type;
980 using spec_type = typename specs_array::value_type;
981 using payload_type = typename row_type::payload_type;
982 using reason_type = typename row_type::reason_type;
983 using time_type = typename detail::time_of<Traits>::type;
986
987 static constexpr size_t state_count = static_cast<size_t>(state_type::Count);
988 static constexpr size_t event_count = static_cast<size_t>(event_type::Count);
989 static constexpr size_t row_count = Traits::rows.size();
990 static constexpr bool strict = detail::strict_of<Traits>();
991
992private:
993 // ---- compile-time validation (messages pinned by the compile-fail suite)
994 static_assert(state_count <= 256, "fsm: at most 256 states (spec index is uint8_t)");
995 static_assert(row_count < static_cast<size_t>(no_row),
996 "fsm: row count must stay below fsm::no_row (0xFFFF)");
997 // Ordered so each table defect trips its own message first: clang stops
998 // at the first failing assert, and a duplicated spec necessarily also
999 // fails states_complete (some enum value must then be missing).
1000 static_assert(detail::no_duplicate_specs(Traits::states), "fsm: duplicate state_spec");
1001 static_assert(detail::states_complete<state_count>(Traits::states),
1002 "fsm: every state needs exactly one state_spec");
1003 static_assert(detail::parents_declared(Traits::states),
1004 "fsm: state_spec .parent is mandatory (fsm::root/fsm::child_of)");
1005 static_assert(detail::parents_valid(Traits::states), "fsm: parent graph must be acyclic");
1006 static_assert(detail::composites_declare_initial(Traits::states),
1007 "fsm: composite state must declare start_at initial");
1008 static_assert(detail::leaves_have_no_initial(Traits::states),
1009 "fsm: leaf state must not declare initial");
1010 static_assert(detail::initials_belong(Traits::states),
1011 "fsm: initial child's parent must be its composite");
1012 static_assert(detail::rows_have_targets(Traits::rows),
1013 "fsm: row .to is mandatory (fsm::to/internal/refuse)");
1014 static_assert(detail::rows_reference_valid<state_count, event_count>(Traits::rows,
1015 Traits::states),
1016 "fsm: row references invalid state or event");
1017 static_assert(detail::rows_no_duplicate_unguarded(Traits::rows),
1018 "fsm: ambiguous rows: duplicate unguarded (from, event)");
1019 static_assert(detail::rows_no_unreachable(Traits::rows),
1020 "fsm: unreachable row after unguarded row for same (from, event)");
1021 static_assert(detail::rows_refuse_have_no_action(Traits::rows),
1022 "fsm: refuse row must not declare an action (never runs on refusal)");
1023 static_assert(detail::deadlines_declared(Traits::states),
1024 "fsm: state_spec .deadline is mandatory (fsm::after/no_timeout)");
1025 static_assert(detail::deadlines_fit<time_type>(Traits::states),
1026 "fsm: deadline duration exceeds half the Time range");
1027 static_assert(detail::timeouts_consumable(Traits::states, Traits::rows),
1028 "fsm: timeout event not consumable from its state");
1029 static_assert(!detail::any_dynamic_deadline(Traits::states) ||
1030 detail::has_deadline_ms<Traits, state_type, context_type>,
1031 "fsm: after_dyn states require Traits::deadline_ms(State, const Context&)");
1032 static_assert(!strict || detail::exhaustive<event_count>(Traits::states, Traits::rows),
1033 "fsm: unhandled (state, event) pair in strict machine");
1034 static_assert(detail::spec_index(Traits::states, Traits::initial) < state_count,
1035 "fsm: machine initial state has no state_spec");
1036 static_assert(std::is_unsigned_v<time_type>, "fsm: Time must be an unsigned integer type");
1037 static_assert(std::is_void_v<payload_type> ||
1038 (std::is_default_constructible_v<payload_type> &&
1039 std::is_trivially_copyable_v<payload_type>),
1040 "fsm: Payload must be default-constructible and trivially copyable");
1041
1043 using payload_arg = std::conditional_t<std::is_void_v<payload_type>, detail::empty,
1044 std::remove_cv_t<payload_type>>;
1045
1046 static constexpr size_t max_depth_v = detail::max_depth(Traits::states);
1047
1049 static constexpr std::array<uint8_t, state_count> spec_pos_ = [] {
1050 std::array<uint8_t, state_count> a{};
1051 for (size_t v = 0; v < state_count; ++v)
1052 a[v] = static_cast<uint8_t>(
1053 detail::spec_index(Traits::states, static_cast<state_type>(v)));
1054 return a;
1055 }();
1056
1057 static constexpr const spec_type& spec_of(state_type s) {
1058 return Traits::states[spec_pos_[static_cast<size_t>(s)]];
1059 }
1060
1062 static constexpr std::array<uint8_t, state_count> depth_ = [] {
1063 std::array<uint8_t, state_count> a{};
1064 for (size_t v = 0; v < state_count; ++v)
1065 a[v] = static_cast<uint8_t>(
1066 detail::depth_of(Traits::states, static_cast<state_type>(v)));
1067 return a;
1068 }();
1069
1070 struct slot {
1071 time_type at;
1072 bool armed;
1073 };
1074
1077 static constexpr size_t post_depth = detail::post_depth_of<Traits>();
1078 static_assert(post_depth >= 1 && post_depth <= 255,
1079 "fsm: post_queue_depth must be in [1, 255]");
1080
1081 struct pending {
1082 event_type e;
1083 payload_arg p;
1084 };
1085
1086 context_type* ctx_;
1087 state_type leaf_{};
1088 bool started_ = false;
1089 bool in_dispatch_ = false;
1090#if FSM_ENABLE_OBSERVER
1091 bool in_notify_ = false;
1092#endif
1093 slot slots_[state_count] = {};
1094 pending posts_[post_depth] = {};
1095 uint8_t post_head_ = 0;
1096 uint8_t post_count_ = 0;
1097#if FSM_ENABLE_OBSERVER
1098 const observer_type* obs_ = nullptr;
1099#endif
1100
1101public:
1106 explicit constexpr machine(context_type& ctx) : ctx_(&ctx) {}
1107
1111 machine(const machine&) = delete;
1112 machine& operator=(const machine&) = delete;
1113
1121 void start(time_type now) {
1122 if (started_ || in_dispatch_) return;
1123 started_ = true;
1124 in_dispatch_ = true;
1125 enter_chain_to(Traits::initial, now);
1126 descend_initials(now);
1127 in_dispatch_ = false;
1128 drain_posts(now); // entry hooks may have post()ed follow-up events
1129 }
1130
1152 bool post(event_type e) { return post_impl(e, payload_arg{}); }
1153
1163 size_t posts_pending() const { return post_count_; }
1164
1167 template <event_type E, class Arg>
1168 requires(!std::is_void_v<payload_type> && requires(const Arg& a) {
1169 { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>;
1170 })
1171 bool post(const Arg& arg) {
1172 return post_impl(E, Traits::make_payload(event_tag<E>{}, arg));
1173 }
1174
1176 template <event_type E>
1177 requires(!std::is_void_v<payload_type> && requires {
1178 { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>;
1179 })
1180 bool post() {
1181 return post_impl(E, Traits::make_payload(event_tag<E>{}));
1182 }
1183
1185 template <class P = payload_type>
1186 requires(!std::is_void_v<P> && std::is_same_v<P, payload_type>)
1187 bool post(event_type e, const P& p) {
1188 return post_impl(e, p);
1189 }
1190
1201 const result_type r = do_dispatch(e, payload_arg{}, now);
1202 if (!in_dispatch_) drain_posts(now);
1203 return r;
1204 }
1205
1215 template <class P = payload_type>
1216 requires(!std::is_void_v<P> && std::is_same_v<P, payload_type>)
1218 const result_type r = do_dispatch(e, p, now);
1219 if (!in_dispatch_) drain_posts(now);
1220 return r;
1221 }
1222
1237 template <event_type E, class Arg>
1238 requires(!std::is_void_v<payload_type> && requires(const Arg& a) {
1239 { Traits::make_payload(event_tag<E>{}, a) } -> std::convertible_to<payload_arg>;
1240 })
1241 result_type dispatch(const Arg& arg, time_type now) {
1242 return dispatch(E, Traits::make_payload(event_tag<E>{}, arg), now);
1243 }
1244
1246 template <event_type E>
1247 requires(!std::is_void_v<payload_type> && requires {
1248 { Traits::make_payload(event_tag<E>{}) } -> std::convertible_to<payload_arg>;
1249 })
1251 return dispatch(E, Traits::make_payload(event_tag<E>{}), now);
1252 }
1253
1264 bool next_deadline(time_type& out) const {
1265 bool found = false;
1266 for (size_t i = 0; i < state_count; ++i) {
1267 if (!slots_[i].armed) continue;
1268 if (!found || detail::time_lt<time_type>(slots_[i].at, out)) {
1269 out = slots_[i].at;
1270 found = true;
1271 }
1272 }
1273 return found;
1274 }
1275
1294 unsigned service(time_type now) {
1295 if (in_dispatch_) return 0;
1296 unsigned fired = 0;
1297 for (size_t fuse = 0; fuse < 4 * state_count; ++fuse) {
1298 size_t best = state_count;
1299 for (size_t i = 0; i < state_count; ++i) {
1300 if (!slots_[i].armed || !detail::time_le<time_type>(slots_[i].at, now)) continue;
1301 if (best == state_count ||
1302 detail::time_lt<time_type>(slots_[i].at, slots_[best].at))
1303 best = i;
1304 }
1305 if (best == state_count) break;
1306 slots_[best].armed = false;
1307 const state_type owner = static_cast<state_type>(best);
1308 const event_type ev = spec_of(owner).deadline.event;
1309#if FSM_ENABLE_OBSERVER
1310 if (obs_ != nullptr && obs_->on_timeout_fired != nullptr)
1311 obs_->on_timeout_fired(obs_->user, owner, ev);
1312#endif
1313 do_dispatch(ev, payload_arg{}, now);
1314 drain_posts(now); // a fired timeout's actions may have post()ed
1315 ++fired;
1316 }
1317 return fired;
1318 }
1319
1321 state_type state() const { return leaf_; }
1322
1324 bool in(state_type s) const {
1325 if (!started_) return false;
1326 state_type cur = leaf_;
1327 for (size_t fuse = 0; fuse <= state_count; ++fuse) {
1328 if (cur == s) return true;
1329 const auto& p = spec_of(cur).parent;
1330 if (p.k != parent_kind::child) return false;
1331 cur = p.s;
1332 }
1333 return false;
1334 }
1335
1337 bool started() const { return started_; }
1338
1339#if FSM_ENABLE_OBSERVER
1345 void set_observer(const observer_type* o) { obs_ = o; }
1346#endif
1347
1348#if FSM_ENABLE_INTROSPECTION
1358 template <class W>
1359 void dump(W&& write) const {
1360 detail::line_buf lb;
1361 lb.str("fsm: ");
1362 lb.u64(state_count);
1363 lb.str(" states, ");
1364 lb.u64(row_count);
1365 lb.str(" rows, leaf=");
1366 put_state_name(lb, leaf_);
1367 lb.str(", started=");
1368 lb.u64(started_ ? 1 : 0);
1369 write(static_cast<const char*>(lb.b));
1370 for (size_t i = 0; i < row_count; ++i) {
1371 const auto& r = Traits::rows[i];
1372 lb.reset();
1373 lb.str("row ");
1374 lb.u64(i);
1375 lb.str(": ");
1376 if (r.from.k == from_kind::any) lb.str("any");
1377 else put_state_name(lb, r.from.s);
1378 lb.str(" --");
1379 put_event_name(lb, r.on.e);
1380 if (r.when != nullptr) lb.str("[guard]");
1381 if (r.then != nullptr) lb.str("/action");
1382 lb.str("--> ");
1383 switch (r.to.k) {
1384 case target_kind::to: put_state_name(lb, r.to.s); break;
1385 case target_kind::internal_: lb.str("internal"); break;
1387 lb.str("refuse(");
1388 put_reason_name(lb, r.to.r);
1389 lb.str(")");
1390 break;
1391 default: lb.str("?"); break;
1392 }
1393 if (r.label != nullptr) {
1394 lb.str(" # ");
1395 lb.str(r.label);
1396 }
1397 write(static_cast<const char*>(lb.b));
1398 }
1399 for (size_t i = 0; i < state_count; ++i) {
1400 if (!slots_[i].armed) continue;
1401 lb.reset();
1402 lb.str("armed: ");
1403 put_state_name(lb, static_cast<state_type>(i));
1404 lb.str(" at ");
1405 lb.u64(static_cast<uint64_t>(slots_[i].at));
1406 write(static_cast<const char*>(lb.b));
1407 }
1408 }
1409
1411 template <class F>
1412 static constexpr void for_each_row(F&& f) {
1413 for (size_t i = 0; i < row_count; ++i) f(Traits::rows[i], i);
1414 }
1415
1417 template <class F>
1418 static constexpr void for_each_state(F&& f) {
1419 for (size_t i = 0; i < state_count; ++i) f(Traits::states[i], i);
1420 }
1421
1423 static constexpr void put_reason_name(detail::line_buf& lb, reason_type r) {
1424 if constexpr (detail::has_reason_names<Traits, reason_type>) {
1425 lb.str(Traits::reason_name(r));
1426 } else {
1427 lb.u64(static_cast<uint64_t>(r));
1428 }
1429 }
1430
1432 static constexpr void put_state_name(detail::line_buf& lb, state_type s) {
1433 if constexpr (detail::has_state_names<Traits, state_type>) {
1434 lb.str(Traits::state_name(s));
1435 } else {
1436 lb.put('s');
1437 lb.u64(static_cast<uint64_t>(s));
1438 }
1439 }
1440
1442 static constexpr void put_event_name(detail::line_buf& lb, event_type e) {
1443 if constexpr (detail::has_event_names<Traits, event_type>) {
1444 lb.str(Traits::event_name(e));
1445 } else {
1446 lb.put('e');
1447 lb.u64(static_cast<uint64_t>(e));
1448 }
1449 }
1450#endif // FSM_ENABLE_INTROSPECTION
1451
1452private:
1453 result_type make(fsm::status st, state_type from, state_type to, event_type e, uint16_t row_i,
1454 reason_type reason) const {
1455 return result_type{st, from, to, e, row_i, reason};
1456 }
1457
1464 result_type notify(const result_type& r) {
1465#if FSM_ENABLE_OBSERVER
1466 if (in_notify_) return r;
1467 in_notify_ = true;
1468 if (obs_ != nullptr && obs_->on_event != nullptr) obs_->on_event(obs_->user, r);
1469 if (obs_ != nullptr && obs_->on_transition != nullptr &&
1470 r.status == fsm::status::transitioned)
1471 obs_->on_transition(obs_->user, r.from, r.to, r.event);
1472 in_notify_ = false;
1473#endif
1474 return r;
1475 }
1476
1477 bool post_impl(event_type e, const payload_arg& p) {
1478 if (!started_ || post_count_ >= post_depth) return false;
1479 posts_[(post_head_ + post_count_) % post_depth] = pending{e, p};
1480 ++post_count_;
1481 return true;
1482 }
1483
1487 void drain_posts(time_type now) {
1488 for (size_t fuse = 0; fuse < 4 * post_depth && post_count_ > 0; ++fuse) {
1489 const pending pn = posts_[post_head_];
1490 post_head_ = static_cast<uint8_t>((post_head_ + 1) % post_depth);
1491 --post_count_;
1492 do_dispatch(pn.e, pn.p, now);
1493 }
1494 }
1495
1496 bool call_guard(typename row_type::guard_fn g, const payload_arg& p) {
1497 if constexpr (std::is_void_v<payload_type>) {
1498 (void)p;
1499 return g(*ctx_);
1500 } else {
1501 return g(*ctx_, p);
1502 }
1503 }
1504
1505 void call_action(typename row_type::action_fn a, const payload_arg& p) {
1506 if (a == nullptr) return;
1507 if constexpr (std::is_void_v<payload_type>) {
1508 (void)p;
1509 a(*ctx_);
1510 } else {
1511 a(*ctx_, p);
1512 }
1513 }
1514
1515 void do_enter(state_type s, time_type now) {
1516 const auto& sp = spec_of(s);
1517 if (sp.entry != nullptr) sp.entry(*ctx_);
1518 if (sp.deadline.k == timeout_kind::after_) {
1519 slots_[static_cast<size_t>(s)] = {static_cast<time_type>(now + sp.deadline.ms), true};
1520 } else if constexpr (detail::has_deadline_ms<Traits, state_type, context_type>) {
1521 if (sp.deadline.k == timeout_kind::after_dyn_) {
1522 // Runtime-valued deadline, re-consulted on every entry
1523 // (self-transition re-entry included). 0 means "due now".
1524 constexpr uint64_t half = static_cast<uint64_t>(static_cast<time_type>(-1)) / 2;
1525 uint64_t ms = Traits::deadline_ms(s, *ctx_);
1526#if FSM_ENABLE_DEADLINE_CHECKS
1527 FSM_DEADLINE_ASSERT(ms <= half);
1528#endif
1529 if (ms > half) ms = half; // stay long rather than fire instantly
1530 slots_[static_cast<size_t>(s)] = {
1531 static_cast<time_type>(now + static_cast<time_type>(ms)), true};
1532 }
1533 }
1534 }
1535
1536 void do_exit(state_type s) {
1537 slots_[static_cast<size_t>(s)].armed = false;
1538 const auto& sp = spec_of(s);
1539 if (sp.exit != nullptr) sp.exit(*ctx_);
1540 }
1541
1545 void enter_path(state_type target, state_type above, bool have_above, time_type now) {
1546 state_type path[max_depth_v + 1];
1547 size_t n = 0;
1548 state_type cur = target;
1549 while (true) {
1550 if (have_above && cur == above) break;
1551 path[n++] = cur;
1552 const auto& p = spec_of(cur).parent;
1553 if (p.k != parent_kind::child) break;
1554 cur = p.s;
1555 }
1556 while (n > 0) do_enter(path[--n], now);
1557 leaf_ = target;
1558 }
1559
1561 void enter_chain_to(state_type target, time_type now) {
1562 enter_path(target, target, false, now);
1563 }
1564
1566 void descend_initials(time_type now) {
1567 state_type cur = leaf_;
1568 for (size_t fuse = 0; fuse <= state_count; ++fuse) {
1569 const auto& ini = spec_of(cur).initial;
1570 if (ini.k != initial_kind::start_at) break;
1571 cur = ini.s;
1572 do_enter(cur, now);
1573 }
1574 leaf_ = cur;
1575 }
1576
1579 void exit_up(state_type stop, bool inclusive) {
1580 state_type cur = leaf_;
1581 for (size_t fuse = 0; fuse <= state_count; ++fuse) {
1582 if (!inclusive && cur == stop) return;
1583 do_exit(cur);
1584 if (inclusive && cur == stop) return;
1585 const auto& p = spec_of(cur).parent;
1586 if (p.k != parent_kind::child) return;
1587 cur = p.s;
1588 }
1589 }
1590
1592 void exit_all() {
1593 state_type cur = leaf_;
1594 for (size_t fuse = 0; fuse <= state_count; ++fuse) {
1595 do_exit(cur);
1596 const auto& p = spec_of(cur).parent;
1597 if (p.k != parent_kind::child) return;
1598 cur = p.s;
1599 }
1600 }
1601
1603 static state_type lca_of(state_type a, state_type b, bool& found) {
1604 size_t da = depth_[static_cast<size_t>(a)];
1605 size_t db = depth_[static_cast<size_t>(b)];
1606 while (da > db) {
1607 a = spec_of(a).parent.s;
1608 --da;
1609 }
1610 while (db > da) {
1611 b = spec_of(b).parent.s;
1612 --db;
1613 }
1614 for (size_t fuse = 0; fuse <= state_count; ++fuse) {
1615 if (a == b) {
1616 found = true;
1617 return a;
1618 }
1619 const auto& pa = spec_of(a).parent;
1620 const auto& pb = spec_of(b).parent;
1621 if (pa.k != parent_kind::child || pb.k != parent_kind::child) break;
1622 a = pa.s;
1623 b = pb.s;
1624 }
1625 found = false;
1626 return a;
1627 }
1628
1629 result_type execute(const row_type& r, uint16_t idx, state_type matched_lvl, event_type e,
1630 const payload_arg& p, time_type now) {
1631 const state_type before = leaf_;
1632 switch (r.to.k) {
1634 return make(fsm::status::refused_by_row, before, before, e, idx, r.to.r);
1636 call_action(r.then, p);
1637 return make(fsm::status::handled_internally, before, before, e, idx,
1638 reason_type{});
1639 case target_kind::to:
1640 default:
1641 break;
1642 }
1643 const state_type main_src =
1644 (r.from.k == from_kind::any) ? before : matched_lvl;
1645 const state_type tgt = r.to.s;
1646 if (tgt == main_src) {
1647 // Self-transition: external semantics — full exit and re-entry.
1648 exit_up(main_src, true);
1649 call_action(r.then, p);
1650 const auto& ps = spec_of(main_src).parent;
1651 enter_path(tgt, ps.s, ps.k == parent_kind::child, now);
1652 } else {
1653 bool have_lca = false;
1654 const state_type lca = lca_of(main_src, tgt, have_lca);
1655 if (have_lca) {
1656 exit_up(lca, false);
1657 call_action(r.then, p);
1658 enter_path(tgt, lca, true, now);
1659 } else {
1660 exit_all();
1661 call_action(r.then, p);
1662 enter_path(tgt, tgt, false, now);
1663 }
1664 }
1665 descend_initials(now);
1666 return make(fsm::status::transitioned, before, leaf_, e, idx, reason_type{});
1667 }
1668
1669 result_type do_dispatch(event_type e, const payload_arg& p, time_type now) {
1670#if FSM_ENABLE_PAYLOAD_CHECKS
1671 // Erased-path guardrail: with a payload_matches() hook declared, a
1672 // payload whose content disagrees with the event trips the assert in
1673 // debug builds (queues, facades and post() all funnel through here).
1674 if constexpr (!std::is_void_v<payload_type> &&
1675 detail::has_payload_matches<Traits, event_type, payload_arg>) {
1676 FSM_PAYLOAD_ASSERT(Traits::payload_matches(e, p));
1677 }
1678#endif
1679 if (in_dispatch_)
1680 return notify(make(fsm::status::refused_reentrant, leaf_, leaf_, e, no_row,
1681 reason_type{}));
1682 if (!started_)
1683 return notify(
1684 make(fsm::status::not_started, leaf_, leaf_, e, no_row, reason_type{}));
1685 in_dispatch_ = true;
1686 uint16_t first_match = no_row;
1687 state_type lvl = leaf_;
1688 for (size_t fuse = 0; fuse <= state_count; ++fuse) {
1689 for (uint16_t i = 0; i < row_count; ++i) {
1690 const auto& r = Traits::rows[i];
1691 if (!detail::row_matches_state(r, lvl, e)) continue;
1692 if (first_match == no_row) first_match = i;
1693 if (r.when != nullptr && !call_guard(r.when, p)) continue;
1694 const result_type res = notify(execute(r, i, lvl, e, p, now));
1695 in_dispatch_ = false;
1696 return res;
1697 }
1698 const auto& par = spec_of(lvl).parent;
1699 if (par.k != parent_kind::child) break;
1700 lvl = par.s;
1701 }
1702 for (uint16_t i = 0; i < row_count; ++i) {
1703 const auto& r = Traits::rows[i];
1704 if (!detail::row_matches_any(r, e)) continue;
1705 if (first_match == no_row) first_match = i;
1706 if (r.when != nullptr && !call_guard(r.when, p)) continue;
1707 const result_type res = notify(execute(r, i, leaf_, e, p, now));
1708 in_dispatch_ = false;
1709 return res;
1710 }
1711 const result_type res = notify(make(first_match == no_row
1714 leaf_, leaf_, e, first_match, reason_type{}));
1715 in_dispatch_ = false;
1716 return res;
1717 }
1718};
1719
1720} // namespace fsm
1721
1722#endif // FSM_FSM_HPP
The state machine engine interpreting a Traits' constexpr tables.
Definition fsm.hpp:972
std::remove_cvref_t< decltype(Traits::rows)> rows_array
Definition fsm.hpp:977
observer< state_type, event_type, reason_type > observer_type
Definition fsm.hpp:985
result_type dispatch(event_type e, time_type now)
Deliver an event and return the verdict.
Definition fsm.hpp:1200
bool next_deadline(time_type &out) const
Earliest armed deadline across the machine's own timeout slots.
Definition fsm.hpp:1264
state_type state() const
Current leaf state.
Definition fsm.hpp:1321
typename row_type::reason_type reason_type
Definition fsm.hpp:982
typename row_type::payload_type payload_type
void if none declared
Definition fsm.hpp:981
bool in(state_type s) const
True if s is the current leaf or one of its active ancestors.
Definition fsm.hpp:1324
typename Traits::Event event_type
Definition fsm.hpp:975
constexpr machine(context_type &ctx)
Bind the machine to a caller-owned context. Does not enter any state — call start() with the current ...
Definition fsm.hpp:1106
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 — wron...
Definition fsm.hpp:1241
std::remove_cvref_t< decltype(Traits::states)> specs_array
Definition fsm.hpp:978
bool post(event_type e, const P &p)
post() carrying input data (machines with a Payload).
Definition fsm.hpp:1187
unsigned service(time_type now)
Fire every expired deadline, earliest first.
Definition fsm.hpp:1294
static constexpr bool strict
Definition fsm.hpp:990
static constexpr size_t row_count
Definition fsm.hpp:989
bool started() const
True once start() has run.
Definition fsm.hpp:1337
typename rows_array::value_type row_type
Definition fsm.hpp:979
bool post()
Typed post() for events whose factory takes no data.
Definition fsm.hpp:1180
static constexpr size_t state_count
Definition fsm.hpp:987
static constexpr void for_each_state(F &&f)
Visit every state spec as f(spec, index) (constexpr-capable).
Definition fsm.hpp:1418
typename Traits::State state_type
Definition fsm.hpp:974
result< state_type, event_type, reason_type > result_type
Definition fsm.hpp:984
static constexpr void for_each_row(F &&f)
Visit every transition row as f(row, index) (constexpr-capable).
Definition fsm.hpp:1412
typename Traits::Context context_type
Definition fsm.hpp:976
void start(time_type now)
Enter the initial state chain (entry actions run, deadlines arm). Idempotent: a second call is a no-o...
Definition fsm.hpp:1121
bool post(const Arg &arg)
Typed post(): same factory-checked construction as the typed dispatch() wrapper, for run-to-completio...
Definition fsm.hpp:1171
result_type dispatch(event_type e, const P &p, time_type now)
Deliver an event carrying input data (machines with a Payload).
Definition fsm.hpp:1217
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.
Definition fsm.hpp:1423
void set_observer(const observer_type *o)
Attach a debug observer (nullptr detaches). Not owned — the observer must outlive its attachment....
Definition fsm.hpp:1345
static constexpr size_t event_count
Definition fsm.hpp:988
bool post(event_type e)
Queue an event for delivery after the current dispatch completes (run-to-completion),...
Definition fsm.hpp:1152
static constexpr void put_event_name(detail::line_buf &lb, event_type e)
Append the (hooked or numeric) name of e to lb.
Definition fsm.hpp:1442
machine(const machine &)=delete
machine & operator=(const machine &)=delete
typename detail::time_of< Traits >::type time_type
Definition fsm.hpp:983
static constexpr void put_state_name(detail::line_buf &lb, state_type s)
Append the (hooked or numeric) name of s to lb.
Definition fsm.hpp:1432
size_t posts_pending() const
Number of post()ed events still queued (not yet drained).
Definition fsm.hpp:1163
typename specs_array::value_type spec_type
Definition fsm.hpp:980
result_type dispatch(time_type now)
Typed dispatch for events whose factory takes no data.
Definition fsm.hpp:1250
void dump(W &&write) const
Stream a human-readable description of the machine — the live table, current leaf,...
Definition fsm.hpp:1359
#define FSM_DEADLINE_ASSERT(cond)
Definition fsm.hpp:87
#define FSM_PAYLOAD_ASSERT(cond)
Definition fsm.hpp:69
Definition dot.hpp:25
constexpr internal_t internal
Row target: handle the event (run the action) without exit/entry.
Definition fsm.hpp:153
constexpr any_t any
Wildcard row source: matches any state, after all ancestor levels.
Definition fsm.hpp:146
constexpr parent_spec< State > child_of(State s)
Declare a state's parent in the hierarchy.
Definition fsm.hpp:317
target_kind
Kinds for row::to. unspecified is the poison default.
Definition fsm.hpp:208
parent_kind
Kinds for state_spec::parent. unspecified is the poison default.
Definition fsm.hpp:288
id_kind
Kinds for state_spec::state. unspecified is the poison default.
Definition fsm.hpp:276
on_kind
Kinds for row::on. unspecified is the poison default.
Definition fsm.hpp:196
constexpr timeout_spec< Event > after_dyn(Event e)
Declare a dynamic deadline: duration read from the context at entry time instead of the table.
Definition fsm.hpp:398
constexpr bool accepted(const result< State, Event, Reason > &r)
True when the event was acted on (transitioned or handled internally).
Definition fsm.hpp:137
constexpr timeout_spec< Event > after(uint32_t ms, Event e)
Declare "dispatch @p e after @p ms milliseconds in this state".
Definition fsm.hpp:371
constexpr detail::refuse_t< Reason > refuse(Reason r)
Make a declarative refusal target carrying a reason.
Definition fsm.hpp:271
status
Verdict category returned by machine::dispatch().
Definition fsm.hpp:107
@ refused_guard
Row(s) matched (from, event) but every guard declined.
@ refused_by_row
An explicit fsm::refuse row matched; result carries its reason.
@ handled_internally
A row matched with target fsm::internal; no state change.
@ transitioned
A row matched and an external transition completed.
@ refused_reentrant
dispatch() called from inside a guard/action; not allowed.
@ not_started
dispatch() before machine::start().
@ unhandled
No row considered this (state, event) pair at all.
constexpr uint16_t no_row
Sentinel row index meaning "no table row was involved".
Definition fsm.hpp:99
constexpr no_timeout_t no_timeout
State deadline declaration: this state has no timeout (explicitly).
Definition fsm.hpp:160
constexpr root_t root
Parent declaration: this state is a hierarchy root (no parent).
Definition fsm.hpp:295
from_kind
Kinds for row::from. unspecified is the poison default.
Definition fsm.hpp:178
initial_kind
Kinds for state_spec::initial. Default is leaf (no children).
Definition fsm.hpp:322
timeout_kind
Kinds for state_spec::deadline. unspecified is the poison default.
Definition fsm.hpp:343
Tag type for fsm::any.
Definition fsm.hpp:142
constexpr any_t()=default
Tag carrying an event as a compile-time value.
Definition fsm.hpp:173
constexpr event_tag()=default
Row source: a concrete state or the fsm::any wildcard.
Definition fsm.hpp:187
from_kind k
Definition fsm.hpp:188
constexpr from_spec(State st)
Definition fsm.hpp:191
constexpr from_spec(any_t)
Definition fsm.hpp:192
State s
Definition fsm.hpp:189
constexpr from_spec()=default
Initial-child declaration for composite states.
Definition fsm.hpp:326
State s
Definition fsm.hpp:328
initial_kind k
Definition fsm.hpp:327
Tag type for fsm::internal.
Definition fsm.hpp:149
constexpr internal_t()=default
Tag type for fsm::no_timeout.
Definition fsm.hpp:156
constexpr no_timeout_t()=default
Runtime-attachable debug observer: plain nullable function pointers.
Definition fsm.hpp:464
void(* on_event)(void *user, const result< State, Event, Reason > &r)
Definition fsm.hpp:467
void(* on_transition)(void *user, State from, State to, Event e)
Definition fsm.hpp:469
void(* on_timeout_fired)(void *user, State armed, Event e)
Definition fsm.hpp:471
void * user
Opaque pointer passed back to every hook.
Definition fsm.hpp:465
Row trigger event, with poison default for omitted .on.
Definition fsm.hpp:200
on_kind k
Definition fsm.hpp:201
constexpr on_spec()=default
Event e
Definition fsm.hpp:202
constexpr on_spec(Event ev)
Definition fsm.hpp:204
Parent link — a mandatory column: fsm::root or fsm::child_of(P).
Definition fsm.hpp:307
constexpr parent_spec()=default
parent_kind k
Definition fsm.hpp:308
State s
Definition fsm.hpp:309
constexpr parent_spec(parent_kind kk, State st)
Definition fsm.hpp:312
constexpr parent_spec(root_t)
Definition fsm.hpp:311
Dispatch verdict: what happened, where, and why.
Definition fsm.hpp:126
Reason reason
Refusal reason for refused_by_row; Reason{} otherwise.
Definition fsm.hpp:132
State from
Leaf state before dispatch.
Definition fsm.hpp:128
uint16_t row
Deciding (or first-matching) row index; fsm::no_row if none.
Definition fsm.hpp:131
State to
Leaf state after dispatch (== from unless transitioned).
Definition fsm.hpp:129
fsm::status status
Verdict category.
Definition fsm.hpp:127
Event event
The dispatched event.
Definition fsm.hpp:130
Tag type for fsm::root.
Definition fsm.hpp:291
constexpr root_t()=default
One transition-table row: {from, on, when, then, to}.
Definition fsm.hpp:416
const char * label
Definition fsm.hpp:432
Payload payload_type
Definition fsm.hpp:417
guard_fn when
Optional guard.
Definition fsm.hpp:426
Reason reason_type
Definition fsm.hpp:418
typename detail::fn_sigs< Context, Payload >::guard guard_fn
Definition fsm.hpp:420
target< State, Reason > to
Transition/internal/refuse (mandatory).
Definition fsm.hpp:428
action_fn then
Optional action.
Definition fsm.hpp:427
on_spec< Event > on
Trigger event (mandatory).
Definition fsm.hpp:425
from_spec< State > from
Source state or fsm::any (mandatory).
Definition fsm.hpp:424
typename detail::fn_sigs< Context, Payload >::action action_fn
Definition fsm.hpp:422
State identifier for a state_spec, with poison default.
Definition fsm.hpp:280
constexpr state_id(State st)
Definition fsm.hpp:284
State s
Definition fsm.hpp:282
id_kind k
Definition fsm.hpp:281
constexpr state_id()=default
One state's specification: hierarchy links, actions, deadline.
Definition fsm.hpp:441
hook_fn entry
Run on entry (before the deadline is armed).
Definition fsm.hpp:448
timeout_spec< Event > deadline
Mandatory: fsm::after(...) or fsm::no_timeout.
Definition fsm.hpp:450
initial_spec< State > initial
fsm::start_at(C) on composites; default leaf.
Definition fsm.hpp:447
parent_spec< State > parent
Mandatory: fsm::root or fsm::child_of(P).
Definition fsm.hpp:446
void(*)(Context &) hook_fn
Definition fsm.hpp:443
hook_fn exit
Run on exit (deadline already disarmed).
Definition fsm.hpp:449
state_id< State > state
Which state this spec describes (mandatory).
Definition fsm.hpp:445
Row target: external transition, internal handling, or refusal.
Definition fsm.hpp:243
target_kind k
Definition fsm.hpp:244
State s
Destination for target_kind::to.
Definition fsm.hpp:245
constexpr target(detail::refuse_t< Reason > f)
Definition fsm.hpp:250
constexpr target()=default
constexpr target(internal_t)
Definition fsm.hpp:249
Reason r
Reason for target_kind::refuse_.
Definition fsm.hpp:246
constexpr target(detail::to_t< State > t)
Definition fsm.hpp:248
Per-state deadline declaration — a mandatory column.
Definition fsm.hpp:354
timeout_kind k
Definition fsm.hpp:355
Event event
Event dispatched when the deadline expires.
Definition fsm.hpp:357
uint32_t ms
Duration in milliseconds after state entry.
Definition fsm.hpp:356
constexpr timeout_spec()=default
constexpr timeout_spec(no_timeout_t)
Definition fsm.hpp:359
constexpr timeout_spec(timeout_kind kk, uint32_t m, Event e)
Definition fsm.hpp:360