From a1ec48b1eae8ad3e20f1459b4bc440250b7b3c17 Mon Sep 17 00:00:00 2001 From: tqcq <99722391+tqcq@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:28:46 +0000 Subject: [PATCH] feat add fsm --- CMakeLists.txt | 1 + src/sled/nonstd/fsm.h | 461 ++++++++++++++++++++++++++++++++++++ src/sled/nonstd/fsm_test.cc | 76 ++++++ src/sled/sled.h | 1 + 4 files changed, 539 insertions(+) create mode 100644 src/sled/nonstd/fsm.h create mode 100644 src/sled/nonstd/fsm_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bed65b..dda367e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,7 @@ if(SLED_BUILD_TESTS) sled_add_test(NAME sled_config_test SRCS src/sled/config_test.cc) sled_add_test(NAME sled_ioc_test SRCS src/sled/ioc/ioc_test.cc) sled_add_test(NAME sled_inja_test SRCS src/sled/nonstd/inja_test.cc) + sled_add_test(NAME sled_fsm_test SRCS src/sled/nonstd/fsm_test.cc) endif(SLED_BUILD_TESTS) if(SLED_BUILD_FUZZ) diff --git a/src/sled/nonstd/fsm.h b/src/sled/nonstd/fsm.h new file mode 100644 index 0000000..828335e --- /dev/null +++ b/src/sled/nonstd/fsm.h @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2015-2021 Thomas Kemmer + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef FSMLITE_FSM_H +#define FSMLITE_FSM_H + +#include +#include + +#if !defined(NDEBUG) && (!__GNUC__ || __EXCEPTIONS) +#include +#endif + +namespace fsmlite { +template +class Fsm; + +namespace detail { + +#if __cplusplus >= 201703L || _MSVC_LANG >= 201703L +template +using invoke_result_t = std::invoke_result_t; + +template +using is_invocable = std::is_invocable; +#elif __cplusplus >= 201103L || _MSVC_LANG >= 201103L +template +using invoke_result_t = typename std::result_of::type; + +struct is_invocable_test { + struct no_type { + int a; + int b; + }; + + template> + static char test(int); + + template + static no_type test(...); +}; + +template +using is_invocable = typename std::integral_constant(0)) == 1>::type; +#else +#error "fsmlite requires C++11 support." +#endif +// C++11 std::forward() is in , which may not be +// present on freestanding implementations +template +constexpr T && +forward(typename std::remove_reference::type &t) noexcept +{ + return static_cast(t); +} + +template +constexpr T && +forward(typename std::remove_reference::type &&t) noexcept +{ + return static_cast(t); +} + +// C++17 std::invoke() is in , which may not be +// present on freestanding implementations +template +invoke_result_t +invoke(F &&f, Args &&...args) +{ + return f(args...); +} + +template +invoke_result_t +invoke(M T::*f, T1 &&obj, Args &&...args) +{ + return (obj.*f)(args...); +} + +// use any of F(), F(Arg1), F(Arg2), F(Arg1, Arg2) +template::value, + bool f2 = is_invocable::value, + bool f3 = is_invocable::value, + bool f4 = is_invocable::value> +struct binary_fn_helper; + +template +struct binary_fn_helper { + using result_type = invoke_result_t; + + static result_type invoke(F &&f, Arg1 &&, Arg2 &&) { return detail::invoke(f); } +}; + +template +struct binary_fn_helper { + using result_type = invoke_result_t; + + static result_type invoke(F &&f, Arg1 &&a, Arg2 &&) { return detail::invoke(f, a); } +}; + +template +struct binary_fn_helper { + using result_type = invoke_result_t; + + static result_type invoke(F &&f, Arg1 &&, Arg2 &&b) { return detail::invoke(f, b); } +}; + +template +struct binary_fn_helper { + using result_type = invoke_result_t; + + static result_type invoke(F &&f, Arg1 &&a, Arg2 &&b) { return detail::invoke(f, a, b); } +}; + +template +using invoke_as_binary_fn_result_t = typename binary_fn_helper::result_type; + +template +invoke_as_binary_fn_result_t +invoke_as_binary_fn(F &&f, Arg1 &&a, Arg2 &&b) +{ + return binary_fn_helper::invoke( + detail::forward(f), + detail::forward(a), + detail::forward(b)); +} + +// basic template metaprogramming stuff; note that we could +// use std::tuple instead of list, but may nor be +// present on freestanding implementations +template +struct list {}; + +template +struct concat; + +template +struct concat> { + using type = list; +}; + +template<> +struct concat<> { + using type = list<>; +}; + +template class Predicate, class...> +struct filter; + +template class Predicate, class T, class... Types> +struct filter { + using type = typename std::conditional::value, + typename concat::type>::type, + typename filter::type>::type; +}; + +template class Predicate> +struct filter { + using type = list<>; +}; +}// namespace detail + +/** + * Finite state machine (FSM) base class template. + * + * @tparam Derived the derived state machine class + * + * @tparam State the FSM's state type, defaults to `int` + */ +template +class Fsm { +public: + /** + * The FSM's state type. + */ + typedef State state_type; + +public: + /** + * Create a state machine with an optional initial state. + * + * @param init_state the FSM's initial state + */ + Fsm(state_type init_state = state_type()) : m_state(init_state) {} + + /** + * Process an event. + * + * @warning This member function must not be called + * recursively, e.g. from another `fsm` instance. + * + * @tparam Event the event type + * + * @param event the event instance + * + * @throw std::logic_error if a recursive invocation is + * detected + */ + template + void ProcessEvent(const Event &event) + { + using rows = typename by_event_type::type; + processing_lock lock(*this); + static_assert(std::is_base_of::value, "must derive from fsm"); + Derived &self = static_cast(*this); + m_state = handle_event::execute(self, event, m_state); + } + + /** + * Return the state machine's current state. + */ + state_type CurrentState() const { return m_state; } + +protected: + /** + * Called when no transition can be found for the given event + * in the current state. Derived state machines may override + * this to throw an exception, or change to some other (error) + * state. The default is to return the current state, so no + * state change occurs. + * + * @tparam Event the event type + * + * @param event the event instance + * + * @return the FSM's new state + */ + template + state_type NoTransition(const Event & /*event*/) + { + return m_state; + } + +private: + template + struct row_base { + using state_type = State; + using event_type = Event; + + static constexpr state_type start_value() { return start; } + + static constexpr state_type target_value() { return target; } + + protected: + template + static void ProcessEvent(Action &&action, Derived &self, const Event &event) + { + detail::invoke_as_binary_fn(action, self, event); + } + + // clang++-5.0: constexpr function's return type 'void' is not a literal type + static /*constexpr*/ void ProcessEvent(std::nullptr_t, Derived & /*self*/, const Event & /*event*/) {} + + template + static bool check_guard(Guard &&guard, const Derived &self, const Event &event) + { + return detail::invoke_as_binary_fn(guard, self, event); + } + + static constexpr bool check_guard(std::nullptr_t, const Derived &, const Event &) { return true; } + }; + +protected: + /** + * Transition table variadic class template. + * + * Each derived state machine class must define a nested + * non-template type `transition_table` that's either derived + * from or a type alias of `table`. + */ + template + using table = detail::list; + + /** + * Basic transition class template. + * + * @tparam start the start state of the transition + * + * @tparam Event the event type triggering the transition + * + * @tparam target the target state of the transition + * + * @tparam Action an action function type, or `std::nullptr_t` + * + * @tparam action a static `Action` instance + * + * @tparam Guard a guard function type, or `std::nullptr_t` + * + * @tparam guard a static `Guard` instance + */ + template + struct basic_row : public row_base { + static void process_event(Derived &self, const Event &event) + { + row_base::ProcessEvent(action, self, event); + } + + static bool check_guard(const Derived &self, const Event &event) + { + return row_base::check_guard(guard, self, event); + } + }; + +#if __cplusplus >= 201703L || _MSVC_LANG >= 201703L + /** + * Generic transition class template (requires C++17). + * + * @tparam start the start state of the transition + * + * @tparam Event the event type triggering the transition + * + * @tparam target the target state of the transition + * + * @tparam action a static action function pointer, or `nullptr` + * + * @tparam guard a static guard function pointer, or `nullptr` + */ + template + struct row : public row_base { + static void ProcessEvent(Derived &self, const Event &event) + { + row_base::process_event(action, self, event); + } + + static bool check_guard(const Derived &self, const Event &event) + { + return row_base::check_guard(guard, self, event); + } + }; +#else + /** + * Member function transition class template. + * + * @tparam start the start state of the transition + * + * @tparam Event the event type triggering the transition + * + * @tparam target the target state of the transition + * + * @tparam action an action member function, or `nullptr` + * + * @tparam guard a guard member function, or `nullptr` + */ + template + struct row : public row_base { + static void ProcessEvent(Derived &self, const Event &event) + { + if (action != nullptr) { row_base::ProcessEvent(action, self, event); } + } + + static bool check_guard(const Derived &self, const Event &event) + { + if (guard != nullptr) { + return row_base::check_guard(guard, self, event); + } else { + return true; + } + } + }; +#endif + +private: + template + struct by_event_type; + + template + struct by_event_type> { + template + using predicate = std::is_same; + using type = typename detail::filter::type; + }; + + template + struct by_event_type> { + using type = detail::list<>; + }; + + template + struct handle_event; + + template + struct handle_event> { + static State execute(Derived &self, const Event &event, State state) + { + return state == T::start_value() && T::check_guard(self, event) ? T::ProcessEvent(self, event), + T::target_value() : handle_event>::execute(self, event, state); + } + }; + + template + struct handle_event> { + static State execute(Derived &self, const Event &event, State) { return self.NoTransition(event); } + }; + +private: + state_type m_state; + +private: +#if !defined(NDEBUG) && (!__GNUC__ || __EXCEPTIONS) + class processing_lock { + public: + processing_lock(Fsm &m) : processing(m.processing) + { + if (processing) { throw std::logic_error("process_event called recursively"); } + processing = true; + } + + ~processing_lock() { processing = false; } + + private: + bool &processing; + }; + + bool processing = false; +#else + struct processing_lock { + processing_lock(fsm &) {} + }; +#endif +}; +}// namespace fsmlite + +namespace sled { +template +using Fsm = fsmlite::Fsm; +} +#endif diff --git a/src/sled/nonstd/fsm_test.cc b/src/sled/nonstd/fsm_test.cc new file mode 100644 index 0000000..f5e66f8 --- /dev/null +++ b/src/sled/nonstd/fsm_test.cc @@ -0,0 +1,76 @@ +#include + +enum class State { + kIdle, + kStarted, + kEnd, +}; + +struct FSMTest : public sled::Fsm { +public: + FSMTest() : Fsm(State::kIdle) {} + + struct StartEvent { + StartEvent(bool do_nothing = false) : do_nothing(do_nothing) {} + + bool do_nothing = false; + }; + + struct StopEvent {}; + + struct ResetEvent {}; + + template + state_type NoTransition(const E &) + { + ++error_count; + return CurrentState(); + } + + int error_count = 0; + +private: + bool StartCheck(const StartEvent &e) const { return !e.do_nothing; } + + void OnStart(const StartEvent &) {} + + void OnStop(const StopEvent &) {} + + void OnReset(const ResetEvent &) {} + +public: + using TransitionTable + = table, + row, + row>; +}; + +TEST_SUITE("fsm") +{ + TEST_CASE("Entry Exit") + { + FSMTest fsm_test; + CHECK_EQ(fsm_test.CurrentState(), State::kIdle); + CHECK_EQ(fsm_test.error_count, 0); + + fsm_test.ProcessEvent(FSMTest::StartEvent(true)); + CHECK_EQ(fsm_test.CurrentState(), State::kIdle); + CHECK_EQ(fsm_test.error_count, 1); + + fsm_test.ProcessEvent(FSMTest::StartEvent()); + CHECK_EQ(fsm_test.CurrentState(), State::kStarted); + CHECK_EQ(fsm_test.error_count, 1); + + fsm_test.ProcessEvent(FSMTest::StopEvent()); + CHECK_EQ(fsm_test.CurrentState(), State::kEnd); + CHECK_EQ(fsm_test.error_count, 1); + + fsm_test.ProcessEvent(FSMTest::ResetEvent()); + CHECK_EQ(fsm_test.CurrentState(), State::kIdle); + CHECK_EQ(fsm_test.error_count, 1); + + fsm_test.ProcessEvent(FSMTest::ResetEvent()); + CHECK_EQ(fsm_test.CurrentState(), State::kIdle); + CHECK_EQ(fsm_test.error_count, 2); + } +} diff --git a/src/sled/sled.h b/src/sled/sled.h index 2d2d4cb..39c88ed 100644 --- a/src/sled/sled.h +++ b/src/sled/sled.h @@ -7,6 +7,7 @@ #include "sled/async/async.h" #include "sled/nonstd/cxxopts.h" #include "sled/nonstd/expected.h" +#include "sled/nonstd/fsm.h" #include "sled/nonstd/inja.h" #include "sled/nonstd/string_view.h" #include "toml.hpp"