Files
kindle2latex/fsm.h
2026-02-28 21:07:16 -05:00

296 lines
9.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <type_traits>
#include <utility>
#include <variant>
/** @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 <typename TContext, typename TFrom, typename TEvent>
bool operator() (TContext &, const TFrom &, const TEvent &) const noexcept {
return true;
}
};
/** @brief Default action: do nothing. */
struct do_nothing {
template <typename TContext, typename TFrom, typename TTo, typename TEvent>
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 <typename TTo, typename TGuard = always_allow, typename TAction = do_nothing>
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 <typename TFrom, typename TEvent>
struct transition {
using type = no_transition;
};
/** @brief Optional mapping to names for logs. */
template <typename T>
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 <typename TState>
struct on_exit_state {
template <typename TContext>
static void call (TContext &, const TState &) noexcept {}
};
template <typename TState>
struct on_entry_state {
template <typename TContext>
static void call (TContext &, const TState &) noexcept {}
};
/** @brief Hooks: event-specific. */
template <typename TState, typename TEvent>
struct on_exit_event {
template <typename TContext>
static void call (TContext &, const TState &, const TEvent &) noexcept {}
};
template <typename TState, typename TEvent>
struct on_entry_event {
template <typename TContext>
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<TContext>().
*
* Default: To{}.
* Specialize state_factory<TContext, From, Event, To> as needed.
*/
template <typename TContext, typename TFrom, typename TEvent, typename TTo>
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 <typename TFrom, typename TEvent>
struct missing_transition {
static constexpr missing_transition_policy policy = missing_transition_policy::ignore;
};
/** @brief Helper: is T one of Ts... */
template <typename T, typename... Ts>
struct is_one_of : std::disjunction<std::is_same<T, Ts>...> {};
/** @brief Helper: transition exists? */
template <typename TFrom, typename TEvent>
struct has_transition
: std::bool_constant < !std::is_same_v<typename transition<TFrom, TEvent>::type, no_transition >> {};
/** @brief Factory validation (call-form). */
template <typename TContext, typename TFrom, typename TEvent, typename TTo, typename = void>
struct is_factory_invocable : std::false_type {};
template <typename TContext, typename TFrom, typename TEvent, typename TTo>
struct is_factory_invocable <
TContext, TFrom, TEvent, TTo,
std::void_t<decltype (state_factory<TContext, TFrom, TEvent, TTo>::make (
std::declval<TContext &>(),
std::declval<const TFrom &>(),
std::declval<const TEvent &>())) >>
: std::bool_constant<std::is_same_v<
decltype (state_factory<TContext, TFrom, TEvent, TTo>::make (
std::declval<TContext &>(),
std::declval<const TFrom &>(),
std::declval<const TEvent &>())),
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<State,Event>::policy (ignore/strict)
*
* validate_events<Ev...>():
* - compile-time checks ONLY for pairs marked strict.
*/
template <typename TContext, typename TLogger, typename... TStates>
class fsm {
public:
using state_variant = std::variant<TStates...>;
template <typename TInitial,
typename = std::enable_if_t<is_one_of<TInitial, TStates...>::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 <typename TEvent>
bool dispatch (const TEvent &ev) {
dispatch_visitor<TEvent> v{*this, ev};
return std::visit (v, m_state);
}
template <typename... TEvents>
static constexpr void validate_events() {
(validate_one_event<TEvents>(), ...);
}
const state_variant &state() const noexcept {
return m_state;
}
state_variant &state() noexcept {
return m_state;
}
private:
template <typename TEvent>
struct dispatch_visitor {
fsm &self;
const TEvent &ev;
template <typename TFrom>
bool operator() (TFrom &from) const {
return self.template dispatch_from<TFrom> (from, ev);
}
};
struct init_entry_visitor {
fsm &self;
template <typename TState>
void operator() (TState &st) const {
on_entry_state<TState>::call (self.m_ctx, st);
}
};
template <typename TEvent>
struct entry_after_commit_visitor {
fsm &self;
const TEvent &ev;
template <typename TState>
void operator() (TState &st) const {
on_entry_state<TState>::call (self.m_ctx, st);
on_entry_event<TState, TEvent>::call (self.m_ctx, st, ev);
}
};
template <typename TFrom, typename TEvent>
bool dispatch_from (TFrom &from, const TEvent &ev) {
using tr = typename transition<TFrom, TEvent>::type;
if constexpr (std::is_same_v<tr, no_transition>) {
if constexpr (missing_transition<TFrom, TEvent>::policy == missing_transition_policy::strict)
m_logger.unhandled_transition (name_of<TFrom>::value, name_of<TEvent>::value);
else
m_logger.ignored_transition (name_of<TFrom>::value, name_of<TEvent>::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<to_state, TStates...>::value,
"FSM: transition target state not in FSM state list.");
static_assert (std::is_invocable_r_v<bool, guard_t, TContext &, const TFrom &, const TEvent &>,
"FSM: guard must be callable as bool(Context&, const From&, const Event&).");
static_assert (std::is_invocable_r_v<void, action_t, TContext &, const TFrom &, const to_state &, const TEvent &>,
"FSM: action must be callable as void(Context&, const From&, const To&, const Event&).");
static_assert (is_factory_invocable<TContext, TFrom, TEvent, to_state>::value,
"FSM: state_factory<Context,From,Event,To>::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<TFrom>::value, name_of<TEvent>::value);
return false;
}
on_exit_event<TFrom, TEvent>::call (m_ctx, from, ev);
on_exit_state<TFrom>::call (m_ctx, from);
to_state to = state_factory<TContext, TFrom, TEvent, to_state>::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<TEvent> {*this, ev}, m_state);
return true;
}
}
template <typename TEvent>
static constexpr void validate_one_event() {
(validate_pair<TStates, TEvent>(), ...);
}
template <typename TState, typename TEvent>
static constexpr void validate_pair() {
if constexpr (missing_transition<TState, TEvent>::policy == missing_transition_policy::strict) {
static_assert (has_transition<TState, TEvent>::value,
"FSM validation: required (strict) transition is missing for (State, Event).");
}
}
private:
TContext &m_ctx;
TLogger m_logger;
state_variant m_state;
};