diff --git a/templatefsm/.gitignore b/templatefsm/.gitignore new file mode 100644 index 0000000..4a0b530 --- /dev/null +++ b/templatefsm/.gitignore @@ -0,0 +1,74 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/templatefsm/fsm.h b/templatefsm/fsm.h new file mode 100644 index 0000000..d674496 --- /dev/null +++ b/templatefsm/fsm.h @@ -0,0 +1,295 @@ +#pragma once + +#include +#include +#include + +/** @brief Missing transition policy. */ +enum class missing_transition_policy { + /** @brief Missing transition is considered required by validation. */ + strict, + + /** @brief Missing transition is allowed (event ignored). */ + ignore +}; + +/** @brief Marker for undefined transition. */ +struct no_transition {}; + +/** @brief Default guard: always allow. */ +struct always_allow { + template + bool operator() (TContext &, const TFrom &, const TEvent &) const noexcept { + return true; + } +}; + +/** @brief Default action: do nothing. */ +struct do_nothing { + template + void operator() (TContext &, const TFrom &, const TTo &, const TEvent &) const noexcept { + } +}; + +/** + * @brief Transition descriptor. + * + * @tparam TTo Destination state type. + * @tparam TGuard Guard functor type. + * @tparam TAction Action functor type. + */ +template +struct transition_to { + using to_state = TTo; + using guard = TGuard; + using action = TAction; +}; + +/** + * @brief Transition table mapping: specialize to define transitions. + * Default: no_transition. + */ +template +struct transition { + using type = no_transition; +}; + +/** @brief Optional mapping to names for logs. */ +template +struct name_of { + static constexpr const char *value = "?"; +}; + +/** + * @brief Logger interface (default no-op). + * + * You can implement: + * - ignored_transition(from, event) + * - unhandled_transition(from, event) // policy=strict but missing transition at runtime + * - guard_blocked(from, event) + */ +struct null_logger { + void ignored_transition (const char *, const char *) noexcept {} + void unhandled_transition (const char *, const char *) noexcept {} + void guard_blocked (const char *, const char *) noexcept {} +}; + +/** @brief Hooks: state-only. */ +template +struct on_exit_state { + template + static void call (TContext &, const TState &) noexcept {} +}; + +template +struct on_entry_state { + template + static void call (TContext &, const TState &) noexcept {} +}; + +/** @brief Hooks: event-specific. */ +template +struct on_exit_event { + template + static void call (TContext &, const TState &, const TEvent &) noexcept {} +}; + +template +struct on_entry_event { + template + static void call (TContext &, const TState &, const TEvent &) noexcept {} +}; + +/** + * @brief Factory: construct destination state. + * + * IMPORTANT: Now includes TContext in template parameters to avoid templated make(). + * + * Default: To{}. + * Specialize state_factory as needed. + */ +template +struct state_factory { + static TTo make (TContext &, const TFrom &, const TEvent &) { + return TTo{}; + } +}; + +/** + * @brief Per (State,Event) missing transition policy. + * + * Default is ignore. Mark only required pairs strict. + */ +template +struct missing_transition { + static constexpr missing_transition_policy policy = missing_transition_policy::ignore; +}; + +/** @brief Helper: is T one of Ts... */ +template +struct is_one_of : std::disjunction...> {}; + +/** @brief Helper: transition exists? */ +template +struct has_transition + : std::bool_constant < !std::is_same_v::type, no_transition >> {}; + +/** @brief Factory validation (call-form). */ +template +struct is_factory_invocable : std::false_type {}; + +template +struct is_factory_invocable < + TContext, TFrom, TEvent, TTo, +std::void_t::make ( + std::declval(), + std::declval(), + std::declval())) >> + : std::bool_constant::make ( + std::declval(), + std::declval(), + std::declval())), + TTo>> {}; + +/** + * @brief FSM with runtime state storage and explicit compile-time validation. + * + * dispatch(): + * - never static_asserts on missing transitions (avoids StateƗEvent explosion with std::visit) + * - handles missing transitions via missing_transition::policy (ignore/strict) + * + * validate_events(): + * - compile-time checks ONLY for pairs marked strict. + */ +template +class fsm { + public: + using state_variant = std::variant; + + template ::value>> + explicit fsm (TContext &ctx, TInitial initial, TLogger logger = TLogger{}) + : m_ctx (ctx) + , m_logger (std::move (logger)) + , m_state (std::move (initial)) { + std::visit (init_entry_visitor{*this}, m_state); + } + + template + bool dispatch (const TEvent &ev) { + dispatch_visitor v{*this, ev}; + return std::visit (v, m_state); + } + + template + static constexpr void validate_events() { + (validate_one_event(), ...); + } + + const state_variant &state() const noexcept { + return m_state; + } + state_variant &state() noexcept { + return m_state; + } + + private: + template + struct dispatch_visitor { + fsm &self; + const TEvent &ev; + + template + bool operator() (TFrom &from) const { + return self.template dispatch_from (from, ev); + } + }; + + struct init_entry_visitor { + fsm &self; + template + void operator() (TState &st) const { + on_entry_state::call (self.m_ctx, st); + } + }; + + template + struct entry_after_commit_visitor { + fsm &self; + const TEvent &ev; + + template + void operator() (TState &st) const { + on_entry_state::call (self.m_ctx, st); + on_entry_event::call (self.m_ctx, st, ev); + } + }; + + template + bool dispatch_from (TFrom &from, const TEvent &ev) { + using tr = typename transition::type; + + if constexpr (std::is_same_v) { + if constexpr (missing_transition::policy == missing_transition_policy::strict) + m_logger.unhandled_transition (name_of::value, name_of::value); + else + m_logger.ignored_transition (name_of::value, name_of::value); + return false; + } else { + using to_state = typename tr::to_state; + using guard_t = typename tr::guard; + using action_t = typename tr::action; + + static_assert (is_one_of::value, + "FSM: transition target state not in FSM state list."); + + static_assert (std::is_invocable_r_v, + "FSM: guard must be callable as bool(Context&, const From&, const Event&)."); + + static_assert (std::is_invocable_r_v, + "FSM: action must be callable as void(Context&, const From&, const To&, const Event&)."); + + static_assert (is_factory_invocable::value, + "FSM: state_factory::make must be callable as " + "To make(Context&, const From&, const Event&)."); + + guard_t guard{}; + if (!guard (m_ctx, from, ev)) { + m_logger.guard_blocked (name_of::value, name_of::value); + return false; + } + + on_exit_event::call (m_ctx, from, ev); + on_exit_state::call (m_ctx, from); + + to_state to = state_factory::make (m_ctx, from, ev); + + action_t action{}; + action (m_ctx, from, to, ev); + + m_state = std::move (to); + + std::visit (entry_after_commit_visitor {*this, ev}, m_state); + return true; + } + } + + template + static constexpr void validate_one_event() { + (validate_pair(), ...); + } + + template + static constexpr void validate_pair() { + if constexpr (missing_transition::policy == missing_transition_policy::strict) { + static_assert (has_transition::value, + "FSM validation: required (strict) transition is missing for (State, Event)."); + } + } + + private: + TContext &m_ctx; + TLogger m_logger; + state_variant m_state; +}; diff --git a/templatefsm/main.cpp b/templatefsm/main.cpp new file mode 100644 index 0000000..aec8e9b --- /dev/null +++ b/templatefsm/main.cpp @@ -0,0 +1,148 @@ +#include "fsm.h" +#include + +/** States */ +struct Idle { }; +struct Active { + std::uint32_t session_id = 0; +}; + +/** Events */ +struct EvStart { + std::uint32_t requested_session = 0; +}; +struct EvStop { }; + +/** Context */ +struct Context { + std::uint32_t starts = 0; +}; + +/** Names (optional) */ +template <> struct name_of { + static constexpr const char *value = "Idle"; +}; +template <> struct name_of { + static constexpr const char *value = "Active"; +}; +template <> struct name_of { + static constexpr const char *value = "EvStart"; +}; +template <> struct name_of { + static constexpr const char *value = "EvStop"; +}; + +/** Logger */ +struct MyLogger { + void ignored_transition (const char *from_state, const char *event) noexcept { + (void)from_state; + (void)event; + // printf("FSM ignored: %s + %s\n", from_state, event); + } + + void unhandled_transition (const char *from_state, const char *event) noexcept { + (void)from_state; + (void)event; + // printf("FSM UNHANDLED: %s + %s\n", from_state, event); + } + + void guard_blocked (const char *from_state, const char *event) noexcept { + (void)from_state; + (void)event; + // printf("FSM guard blocked: %s + %s\n", from_state, event); + } +}; + +/** Guard */ +struct GuardStartAllowed { + bool operator() (Context &ctx, const Idle &, const EvStart &) const noexcept { + return ctx.starts < 10U; + } +}; + +/** Action */ +struct ActionOnStart { + void operator() (Context &ctx, const Idle &, const Active &, const EvStart &) const noexcept { + ++ctx.starts; + } +}; + +/** Factory specialization (NOTE: now includes Context in template parameters) */ +template <> +struct state_factory { + static Active make (Context &, const Idle &, const EvStart &ev) { + Active a{}; + a.session_id = ev.requested_session; + return a; + } +}; + +/** Hooks (optional) */ +template <> +struct on_entry_state { + static void call (Context &, const Active &) noexcept { + // start timers, etc. + } +}; + +template <> +struct on_exit_state { + static void call (Context &, const Active &) noexcept { + // stop timers, etc. + } +}; + +template <> +struct on_entry_event { + static void call (Context &, const Active &, const EvStart &) noexcept { + // entered Active due to EvStart + } +}; + +/** Transitions */ +template <> +struct transition { + using type = transition_to; +}; + +template <> +struct transition { + using type = transition_to; +}; + +/** + * "Adult strictness": mark only required pairs strict. + * Everything else is ignore by default. + */ +template <> +struct missing_transition { + static constexpr missing_transition_policy policy = missing_transition_policy::strict; +}; + +template <> +struct missing_transition { + static constexpr missing_transition_policy policy = missing_transition_policy::strict; +}; + +/** FSM type */ +using MyFsm = fsm; + +static constexpr void validate_fsm_contract() { + MyFsm::validate_events(); +} + +int main() { + validate_fsm_contract(); + + Context ctx{}; + MyFsm f (ctx, Idle{}, MyLogger{}); + + f.dispatch (EvStart{42}); // Idle -> Active + f.dispatch (EvStop{}); // Active -> Idle + + // Not required by strict contract => default ignore (logged via ignored_transition) + f.dispatch (EvStop{}); // Idle + EvStop => ignored + f.dispatch (EvStart{7}); // (if currently Active) Active + EvStart => ignored + + return 0; +} diff --git a/templatefsm/templatefsm.pro b/templatefsm/templatefsm.pro new file mode 100644 index 0000000..51a0ecc --- /dev/null +++ b/templatefsm/templatefsm.pro @@ -0,0 +1,10 @@ +TEMPLATE = app +CONFIG += console c++17 +CONFIG -= app_bundle +CONFIG -= qt + +SOURCES += \ + main.cpp + +HEADERS += \ + fsm.h