├── tests ├── main.cpp ├── inline_fsm.cpp ├── benchmarks.cpp ├── fsm_nothrow.cpp ├── fsm.cpp ├── constexpr_fsm_win.cpp ├── constexpr_fsm.cpp └── hfsm.cpp ├── conanfile.txt ├── CMakeLists.conan.txt ├── appveyor.yml ├── license ├── .travis.yml ├── readme.md ├── .clang-format ├── CMakeLists.txt ├── .gitignore └── include └── fea_state_machines ├── fsm.hpp ├── constexpr_fsm.hpp └── hfsm.hpp /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char** argv) { 4 | ::testing::InitGoogleTest(&argc, argv); 5 | return RUN_ALL_TESTS(); 6 | } 7 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | gtest/1.10.0@_/_ 3 | 4 | [generators] 5 | cmake_find_package_multi 6 | 7 | [options] 8 | gtest:build_gmock=False 9 | 10 | [imports] 11 | bin, *.pdb -> ./bin 12 | bin, *.pdb -> ./lib 13 | bin, *.dll -> ./bin 14 | -------------------------------------------------------------------------------- /CMakeLists.conan.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.10) 2 | project(conan-setup NONE) 3 | 4 | execute_process(COMMAND conan install ${CMAKE_CURRENT_SOURCE_DIR} --build missing -s build_type=Debug) 5 | execute_process(COMMAND conan install ${CMAKE_CURRENT_SOURCE_DIR} --build missing -s build_type=Release) 6 | -------------------------------------------------------------------------------- /tests/inline_fsm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | template 6 | inline auto runtime_get(Func func, Tuple& tup, size_t idx) { 7 | if (N == idx) { 8 | return func(std::get(tup)); 9 | } 10 | 11 | if constexpr (N + 1 < std::tuple_size_v) { 12 | return runtime_get(func, tup, idx); 13 | } 14 | } 15 | 16 | // TEST(inline_fsm, basics) { 17 | // 18 | // // std::tuple t{ 42, 3 }; 19 | // // size_t runtime = 0; 20 | // 21 | // // runtime_get([](auto& myval) { printf("\n%d\n\n", myval); }, t, 0); 22 | //} 23 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Visual Studio 2019 3 | platform: 4 | - x64 5 | - Win32 6 | configuration: 7 | - Debug 8 | - Release 9 | clone_folder: c:\projects\fea_state_machines 10 | install: 11 | - cmd: set PATH=%PATH%;%PYTHON%/Scripts/ 12 | - cmd: pip.exe install conan 13 | - cmd: cd .. 14 | - cmd: conan user 15 | - cmd: conan --version 16 | - cmd: conan remote add bincrafters https://api.bintray.com/conan/conan/conan-center 17 | - cmd: conan profile new default --detect 18 | - cmd: cd c:\projects\fea_state_machines 19 | build: 20 | project: c:\projects\fea_state_machines\build\fea_state_machines.sln 21 | parallel: true 22 | test_script: 23 | - cmd: c:\projects\fea_state_machines\build\bin\fea_state_machines_tests.exe 24 | for: 25 | - 26 | matrix: 27 | only: 28 | - platform: x64 29 | before_build: 30 | - cmd: conan profile update settings.arch="x86_64" default 31 | - cmd: cd c:\projects\fea_state_machines 32 | - cmd: mkdir build 33 | - cmd: cd build 34 | - cmd: cmake .. -A %PLATFORM% -DBUILD_TESTING=On 35 | - 36 | matrix: 37 | only: 38 | - platform: Win32 39 | before_build: 40 | - cmd: conan profile update settings.arch="x86" default 41 | - cmd: cd c:\projects\fea_state_machines 42 | - cmd: mkdir build 43 | - cmd: cd build 44 | - cmd: cmake .. -A %PLATFORM% -DBUILD_TESTING=On -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Philippe Groarke 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | before_install: 4 | - eval "${MATRIX_EVAL}" 5 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 6 | mkdir $HOME/usr; 7 | export PATH="$HOME/usr/bin:$PATH"; 8 | wget https://cmake.org/files/v3.16/cmake-3.16.0-Linux-x86_64.sh; 9 | chmod +x cmake-3.16.0-Linux-x86_64.sh; 10 | ./cmake-3.16.0-Linux-x86_64.sh --prefix=$HOME/usr --exclude-subdir --skip-license; 11 | fi 12 | install: 13 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 14 | pyenv global 3.7; 15 | fi 16 | - pip3 install conan 17 | - conan user 18 | - conan remote add bincrafters https://api.bintray.com/conan/conan/conan-center 19 | - conan profile new default --detect 20 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 21 | conan profile update settings.compiler.libcxx=libstdc++11 default; 22 | fi 23 | 24 | matrix: 25 | include: 26 | - os: osx 27 | osx_image: xcode11.2 28 | env: 29 | - MATRIX_EVAL="CC=clang && CXX=clang++ && CONFIG=Debug" 30 | 31 | - os: osx 32 | osx_image: xcode11.2 33 | env: 34 | - MATRIX_EVAL="CC=clang && CXX=clang++ && CONFIG=Release" 35 | 36 | - os: linux 37 | dist: bionic 38 | addons: 39 | apt: 40 | sources: 41 | - ubuntu-toolchain-r-test 42 | packages: 43 | - gcc-7 44 | - g++-7 45 | env: 46 | - MATRIX_EVAL="CC=gcc-7 && CXX=g++-7 && CONFIG=Debug" 47 | 48 | - os: linux 49 | dist: bionic 50 | addons: 51 | apt: 52 | sources: 53 | - ubuntu-toolchain-r-test 54 | packages: 55 | - gcc-7 56 | - g++-7 57 | env: 58 | - MATRIX_EVAL="CC=gcc-7 && CXX=g++-7 && CONFIG=Release" 59 | 60 | - os: linux 61 | dist: bionic 62 | addons: 63 | apt: 64 | sources: 65 | - ubuntu-toolchain-r-test 66 | packages: 67 | - gcc-8 68 | - g++-8 69 | env: 70 | - MATRIX_EVAL="CC=gcc-8 && CXX=g++-8 && CONFIG=Debug" 71 | 72 | - os: linux 73 | dist: bionic 74 | addons: 75 | apt: 76 | sources: 77 | - ubuntu-toolchain-r-test 78 | packages: 79 | - gcc-8 80 | - g++-8 81 | env: 82 | - MATRIX_EVAL="CC=gcc-8 && CXX=g++-8 && CONFIG=Release" 83 | 84 | script: 85 | - mkdir build && cd build 86 | - cmake .. -DBUILD_TESTING=On -DCMAKE_BUILD_TYPE=$CONFIG 87 | - cmake --build . --config $CONFIG 88 | - ./bin/fea_state_machines_tests 89 | 90 | notifications: 91 | email: 92 | on_success: never 93 | on_failure: always 94 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # This repository is inactive and kept for posterity's sake. 4 | # Any further development will happen in [fea_libs](https://github.com/p-groarke/fea_libs). Cheers. 5 |
6 |
7 | 8 | # fea_state_machines 9 | [![Build status](https://ci.appveyor.com/api/projects/status/vf630kekui0oanfn/branch/master?svg=true)](https://ci.appveyor.com/project/p-groarke/fea-state-machines/branch/master) 10 | [![Build Status](https://travis-ci.org/p-groarke/fea_state_machines.svg?branch=master)](https://travis-ci.org/p-groarke/fea_state_machines) 11 | 12 | A buffet of state machines. 13 | 14 | ## Available Machines 15 | 16 | Each header contains a more detailed explanation of the state machine's features, design and limitations. Here is a quick overview. 17 | 18 | ### fsm.hpp 19 | 20 | This is a stack based state machine. It is very fast and supports the most essential fsm features. Under the hood, it uses `std::array` and `std::function`. This is a great fsm if you need many machines running concurrently, or you need a very fast runtime machine. The design is very simple, making it robust and easy to debug. 21 | 22 | ### hfsm.hpp 23 | 24 | This is the "big boi". A full implementation of a hierarchical finite state machine (aka [state chart](https://statecharts.github.io/)). It is a heap fsm, evaluation uses a queue to process your events. It has all the bells and whistles : transition guards, auto transition guards, history state, parallel states, parent/children states, etc. Use this for very complex behavior you need to manage. 25 | 26 | ### constexpr_fsm.hpp 27 | 28 | This is a compile-time executable state machine. It is mostly a novel design to experiment with the possibilities opened up when creating a truly `constexpr` state machine. The features are quite limited, though you have the added benefit of compile-time validation. For experimentation only, a thorough deep-dive is available in this [blog post](https://philippegroarke.com/posts/2020/constexpr_fsm/). 29 | 30 | ### inlined_fsm 31 | 32 | TODO: This will be a mirror state machine to fsm.hpp. It should have a reasonable amount of features, with less overhead using `std::tuple` and storing your functions directly without the need of `std::function` or C pointers. If this gains feature parity with fsm.hpp, it will replace it. 33 | 34 | 35 | ## Build 36 | `fea_state_machines` is a header only state machine lib with no dependencies other than the stl. 37 | 38 | The unit tests depend on gtest. They are not built by default. 39 | 40 | Install recent conan, cmake and compiler. 41 | 42 | ```bash 43 | mkdir build && cd build 44 | cmake .. -DBUILD_TESTING=On && cmake --build . --config debug 45 | bin/fea_state_machines_tests.exe 46 | 47 | // Optionally 48 | cmake --build . --target install 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: DontAlign 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: DontAlign 9 | AlignOperands: false 10 | AlignTrailingComments: false 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: false 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: true 22 | BinPackParameters: true 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | BeforeCatch: false 33 | BeforeElse: false 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: All 36 | BreakBeforeBraces: Attach 37 | BreakBeforeInheritanceComma: false 38 | BreakBeforeTernaryOperators: true 39 | BreakConstructorInitializersBeforeComma: false 40 | BreakConstructorInitializers: BeforeComma 41 | BreakAfterJavaFieldAnnotations: false 42 | BreakStringLiterals: true 43 | ColumnLimit: 80 44 | CommentPragmas: '^ IWYU pragma:' 45 | CompactNamespaces: false 46 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 47 | ConstructorInitializerIndentWidth: 8 48 | ContinuationIndentWidth: 8 49 | Cpp11BracedListStyle: false 50 | DerivePointerAlignment: false 51 | DisableFormat: false 52 | ExperimentalAutoDetectBinPacking: false 53 | FixNamespaceComments: true 54 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 55 | IncludeCategories: 56 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 57 | Priority: 2 58 | - Regex: '^(<|"(gtest|isl|json)/)' 59 | Priority: 3 60 | - Regex: '.*' 61 | Priority: 1 62 | IncludeIsMainRegex: '$' 63 | IndentCaseLabels: false 64 | IndentWidth: 4 65 | IndentWrappedFunctionNames: false 66 | JavaScriptQuotes: Leave 67 | JavaScriptWrapImports: true 68 | KeepEmptyLinesAtTheStartOfBlocks: true 69 | MacroBlockBegin: '' 70 | MacroBlockEnd: '' 71 | MaxEmptyLinesToKeep: 2 72 | NamespaceIndentation: None 73 | ObjCBlockIndentWidth: 4 74 | ObjCSpaceAfterProperty: false 75 | ObjCSpaceBeforeProtocolList: true 76 | PenaltyBreakAssignment: 2 77 | PenaltyBreakBeforeFirstCallParameter: 2000 78 | PenaltyBreakComment: 1 79 | PenaltyBreakFirstLessLess: 120 80 | PenaltyBreakString: 1000 81 | PenaltyExcessCharacter: 1000000 82 | PenaltyReturnTypeOnItsOwnLine: 60 83 | PointerAlignment: Left 84 | ReflowComments: true 85 | SortIncludes: true 86 | SpaceAfterCStyleCast: false 87 | SpaceAfterTemplateKeyword: true 88 | SpaceBeforeAssignmentOperators: true 89 | SpaceBeforeParens: ControlStatements 90 | SpaceInEmptyParentheses: false 91 | SpacesBeforeTrailingComments: 1 92 | SpacesInAngles: false 93 | SpacesInContainerLiterals: true 94 | SpacesInCStyleCastParentheses: false 95 | SpacesInParentheses: false 96 | SpacesInSquareBrackets: false 97 | Standard: Cpp11 98 | TabWidth: 4 99 | UseTab: Always 100 | ... 101 | 102 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.conan.txt) 2 | 3 | cmake_minimum_required (VERSION 3.15) 4 | project(fea_state_machines VERSION 1.1.0 LANGUAGES CXX) 5 | 6 | include(GNUInstallDirs) 7 | include(CMakePackageConfigHelpers) 8 | include(GoogleTest) 9 | include(FetchContent) 10 | 11 | set(CMAKE_CXX_STANDARD 17) 12 | 13 | # Conan search path. 14 | set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH}) 15 | set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR} ${CMAKE_PREFIX_PATH}) 16 | 17 | # Output binary to predictable location (fixes cyclic dependency issues). 18 | set(BINARY_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/bin) 19 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${BINARY_OUT_DIR}) 20 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${BINARY_OUT_DIR}) 21 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${BINARY_OUT_DIR}) 22 | 23 | foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) 24 | string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG) 25 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${BINARY_OUT_DIR}) 26 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${BINARY_OUT_DIR}) 27 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${BINARY_OUT_DIR}) 28 | endforeach(OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES) 29 | 30 | # Organize unrelated targets to clean IDE hierarchy. 31 | set(DEPENDENCY_FOLDER "Dependencies") 32 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 33 | set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER ${DEPENDENCY_FOLDER}) 34 | 35 | # Compile Options 36 | function(set_compile_options REQUIRED_ARG EXPOSURE) 37 | if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") 38 | target_compile_options(${REQUIRED_ARG} ${EXPOSURE} -Wall -Wextra -Wpedantic -Werror -Wno-gnu-zero-variadic-macro-arguments) 39 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 40 | target_compile_options(${REQUIRED_ARG} ${EXPOSURE} -Wall -Wextra -Wpedantic -Werror) 41 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel") 42 | # using Intel C++ 43 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") 44 | target_compile_definitions(${REQUIRED_ARG} ${EXPOSURE} NOMINMAX UNICODE _UNICODE) 45 | target_compile_options(${REQUIRED_ARG} ${EXPOSURE} /Zc:__cplusplus /Zc:alignedNew /permissive- /FAs /W4 /WX /utf-8) 46 | # set_target_properties(${REQUIRED_ARG} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 47 | endif() 48 | endfunction() 49 | 50 | # clang-format 51 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/.clang-format ${CMAKE_CURRENT_BINARY_DIR}/.clang-format COPYONLY) 52 | 53 | 54 | # Main Project 55 | file(GLOB_RECURSE HEADER_FILES "${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/*.hpp") 56 | add_library(${PROJECT_NAME} INTERFACE) 57 | 58 | # To see files in IDE 59 | target_sources(${PROJECT_NAME} INTERFACE 60 | "$" 61 | ) 62 | 63 | # Interface 64 | target_include_directories(${PROJECT_NAME} INTERFACE 65 | $ 66 | $ 67 | ) 68 | set_compile_options(${PROJECT_NAME} INTERFACE) 69 | 70 | 71 | # Install Package Configuration 72 | install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}_targets) 73 | 74 | install(EXPORT ${PROJECT_NAME}_targets 75 | NAMESPACE ${PROJECT_NAME}:: 76 | FILE ${PROJECT_NAME}-config.cmake 77 | DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${PROJECT_NAME}" 78 | ) 79 | 80 | install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/${PROJECT_NAME}" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") 81 | 82 | 83 | # Tests 84 | option(FEA_STATE_MACHINES_TESTS "Build and run tests." On) 85 | if (${FEA_STATE_MACHINES_TESTS}) 86 | enable_testing() 87 | 88 | # Tests external dependencies. 89 | FetchContent_Declare(fea_benchmark 90 | GIT_REPOSITORY https://github.com/p-groarke/fea_benchmark.git 91 | ) 92 | FetchContent_MakeAvailable(fea_benchmark) 93 | set_target_properties(fea_benchmark_tests PROPERTIES FOLDER ${DEPENDENCY_FOLDER}) 94 | 95 | find_package(GTest CONFIG REQUIRED) 96 | 97 | 98 | # Test Project 99 | set(TEST_NAME ${PROJECT_NAME}_tests) 100 | file(GLOB_RECURSE TEST_SOURCES "tests/*.cpp" "tests/*.c" "tests/*.hpp" "tests/*.h" "tests/*.tpp") 101 | add_executable(${TEST_NAME} ${TEST_SOURCES}) 102 | set_compile_options(${TEST_NAME} PRIVATE) 103 | set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${TEST_NAME}) 104 | 105 | target_link_libraries(${TEST_NAME} PRIVATE ${PROJECT_NAME} GTest::GTest fea_benchmark) 106 | gtest_discover_tests(${TEST_NAME}) 107 | 108 | endif() # FEA_STATE_MACHINES_TESTS 109 | -------------------------------------------------------------------------------- /tests/benchmarks.cpp: -------------------------------------------------------------------------------- 1 | //#include "transitions_and_states.gen.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | //#if defined(NDEBUG) 13 | 14 | namespace { 15 | // constexpr size_t num_trans_and_states = 10; 16 | // 17 | // void gen_header() { 18 | // std::ofstream ofs{ "transitions_and_states.gen.hpp" }; 19 | // 20 | // ofs << "#pragma once" << std::endl; 21 | // ofs << R"( 22 | //#include 23 | //#include 24 | //)"; 25 | // 26 | // ofs << "enum class transition : unsigned {" << std::endl; 27 | // for (size_t i = 0; i < num_trans_and_states; ++i) { 28 | // ofs << "\tt" << std::to_string(i) << "," << std::endl; 29 | // } 30 | // ofs << "count" << std::endl; 31 | // ofs << "};" << std::endl << std::endl; 32 | // 33 | // ofs << "enum class state : unsigned {" << std::endl; 34 | // for (size_t i = 0; i < num_trans_and_states; ++i) { 35 | // ofs << "\ts" << std::to_string(i) << "," << std::endl; 36 | // } 37 | // ofs << "count" << std::endl; 38 | // ofs << "};" << std::endl << std::endl; 39 | // 40 | // ofs << R"( 41 | // template 42 | // void add_states_and_transitions(Machine* m) { 43 | //)"; 44 | // 45 | // for (size_t s = 0; s < num_trans_and_states; ++s) { 46 | // ofs << R"( 47 | // { 48 | // auto mstate = std::make_unique< 49 | // fea::fsm_state>(); 50 | // 51 | // mstate->add_event( 52 | // [](auto&, size_t& event_counter) { ++event_counter; }); 53 | // mstate->add_event( 54 | // [](auto&, size_t& event_counter) { ++event_counter; }); 55 | // mstate->add_event( 56 | // [](auto&, size_t& event_counter) { ++event_counter; }); 57 | //)"; 58 | // 59 | // for (size_t i = 0; i < num_trans_and_states; ++i) { 60 | // ofs << "\t\tmstate->add_transition();" << std::endl; 63 | // } 64 | // ofs << "\t\tm->add_state(std::move(*mstate));" << std::endl; 66 | // 67 | // ofs << "\t}" << std::endl; 68 | // } 69 | // ofs << "}" << std::endl << std::endl; 70 | // 71 | // ofs << R"( 72 | // template 73 | // void do_benchmark(Machine * m, size_t & event_counter) { 74 | //)"; 75 | // 76 | // for (size_t i = 0; i < num_trans_and_states; ++i) { 77 | // ofs << "\tm->update(event_counter);" << std::endl; 78 | // ofs << "\tm->trigger(event_counter);" << std::endl; 80 | // } 81 | // 82 | // ofs << "}" << std::endl; 83 | //} 84 | 85 | // template 86 | // void add_states_and_transitions(Machine* m) { 87 | // { 88 | // auto mstate = std::make_unique< 89 | // fea::fsm_state>(); 90 | // 91 | // mstate->add_event( 92 | // [](auto&, size_t& event_counter) { ++event_counter; }); 93 | // mstate->add_event( 94 | // [](auto&, size_t& event_counter) { ++event_counter; }); 95 | // mstate->add_event( 96 | // [](auto&, size_t& event_counter) { ++event_counter; }); 97 | // 98 | // mstate->add_transition(); 99 | // m->add_state(std::move(*mstate)); 100 | // } 101 | //} 102 | 103 | // template 104 | // void do_benchmark(Machine* m, size_t& event_counter) { 105 | // for (size_t i = 0; i < size_t(transition::count); ++i) { 106 | // m->update(event_counter); 107 | // m->trigger(event_counter); 108 | // } 109 | //} 110 | 111 | // TEST(simple_fsm, benchmarks) { 112 | // gen_header(); 113 | // 114 | // // bench::title("100 states, 100 transitions each"); 115 | // // bench::start(); 116 | // // std::unique_ptr> machine 117 | // // = std::make_unique>(); 118 | // // add_states_and_transitions(machine.get()); 119 | // // bench::stop("build machine"); 120 | // 121 | // // size_t event_counter = 0; 122 | // 123 | // // bench::start(); 124 | // // do_benchmark(machine.get(), event_counter); 125 | // // bench::stop("update and transition all states once"); 126 | // 127 | // // printf("\nNum total events called : %zu\n", event_counter); 128 | //} 129 | } // namespace 130 | 131 | //#endif // NDEBUG 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Compiled Dynamic libraries 12 | *.so 13 | *.dylib 14 | *.dll 15 | 16 | # Fortran module files 17 | *.mod 18 | *.smod 19 | 20 | # Compiled Static libraries 21 | *.lai 22 | *.la 23 | *.a 24 | *.lib 25 | 26 | # Executables 27 | *.exe 28 | *.out* 29 | *.app 30 | 31 | .DS_Store 32 | [Bb]uild*/ 33 | 34 | ## Ignore Visual Studio temporary files, build results, and 35 | ## files generated by popular Visual Studio add-ons. 36 | ## 37 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.userosscache 43 | *.sln.docstates 44 | 45 | # User-specific files (MonoDevelop/Xamarin Studio) 46 | *.userprefs 47 | 48 | # Build results 49 | [Dd]ebug/ 50 | [Dd]ebugPublic/ 51 | [Rr]elease/ 52 | [Rr]eleases/ 53 | x64/ 54 | x86/ 55 | bld/ 56 | [Bb]in/ 57 | [Oo]bj/ 58 | [Ll]og/ 59 | 60 | # Visual Studio 2015 cache/options directory 61 | .vs/ 62 | # Uncomment if you have tasks that create the project's static files in wwwroot 63 | #wwwroot/ 64 | 65 | # MSTest test Results 66 | [Tt]est[Rr]esult*/ 67 | [Bb]uild[Ll]og.* 68 | 69 | # NUNIT 70 | *.VisualState.xml 71 | TestResult.xml 72 | 73 | # Build Results of an ATL Project 74 | [Dd]ebugPS/ 75 | [Rr]eleasePS/ 76 | dlldata.c 77 | 78 | # Benchmark Results 79 | BenchmarkDotNet.Artifacts/ 80 | 81 | # .NET Core 82 | project.lock.json 83 | project.fragment.lock.json 84 | artifacts/ 85 | **/Properties/launchSettings.json 86 | 87 | *_i.c 88 | *_p.c 89 | *_i.h 90 | *.ilk 91 | *.meta 92 | *.obj 93 | *.pch 94 | *.pdb 95 | *.pgc 96 | *.pgd 97 | *.rsp 98 | *.sbr 99 | *.tlb 100 | *.tli 101 | *.tlh 102 | *.tmp 103 | *.tmp_proj 104 | *.log 105 | *.vspscc 106 | *.vssscc 107 | .builds 108 | *.pidb 109 | *.svclog 110 | *.scc 111 | 112 | # Chutzpah Test files 113 | _Chutzpah* 114 | 115 | # Visual C++ cache files 116 | ipch/ 117 | *.aps 118 | *.ncb 119 | *.opendb 120 | *.opensdf 121 | *.sdf 122 | *.cachefile 123 | *.VC.db 124 | *.VC.VC.opendb 125 | 126 | # Visual Studio profiler 127 | *.psess 128 | *.vsp 129 | *.vspx 130 | *.sap 131 | 132 | # Visual Studio Trace Files 133 | *.e2e 134 | 135 | # TFS 2012 Local Workspace 136 | $tf/ 137 | 138 | # Guidance Automation Toolkit 139 | *.gpState 140 | 141 | # ReSharper is a .NET coding add-in 142 | _ReSharper*/ 143 | *.[Rr]e[Ss]harper 144 | *.DotSettings.user 145 | 146 | # JustCode is a .NET coding add-in 147 | .JustCode 148 | 149 | # TeamCity is a build add-in 150 | _TeamCity* 151 | 152 | # DotCover is a Code Coverage Tool 153 | *.dotCover 154 | 155 | # AxoCover is a Code Coverage Tool 156 | .axoCover/* 157 | !.axoCover/settings.json 158 | 159 | # Visual Studio code coverage results 160 | *.coverage 161 | *.coveragexml 162 | 163 | # NCrunch 164 | _NCrunch_* 165 | .*crunch*.local.xml 166 | nCrunchTemp_* 167 | 168 | # MightyMoose 169 | *.mm.* 170 | AutoTest.Net/ 171 | 172 | # Web workbench (sass) 173 | .sass-cache/ 174 | 175 | # Installshield output folder 176 | [Ee]xpress/ 177 | 178 | # DocProject is a documentation generator add-in 179 | DocProject/buildhelp/ 180 | DocProject/Help/*.HxT 181 | DocProject/Help/*.HxC 182 | DocProject/Help/*.hhc 183 | DocProject/Help/*.hhk 184 | DocProject/Help/*.hhp 185 | DocProject/Help/Html2 186 | DocProject/Help/html 187 | 188 | # Click-Once directory 189 | publish/ 190 | 191 | # Publish Web Output 192 | *.[Pp]ublish.xml 193 | *.azurePubxml 194 | # Note: Comment the next line if you want to checkin your web deploy settings, 195 | # but database connection strings (with potential passwords) will be unencrypted 196 | *.pubxml 197 | *.publishproj 198 | 199 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 200 | # checkin your Azure Web App publish settings, but sensitive information contained 201 | # in these scripts will be unencrypted 202 | PublishScripts/ 203 | 204 | # NuGet Packages 205 | *.nupkg 206 | # The packages folder can be ignored because of Package Restore 207 | **/packages/* 208 | # except build/, which is used as an MSBuild target. 209 | !**/packages/build/ 210 | # Uncomment if necessary however generally it will be regenerated when needed 211 | #!**/packages/repositories.config 212 | # NuGet v3's project.json files produces more ignorable files 213 | *.nuget.props 214 | *.nuget.targets 215 | 216 | # Microsoft Azure Build Output 217 | csx/ 218 | *.build.csdef 219 | 220 | # Microsoft Azure Emulator 221 | ecf/ 222 | rcf/ 223 | 224 | # Windows Store app package directories and files 225 | AppPackages/ 226 | BundleArtifacts/ 227 | Package.StoreAssociation.xml 228 | _pkginfo.txt 229 | *.appx 230 | 231 | # Visual Studio cache files 232 | # files ending in .cache can be ignored 233 | *.[Cc]ache 234 | # but keep track of directories ending in .cache 235 | !*.[Cc]ache/ 236 | 237 | # Others 238 | ClientBin/ 239 | ~$* 240 | *~ 241 | *.dbmdl 242 | *.dbproj.schemaview 243 | *.jfm 244 | *.pfx 245 | *.publishsettings 246 | orleans.codegen.cs 247 | 248 | # Since there are multiple workflows, uncomment next line to ignore bower_components 249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 250 | #bower_components/ 251 | 252 | # RIA/Silverlight projects 253 | Generated_Code/ 254 | 255 | # Backup & report files from converting an old project file 256 | # to a newer Visual Studio version. Backup files are not needed, 257 | # because we have git ;-) 258 | _UpgradeReport_Files/ 259 | Backup*/ 260 | UpgradeLog*.XML 261 | UpgradeLog*.htm 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | 273 | # Microsoft Fakes 274 | FakesAssemblies/ 275 | 276 | # GhostDoc plugin setting file 277 | *.GhostDoc.xml 278 | 279 | # Node.js Tools for Visual Studio 280 | .ntvs_analysis.dat 281 | node_modules/ 282 | 283 | # Typescript v1 declaration files 284 | typings/ 285 | 286 | # Visual Studio 6 build log 287 | *.plg 288 | 289 | # Visual Studio 6 workspace options file 290 | *.opt 291 | 292 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 293 | *.vbw 294 | 295 | # Visual Studio LightSwitch build output 296 | **/*.HTMLClient/GeneratedArtifacts 297 | **/*.DesktopClient/GeneratedArtifacts 298 | **/*.DesktopClient/ModelManifest.xml 299 | **/*.Server/GeneratedArtifacts 300 | **/*.Server/ModelManifest.xml 301 | _Pvt_Extensions 302 | 303 | # Paket dependency manager 304 | .paket/paket.exe 305 | paket-files/ 306 | 307 | # FAKE - F# Make 308 | .fake/ 309 | 310 | # JetBrains Rider 311 | .idea/ 312 | *.sln.iml 313 | 314 | # CodeRush 315 | .cr/ 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ -------------------------------------------------------------------------------- /tests/fsm_nothrow.cpp: -------------------------------------------------------------------------------- 1 | #define FEA_FSM_NOTHROW 2 | #include 3 | #include 4 | 5 | 6 | namespace { 7 | 8 | TEST(fsm_nothrow, example) { 9 | struct test_data { 10 | bool walk_enter = false; 11 | bool walk_update = false; 12 | 13 | size_t num_onenterfrom_calls = 0; 14 | size_t num_onenter_calls = 0; 15 | size_t num_onupdate_calls = 0; 16 | size_t num_onexit_calls = 0; 17 | size_t num_onexitto_calls = 0; 18 | }; 19 | test_data mtest_data; 20 | 21 | // Create your enums. They MUST end with 'count'. 22 | enum class state { walk, run, jump, count }; 23 | enum class transition { do_walk, do_run, do_jump, count }; 24 | 25 | // Used for callbacks 26 | using machine_t = fea::fsm; 27 | 28 | // Create your state machine. 29 | fea::fsm_builder builder; 30 | auto machine = builder.make_machine(); 31 | 32 | // Create your states. 33 | // Walk 34 | { 35 | auto walk_state = builder.make_state(); 36 | 37 | // Add allowed transitions. 38 | walk_state.add_transition(); 39 | 40 | // Add state events. 41 | walk_state.add_event( 42 | [](test_data& t, machine_t& /*machine*/) { 43 | t.walk_enter = true; 44 | ++t.num_onenter_calls; 45 | }); 46 | walk_state.add_event( 47 | [](test_data& t, machine_t& /*machine*/) { 48 | t.walk_update = true; 49 | ++t.num_onupdate_calls; 50 | }); 51 | 52 | machine.add_state(std::move(walk_state)); 53 | } 54 | 55 | // Run 56 | { 57 | auto run_state = builder.make_state(); 58 | run_state.add_transition(); 59 | run_state.add_transition(); 60 | run_state.add_event( 61 | [](test_data& t, machine_t& machine) { 62 | ++t.num_onenterfrom_calls; 63 | // This is OK. 64 | machine.trigger(t); 65 | }); 66 | run_state.add_event( 67 | [](test_data& t, machine_t&) { ++t.num_onupdate_calls; }); 68 | run_state.add_event( 69 | [](test_data& t, machine_t& machine) { 70 | ++t.num_onexit_calls; 71 | 72 | // This is also OK, though probably not recommended from a 73 | // "design" standpoint. 74 | machine.trigger(t); 75 | }); 76 | machine.add_state(std::move(run_state)); 77 | } 78 | 79 | // Jump 80 | { 81 | auto jump_state = builder.make_state(); 82 | jump_state.add_transition(); 83 | jump_state.add_transition(); 84 | 85 | jump_state.add_event( 86 | [](test_data& t, machine_t&) { ++t.num_onenterfrom_calls; }); 87 | 88 | jump_state.add_event( 89 | [](test_data& t, machine_t&) { ++t.num_onexitto_calls; }); 90 | 91 | machine.add_state(std::move(jump_state)); 92 | } 93 | 94 | 95 | // Init and update default state (walk). 96 | machine.update(mtest_data); 97 | EXPECT_TRUE(mtest_data.walk_enter); 98 | EXPECT_TRUE(mtest_data.walk_update); 99 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 100 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 101 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 102 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 103 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 104 | 105 | // Currently doesn't handle walk to jump transition. 106 | #if !defined(NDEBUG) 107 | EXPECT_DEATH(machine.trigger(mtest_data), ""); 108 | #endif 109 | 110 | // Go to jump. 111 | machine.state() 112 | .add_transition(); 113 | EXPECT_NO_THROW(machine.trigger(mtest_data)); 114 | 115 | // Nothing should have changed. 116 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 117 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 118 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 119 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 120 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 121 | 122 | // Go back to walk. 123 | machine.trigger(mtest_data); 124 | 125 | // Should get on exit to walk + on enter walk. 126 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 127 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 128 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 129 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 130 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 131 | 132 | // Update walk. 133 | machine.update(mtest_data); 134 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 135 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 136 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 137 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 138 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 139 | 140 | // Test retrigger in on_enter and in on_exit. 141 | machine.trigger(mtest_data); 142 | // run on_enter_from -> run on_exit -> jump on_enter_from 143 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); 144 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 145 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 146 | EXPECT_EQ(mtest_data.num_onexit_calls, 1u); 147 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 148 | 149 | // Does nothing, no jump update. 150 | machine.update(mtest_data); 151 | machine.update(mtest_data); 152 | machine.update(mtest_data); 153 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); 154 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 155 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 156 | EXPECT_EQ(mtest_data.num_onexit_calls, 1u); 157 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 158 | 159 | // And back to walk. 160 | machine.trigger(mtest_data); 161 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); 162 | EXPECT_EQ(mtest_data.num_onenter_calls, 3u); 163 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 164 | EXPECT_EQ(mtest_data.num_onexit_calls, 1u); 165 | EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); 166 | } 167 | 168 | TEST(fsm_nothrow, basics) { 169 | enum class state { 170 | walk, 171 | run, 172 | jump, 173 | count, 174 | }; 175 | 176 | enum class transition { 177 | do_walk, 178 | do_run, 179 | do_jump, 180 | count, 181 | }; 182 | 183 | size_t on_enters = 0; 184 | size_t on_updates = 0; 185 | size_t on_exits = 0; 186 | bool inpute = false; 187 | 188 | fea::fsm machine; 189 | 190 | fea::fsm_state walk_state; 191 | walk_state.add_event([&](bool& b, auto&) { 192 | b = true; 193 | ++on_enters; 194 | }); 195 | walk_state.add_event([&](bool& b, auto&) { 196 | b = true; 197 | ++on_updates; 198 | machine.trigger(b); 199 | }); 200 | walk_state.add_event([&](bool& b, auto&) { 201 | b = true; 202 | ++on_exits; 203 | }); 204 | walk_state.add_transition(); 205 | machine.add_state(std::move(walk_state)); 206 | 207 | fea::fsm_state run_state; 208 | run_state.add_event([&](bool& b, auto&) { 209 | b = true; 210 | ++on_enters; 211 | }); 212 | run_state.add_event([&](bool& b, auto&) { 213 | b = true; 214 | ++on_updates; 215 | machine.trigger(b); 216 | }); 217 | run_state.add_event([&](bool& b, auto&) { 218 | b = true; 219 | ++on_exits; 220 | }); 221 | run_state.add_transition(); 222 | machine.add_state(std::move(run_state)); 223 | 224 | fea::fsm_state jump_state; 225 | jump_state.add_event([&](bool& b, auto&) { 226 | b = true; 227 | ++on_enters; 228 | }); 229 | jump_state.add_event([&](bool& b, auto&) { 230 | b = true; 231 | ++on_updates; 232 | machine.trigger(b); 233 | }); 234 | jump_state.add_event([&](bool& b, auto&) { 235 | b = true; 236 | ++on_exits; 237 | }); 238 | jump_state.add_transition(); 239 | machine.add_state(std::move(jump_state)); 240 | 241 | machine.update(inpute); 242 | machine.update(inpute); 243 | machine.update(inpute); 244 | 245 | EXPECT_TRUE(inpute); 246 | EXPECT_EQ(on_enters, 4u); 247 | EXPECT_EQ(on_updates, 3u); 248 | EXPECT_EQ(on_exits, 3u); 249 | } 250 | 251 | TEST(fsm_nothrow, event_triggering) { 252 | struct test_data { 253 | size_t num_onenterfrom_calls = 0; 254 | size_t num_onenter_calls = 0; 255 | size_t num_onupdate_calls = 0; 256 | size_t num_onexit_calls = 0; 257 | size_t num_onexitto_calls = 0; 258 | }; 259 | test_data mtest_data; 260 | 261 | // Create your enums. They MUST end with 'count'. 262 | enum class state { walk, run, jump, count }; 263 | enum class transition { do_walk, do_run, do_jump, count }; 264 | 265 | // Used for callbacks 266 | using machine_t = fea::fsm; 267 | 268 | // Create your state machine. 269 | fea::fsm machine; 270 | 271 | // Create your states. 272 | // Walk 273 | { 274 | fea::fsm_state walk_state; 275 | 276 | // Add allowed transitions. 277 | walk_state.add_transition(); 278 | walk_state.add_transition(); 279 | 280 | // Add state events. 281 | walk_state.add_event( 282 | [](test_data& t, machine_t& machine) { 283 | ++t.num_onenter_calls; 284 | machine.trigger(t); 285 | }); 286 | walk_state.add_event( 287 | [](test_data& t, machine_t&) { 288 | ++t.num_onenterfrom_calls; 289 | // Should finish here. 290 | // machine.trigger(t); 291 | }); 292 | walk_state.add_event( 293 | [](test_data& t, machine_t& machine) { 294 | ++t.num_onexitto_calls; 295 | machine.trigger(t); 296 | }); 297 | machine.add_state(std::move(walk_state)); 298 | } 299 | 300 | // Run 301 | { 302 | fea::fsm_state run_state; 303 | run_state.add_transition(); 304 | run_state.add_transition(); 305 | run_state.add_event( 306 | [](test_data& t, machine_t& machine) { 307 | ++t.num_onenterfrom_calls; 308 | machine.trigger(t); 309 | }); 310 | run_state.add_event( 311 | [](test_data& t, machine_t& machine) { 312 | ++t.num_onexitto_calls; 313 | machine.trigger(t); 314 | }); 315 | machine.add_state(std::move(run_state)); 316 | } 317 | 318 | // Jump 319 | { 320 | fea::fsm_state jump_state; 321 | jump_state.add_transition(); 322 | jump_state.add_transition(); 323 | 324 | jump_state.add_event( 325 | [](test_data& t, machine_t& m) { 326 | ++t.num_onenterfrom_calls; 327 | m.trigger(t); 328 | }); 329 | 330 | jump_state.add_event( 331 | [](test_data& t, machine_t& m) { 332 | ++t.num_onexitto_calls; 333 | m.trigger(t); 334 | }); 335 | 336 | machine.add_state(std::move(jump_state)); 337 | } 338 | 339 | machine.update(mtest_data); 340 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 3u); 341 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 342 | EXPECT_EQ(mtest_data.num_onupdate_calls, 0u); 343 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 344 | EXPECT_EQ(mtest_data.num_onexitto_calls, 3u); 345 | } 346 | } // namespace 347 | -------------------------------------------------------------------------------- /tests/fsm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | 5 | namespace { 6 | 7 | TEST(fsm, example) { 8 | struct test_data { 9 | bool walk_enter = false; 10 | bool walk_update = false; 11 | 12 | size_t num_onenterfrom_calls = 0; 13 | size_t num_onenter_calls = 0; 14 | size_t num_onupdate_calls = 0; 15 | size_t num_onexit_calls = 0; 16 | size_t num_onexitto_calls = 0; 17 | }; 18 | test_data mtest_data; 19 | 20 | // Create your enums. They MUST end with 'count'. 21 | enum class state { walk, run, jump, count }; 22 | enum class transition { do_walk, do_run, do_jump, count }; 23 | 24 | // Used for callbacks 25 | using machine_t = fea::fsm; 26 | 27 | // Create your state machine. 28 | fea::fsm_builder builder; 29 | auto machine = builder.make_machine(); 30 | 31 | // Create your states. 32 | // Walk 33 | { 34 | auto walk_state = builder.make_state(); 35 | 36 | // Add allowed transitions. 37 | walk_state.add_transition(); 38 | 39 | // Add state events. 40 | walk_state.add_event( 41 | [](test_data& t, machine_t& /*machine*/) { 42 | t.walk_enter = true; 43 | ++t.num_onenter_calls; 44 | }); 45 | walk_state.add_event( 46 | [](test_data& t, machine_t& /*machine*/) { 47 | t.walk_update = true; 48 | ++t.num_onupdate_calls; 49 | }); 50 | 51 | machine.add_state(std::move(walk_state)); 52 | } 53 | 54 | // Run 55 | { 56 | auto run_state = builder.make_state(); 57 | run_state.add_transition(); 58 | run_state.add_transition(); 59 | run_state.add_event( 60 | [](test_data& t, machine_t& machine) { 61 | ++t.num_onenterfrom_calls; 62 | // This is OK. 63 | machine.trigger(t); 64 | }); 65 | run_state.add_event( 66 | [](test_data& t, machine_t&) { ++t.num_onupdate_calls; }); 67 | run_state.add_event( 68 | [](test_data& t, machine_t& machine) { 69 | ++t.num_onexit_calls; 70 | 71 | // This is also OK, though probably not recommended from a 72 | // "design" standpoint. 73 | machine.trigger(t); 74 | }); 75 | machine.add_state(std::move(run_state)); 76 | } 77 | 78 | // Jump 79 | { 80 | auto jump_state = builder.make_state(); 81 | jump_state.add_transition(); 82 | jump_state.add_transition(); 83 | 84 | jump_state.add_event( 85 | [](test_data& t, machine_t&) { ++t.num_onenterfrom_calls; }); 86 | 87 | jump_state.add_event( 88 | [](test_data& t, machine_t&) { ++t.num_onexitto_calls; }); 89 | 90 | machine.add_state(std::move(jump_state)); 91 | } 92 | 93 | 94 | // Init and update default state (walk). 95 | machine.update(mtest_data); 96 | EXPECT_TRUE(mtest_data.walk_enter); 97 | EXPECT_TRUE(mtest_data.walk_update); 98 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 99 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 100 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 101 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 102 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 103 | 104 | // Currently doesn't handle walk to jump transition. 105 | #if !defined(NDEBUG) 106 | EXPECT_DEATH(machine.trigger(mtest_data), ""); 107 | #else 108 | EXPECT_THROW(machine.trigger(mtest_data), 109 | std::invalid_argument); 110 | #endif 111 | 112 | // Go to jump. 113 | machine.state() 114 | .add_transition(); 115 | EXPECT_NO_THROW(machine.trigger(mtest_data)); 116 | 117 | // Nothing should have changed. 118 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 119 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 120 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 121 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 122 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 123 | 124 | // Go back to walk. 125 | machine.trigger(mtest_data); 126 | 127 | // Should get on exit to walk + on enter walk. 128 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 129 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 130 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 131 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 132 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 133 | 134 | // Update walk. 135 | machine.update(mtest_data); 136 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 137 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 138 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 139 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 140 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 141 | 142 | // Test retrigger in on_enter and in on_exit. 143 | machine.trigger(mtest_data); 144 | // run on_enter_from -> run on_exit -> jump on_enter_from 145 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); 146 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 147 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 148 | EXPECT_EQ(mtest_data.num_onexit_calls, 1u); 149 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 150 | 151 | // Does nothing, no jump update. 152 | machine.update(mtest_data); 153 | machine.update(mtest_data); 154 | machine.update(mtest_data); 155 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); 156 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 157 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 158 | EXPECT_EQ(mtest_data.num_onexit_calls, 1u); 159 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 160 | 161 | // And back to walk. 162 | machine.trigger(mtest_data); 163 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); 164 | EXPECT_EQ(mtest_data.num_onenter_calls, 3u); 165 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 166 | EXPECT_EQ(mtest_data.num_onexit_calls, 1u); 167 | EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); 168 | } 169 | 170 | TEST(fsm, basics) { 171 | enum class state { 172 | walk, 173 | run, 174 | jump, 175 | count, 176 | }; 177 | 178 | enum class transition { 179 | do_walk, 180 | do_run, 181 | do_jump, 182 | count, 183 | }; 184 | 185 | size_t on_enters = 0; 186 | size_t on_updates = 0; 187 | size_t on_exits = 0; 188 | bool inpute = false; 189 | 190 | fea::fsm machine; 191 | 192 | fea::fsm_state walk_state; 193 | walk_state.add_event([&](bool& b, auto&) { 194 | b = true; 195 | ++on_enters; 196 | }); 197 | walk_state.add_event([&](bool& b, auto&) { 198 | b = true; 199 | ++on_updates; 200 | machine.trigger(b); 201 | }); 202 | walk_state.add_event([&](bool& b, auto&) { 203 | b = true; 204 | ++on_exits; 205 | }); 206 | walk_state.add_transition(); 207 | machine.add_state(std::move(walk_state)); 208 | 209 | fea::fsm_state run_state; 210 | run_state.add_event([&](bool& b, auto&) { 211 | b = true; 212 | ++on_enters; 213 | }); 214 | run_state.add_event([&](bool& b, auto&) { 215 | b = true; 216 | ++on_updates; 217 | machine.trigger(b); 218 | }); 219 | run_state.add_event([&](bool& b, auto&) { 220 | b = true; 221 | ++on_exits; 222 | }); 223 | run_state.add_transition(); 224 | machine.add_state(std::move(run_state)); 225 | 226 | fea::fsm_state jump_state; 227 | jump_state.add_event([&](bool& b, auto&) { 228 | b = true; 229 | ++on_enters; 230 | }); 231 | jump_state.add_event([&](bool& b, auto&) { 232 | b = true; 233 | ++on_updates; 234 | machine.trigger(b); 235 | }); 236 | jump_state.add_event([&](bool& b, auto&) { 237 | b = true; 238 | ++on_exits; 239 | }); 240 | jump_state.add_transition(); 241 | machine.add_state(std::move(jump_state)); 242 | 243 | machine.update(inpute); 244 | machine.update(inpute); 245 | machine.update(inpute); 246 | 247 | EXPECT_TRUE(inpute); 248 | EXPECT_EQ(on_enters, 4u); 249 | EXPECT_EQ(on_updates, 3u); 250 | EXPECT_EQ(on_exits, 3u); 251 | } 252 | 253 | TEST(fsm, event_triggering) { 254 | struct test_data { 255 | size_t num_onenterfrom_calls = 0; 256 | size_t num_onenter_calls = 0; 257 | size_t num_onupdate_calls = 0; 258 | size_t num_onexit_calls = 0; 259 | size_t num_onexitto_calls = 0; 260 | }; 261 | test_data mtest_data; 262 | 263 | // Create your enums. They MUST end with 'count'. 264 | enum class state { walk, run, jump, count }; 265 | enum class transition { do_walk, do_run, do_jump, count }; 266 | 267 | // Used for callbacks 268 | using machine_t = fea::fsm; 269 | 270 | // Create your state machine. 271 | fea::fsm machine; 272 | 273 | // Create your states. 274 | // Walk 275 | { 276 | fea::fsm_state walk_state; 277 | 278 | // Add allowed transitions. 279 | walk_state.add_transition(); 280 | walk_state.add_transition(); 281 | 282 | // Add state events. 283 | walk_state.add_event( 284 | [](test_data& t, machine_t& machine) { 285 | ++t.num_onenter_calls; 286 | machine.trigger(t); 287 | }); 288 | walk_state.add_event( 289 | [](test_data& t, machine_t&) { 290 | ++t.num_onenterfrom_calls; 291 | // Should finish here. 292 | // machine.trigger(t); 293 | }); 294 | walk_state.add_event( 295 | [](test_data& t, machine_t& machine) { 296 | ++t.num_onexitto_calls; 297 | machine.trigger(t); 298 | }); 299 | machine.add_state(std::move(walk_state)); 300 | } 301 | 302 | // Run 303 | { 304 | fea::fsm_state run_state; 305 | run_state.add_transition(); 306 | run_state.add_transition(); 307 | run_state.add_event( 308 | [](test_data& t, machine_t& machine) { 309 | ++t.num_onenterfrom_calls; 310 | machine.trigger(t); 311 | }); 312 | run_state.add_event( 313 | [](test_data& t, machine_t& machine) { 314 | ++t.num_onexitto_calls; 315 | machine.trigger(t); 316 | }); 317 | machine.add_state(std::move(run_state)); 318 | } 319 | 320 | // Jump 321 | { 322 | fea::fsm_state jump_state; 323 | jump_state.add_transition(); 324 | jump_state.add_transition(); 325 | 326 | jump_state.add_event( 327 | [](test_data& t, machine_t& m) { 328 | ++t.num_onenterfrom_calls; 329 | m.trigger(t); 330 | }); 331 | 332 | jump_state.add_event( 333 | [](test_data& t, machine_t& m) { 334 | ++t.num_onexitto_calls; 335 | m.trigger(t); 336 | }); 337 | 338 | machine.add_state(std::move(jump_state)); 339 | } 340 | 341 | machine.update(mtest_data); 342 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 3u); 343 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 344 | EXPECT_EQ(mtest_data.num_onupdate_calls, 0u); 345 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 346 | EXPECT_EQ(mtest_data.num_onexitto_calls, 3u); 347 | } 348 | } // namespace 349 | -------------------------------------------------------------------------------- /tests/constexpr_fsm_win.cpp: -------------------------------------------------------------------------------- 1 | #if defined(_MSC_VER) 2 | 3 | #include 4 | #define FEA_FSM_NO_EVENT_WRAPPER 5 | #include 6 | #include 7 | 8 | namespace fea { 9 | using namespace cexpr; 10 | } 11 | 12 | namespace { 13 | 14 | TEST(constexpr_fsm, example_windows) { 15 | struct test_data { 16 | size_t num_onenterfrom_calls = 0; 17 | size_t num_onenter_calls = 0; 18 | size_t num_onupdate_calls = 0; 19 | size_t num_onexit_calls = 0; 20 | size_t num_onexitto_calls = 0; 21 | }; 22 | test_data mtest_data; 23 | 24 | // Create your enums. They MUST end with 'count'. 25 | enum class state { walk, run, jump, count }; 26 | enum class transition { do_walk, do_run, do_jump, count }; 27 | 28 | // Used for callbacks 29 | 30 | 31 | // Create your states. 32 | fea::fsm_builder builder; 33 | 34 | // Walk 35 | auto walk_transitions 36 | = builder.make_transition() 37 | .make_transition() 38 | .make_transition(); 39 | 40 | // fea_event(walk_onenter, [](auto&, test_data& t) { ++t.num_onenter_calls; 41 | // }); 42 | 43 | auto walk_events 44 | = builder.make_event( 45 | [](auto&, test_data& t) { ++t.num_onenter_calls; }) 46 | .make_event( 47 | [](auto&, test_data& t) { 48 | ++t.num_onupdate_calls; 49 | }) 50 | .make_event( 51 | [](auto&, test_data& t) { 52 | ++t.num_onexitto_calls; 53 | }); 54 | 55 | auto walk_state 56 | = builder.make_state(walk_transitions, walk_events); 57 | 58 | 59 | // Jump 60 | auto jump_transitions 61 | = builder.make_transition() 62 | .make_transition(); 63 | 64 | auto jump_events 65 | = builder.make_event( 66 | [](auto&, test_data& t) { 67 | // format 68 | ++t.num_onenter_calls; 69 | }) 70 | .make_event( 71 | [](auto&, test_data& t) { 72 | ++t.num_onexitto_calls; 73 | }); 74 | 75 | auto jump_state 76 | = builder.make_state(jump_transitions, jump_events); 77 | 78 | 79 | // Run 80 | auto run_transitions 81 | = builder.make_transition() 82 | .make_transition(); 83 | 84 | auto run_events 85 | = builder.make_event( 86 | [](auto&, test_data& t) { 87 | ++t.num_onenterfrom_calls; 88 | }) 89 | .make_event( 90 | [](auto&, test_data& t) { 91 | ++t.num_onupdate_calls; 92 | }) 93 | .make_event( 94 | [](auto&, test_data& t) { 95 | ++t.num_onexit_calls; 96 | }); 97 | 98 | auto run_state 99 | = builder.make_state(run_transitions, run_events); 100 | 101 | 102 | // Create your state machine. 103 | constexpr auto to_init 104 | = builder.make_machine(walk_state, jump_state, run_state); 105 | 106 | auto machine = to_init.init(mtest_data); 107 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 108 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 109 | EXPECT_EQ(mtest_data.num_onupdate_calls, 0u); 110 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 111 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 112 | 113 | machine.update(mtest_data); 114 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 115 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 116 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 117 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 118 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 119 | 120 | // Capture the trigger output. 121 | auto m1 = machine.template trigger(mtest_data); 122 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 123 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 124 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 125 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 126 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 127 | 128 | m1.update(mtest_data); 129 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 130 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 131 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 132 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 133 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 134 | 135 | auto m2 = m1.template trigger(mtest_data); 136 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 1u); 137 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 138 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 139 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 140 | EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); 141 | 142 | m2.update(mtest_data); 143 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 1u); 144 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 145 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 146 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 147 | EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); 148 | } 149 | 150 | 151 | TEST(constexpr_fsm, compiler_letter_windows) { 152 | #if defined(NDEBUG) 153 | static constexpr bool debug_build = false; 154 | #else 155 | static constexpr bool debug_build = true; 156 | #endif 157 | static constexpr bool assert_val = true; 158 | 159 | enum class transition { 160 | do_intro, 161 | do_debug, 162 | do_release, 163 | do_paragraph, 164 | do_outro, 165 | count, 166 | }; 167 | enum class state { 168 | intro, 169 | debug, 170 | release, 171 | paragraph, 172 | outro, 173 | count, 174 | }; 175 | 176 | fea::fsm_builder builder; 177 | 178 | 179 | // intro 180 | auto intro_transitions 181 | = builder.make_transition() 182 | .make_transition(); 184 | 185 | auto intro_events 186 | = builder.make_event([](auto& machine) { 187 | static_assert(assert_val, "Dear"); 188 | 189 | if constexpr (debug_build) { 190 | return machine 191 | .template trigger(); 192 | } else { 193 | return machine.template trigger< 194 | transition::do_release>(); 195 | } 196 | }) 197 | .make_event( 198 | [](auto&) { return 0; }) 199 | .make_event( 200 | [](auto&) { 201 | static_assert(assert_val, 202 | "slow Visual Studio Compiler,"); 203 | }) 204 | .make_event([](auto&) { 206 | static_assert(assert_val, 207 | "relatively fast Visual Studio Compiler ;)"); 208 | }); 209 | 210 | auto intro_state 211 | = builder.make_state(intro_transitions, intro_events); 212 | 213 | 214 | // debug 215 | auto debug_transitions = builder.make_transition(); 217 | 218 | auto debug_events 219 | = builder.make_event([](auto& m) { 220 | static_assert(assert_val, "In debug mode,"); 221 | return m.template trigger(); 222 | }) 223 | .make_event( 224 | [](auto&) { return 1; }); 225 | 226 | auto debug_state 227 | = builder.make_state(debug_transitions, debug_events); 228 | 229 | 230 | // release 231 | auto release_transitions = builder.make_transition(); 233 | 234 | auto release_events 235 | = builder.make_event([](auto& m) { 236 | static_assert(assert_val, "In release mode,"); 237 | return m.template trigger(); 238 | }) 239 | .make_event( 240 | [](auto&) { return 2; }); 241 | 242 | auto release_state = builder.make_state( 243 | release_transitions, release_events); 244 | 245 | 246 | // paragraph 247 | auto par_transitions 248 | = builder.make_transition(); 249 | 250 | auto par_events 251 | = builder.make_event([](auto& m) { 252 | static_assert(assert_val, 253 | "We've been very critical of you in the " 254 | "past."); 255 | return m.template trigger(); 256 | }) 257 | .make_event( 258 | [](auto&) { return 3; }) 259 | .make_event([](auto&) { 260 | static_assert(assert_val, "And we still are."); 261 | }); 262 | 263 | auto par_state 264 | = builder.make_state(par_transitions, par_events); 265 | 266 | 267 | // outro 268 | auto outro_events = builder.make_event([](auto&) { 269 | static_assert(assert_val, 270 | "But look how you've grown!"); 271 | }) 272 | .make_event( 273 | [](auto&) { return 42; }); 274 | 275 | auto outro_state 276 | = builder.make_state(builder.empty_t(), outro_events); 277 | 278 | // Tada! 279 | auto machine = builder.make_machine(intro_state, debug_state, release_state, 280 | par_state, outro_state) 281 | .init(); 282 | 283 | // And we can get constexpr values from update, generated conditionally at 284 | // compile time. 285 | constexpr auto result = machine.update(); 286 | static_assert(result == 42, "Wrong answer to life."); 287 | } 288 | 289 | template 290 | struct my_test1 { 291 | 292 | static constexpr auto my_val = Machine::update(); 293 | }; 294 | 295 | template 296 | struct my_test2 { 297 | 298 | static constexpr auto my_val = Machine::update(); 299 | }; 300 | 301 | TEST(constexpr_fsm, generate_tuple_windows) { 302 | enum class transition { flip_flop, count }; 303 | enum class state { gen_string, gen_int, count }; 304 | 305 | fea::fsm_builder builder; 306 | 307 | auto string_transitions 308 | = builder.make_transition(); 309 | 310 | auto string_events = builder.make_event( 311 | [](auto& m, auto val) { 312 | using tup_t = std::decay_t; 313 | if constexpr (std::tuple_size_v >= 10) { 314 | return val(); 315 | } else { 316 | 317 | return m.template trigger([=]() { 318 | return std::tuple_cat( 319 | val(), std::make_tuple("a string")); 320 | }); 321 | } 322 | }); 323 | 324 | auto string_state = builder.make_state( 325 | string_transitions, string_events); 326 | 327 | 328 | auto int_transitions = builder.make_transition(); 330 | 331 | auto int_events = builder.make_event( 332 | [](auto& m, auto val) { 333 | return m.template trigger([=]() { 334 | return std::tuple_cat(val(), std::make_tuple(42)); 335 | }); 336 | }); 337 | 338 | auto int_state 339 | = builder.make_state(int_transitions, int_events); 340 | 341 | constexpr auto tup 342 | = builder.make_machine(string_state, int_state).init([]() { 343 | return std::make_tuple("prime"); 344 | }); 345 | 346 | // Print the tuple. 347 | // std::apply([](auto&... vals) { ((std::cout << vals << std::endl), ...); 348 | // }, tup); 349 | 350 | constexpr auto test_tup = std::make_tuple("prime", "a string", 42, 351 | "a string", 42, "a string", 42, "a string", 42, "a string", 42); 352 | 353 | using machine_tup_t = std::decay_t; 354 | using expected_tup_t = std::decay_t; 355 | 356 | static_assert(std::tuple_size_v< 357 | machine_tup_t> == std::tuple_size_v, 358 | "unit test failed : tuples aren't the same size"); 359 | 360 | static_assert(std::is_same_v, 361 | "unit test failed : tuples are different types"); 362 | } 363 | 364 | } // namespace 365 | #endif 366 | -------------------------------------------------------------------------------- /tests/constexpr_fsm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace fea { 6 | using namespace cexpr; 7 | } 8 | 9 | namespace { 10 | 11 | TEST(constexpr_fsm, example) { 12 | struct test_data { 13 | size_t num_onenterfrom_calls = 0; 14 | size_t num_onenter_calls = 0; 15 | size_t num_onupdate_calls = 0; 16 | size_t num_onexit_calls = 0; 17 | size_t num_onexitto_calls = 0; 18 | }; 19 | test_data mtest_data; 20 | 21 | // Create your enums. They MUST end with 'count'. 22 | enum class state { walk, run, jump, count }; 23 | enum class transition { do_walk, do_run, do_jump, count }; 24 | 25 | // Used for callbacks 26 | 27 | 28 | // Create your states. 29 | fea::fsm_builder builder; 30 | 31 | // Walk 32 | auto walk_transitions 33 | = builder.make_transition() 34 | .make_transition() 35 | .make_transition(); 36 | 37 | fea_event(walk_onenter, [](auto&, test_data& t) { ++t.num_onenter_calls; }); 38 | fea_event( 39 | walk_onupdate, [](auto&, test_data& t) { ++t.num_onupdate_calls; }); 40 | fea_event( 41 | walk_onexitto, [](auto&, test_data& t) { ++t.num_onexitto_calls; }); 42 | 43 | auto walk_events 44 | = builder.make_event(walk_onenter) 45 | .make_event(walk_onupdate) 46 | .make_event( 47 | walk_onexitto); 48 | 49 | auto walk_state 50 | = builder.make_state(walk_transitions, walk_events); 51 | 52 | 53 | // Jump 54 | auto jump_transitions 55 | = builder.make_transition() 56 | .make_transition(); 57 | 58 | fea_event(jump_onenter, [](auto&, test_data& t) { ++t.num_onenter_calls; }); 59 | fea_event( 60 | jump_onexitto, [](auto&, test_data& t) { ++t.num_onexitto_calls; }); 61 | 62 | auto jump_events 63 | = builder.make_event(jump_onenter) 64 | .make_event( 65 | jump_onexitto); 66 | 67 | auto jump_state 68 | = builder.make_state(jump_transitions, jump_events); 69 | 70 | 71 | // Run 72 | auto run_transitions 73 | = builder.make_transition() 74 | .make_transition(); 75 | 76 | fea_event(run_onenterfrom, 77 | [](auto&, test_data& t) { ++t.num_onenterfrom_calls; }); 78 | fea_event( 79 | run_onupdate, [](auto&, test_data& t) { ++t.num_onupdate_calls; }); 80 | fea_event(run_onexit, [](auto&, test_data& t) { ++t.num_onexit_calls; }); 81 | 82 | auto run_events 83 | = builder.make_event( 84 | run_onenterfrom) 85 | .make_event(run_onupdate) 86 | .make_event(run_onexit); 87 | 88 | auto run_state 89 | = builder.make_state(run_transitions, run_events); 90 | 91 | 92 | // Create your state machine. 93 | constexpr auto to_init 94 | = builder.make_machine(walk_state, jump_state, run_state); 95 | 96 | auto machine = to_init.init(mtest_data); 97 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 98 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 99 | EXPECT_EQ(mtest_data.num_onupdate_calls, 0u); 100 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 101 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 102 | 103 | machine.update(mtest_data); 104 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 105 | EXPECT_EQ(mtest_data.num_onenter_calls, 1u); 106 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 107 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 108 | EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); 109 | 110 | // Capture the trigger output. 111 | auto m1 = machine.template trigger(mtest_data); 112 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 113 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 114 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 115 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 116 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 117 | 118 | m1.update(mtest_data); 119 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); 120 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 121 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 122 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 123 | EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); 124 | 125 | auto m2 = m1.template trigger(mtest_data); 126 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 1u); 127 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 128 | EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); 129 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 130 | EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); 131 | 132 | m2.update(mtest_data); 133 | EXPECT_EQ(mtest_data.num_onenterfrom_calls, 1u); 134 | EXPECT_EQ(mtest_data.num_onenter_calls, 2u); 135 | EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); 136 | EXPECT_EQ(mtest_data.num_onexit_calls, 0u); 137 | EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); 138 | } 139 | 140 | 141 | TEST(constexpr_fsm, compiler_letter) { 142 | #if defined(NDEBUG) 143 | static constexpr bool debug_build = false; 144 | #else 145 | static constexpr bool debug_build = true; 146 | #endif 147 | static constexpr bool assert_val = true; 148 | 149 | enum class transition { 150 | do_intro, 151 | do_debug, 152 | do_release, 153 | do_paragraph, 154 | do_outro, 155 | count 156 | }; 157 | enum class state { intro, debug, release, paragraph, outro, count }; 158 | 159 | fea::fsm_builder builder; 160 | 161 | 162 | // intro 163 | auto intro_transitions 164 | = builder.make_transition() 165 | .make_transition(); 167 | 168 | fea_event(intro_onenter, [](auto& machine) { 169 | static_assert(assert_val, "Dear"); 170 | 171 | if constexpr (debug_build) { 172 | return machine.template trigger(); 173 | } else { 174 | return machine.template trigger(); 175 | } 176 | }); 177 | fea_event(intro_onupdate, [](auto&) { return 0; }); 178 | fea_event(intro_onexitto_debug, [](auto&) { 179 | static_assert(assert_val, "slow Visual Studio Compiler,"); 180 | }); 181 | fea_event(intro_onexitto_release, [](auto&) { 182 | static_assert(assert_val, "relatively fast Visual Studio Compiler ;)"); 183 | }); 184 | 185 | auto intro_events 186 | = builder.make_event(intro_onenter) 187 | .make_event(intro_onupdate) 188 | .make_event( 189 | intro_onexitto_debug) 190 | .make_event( 191 | intro_onexitto_release); 192 | 193 | auto intro_state 194 | = builder.make_state(intro_transitions, intro_events); 195 | 196 | 197 | // debug 198 | auto debug_transitions = builder.make_transition(); 200 | 201 | fea_event(debug_onenter, [](auto& m) { 202 | static_assert(assert_val, "In debug mode,"); 203 | return m.template trigger(); 204 | }); 205 | fea_event(debug_onupdate, [](auto&) { return 1; }); 206 | 207 | auto debug_events 208 | = builder.make_event(debug_onenter) 209 | .make_event(debug_onupdate); 210 | 211 | auto debug_state 212 | = builder.make_state(debug_transitions, debug_events); 213 | 214 | 215 | // release 216 | auto release_transitions = builder.make_transition(); 218 | 219 | fea_event(release_onenter, [](auto& m) { 220 | static_assert(assert_val, "In release mode,"); 221 | return m.template trigger(); 222 | }); 223 | fea_event(release_onupdate, [](auto&) { return 2; }); 224 | 225 | auto release_events 226 | = builder.make_event(release_onenter) 227 | .make_event(release_onupdate); 228 | 229 | auto release_state = builder.make_state( 230 | release_transitions, release_events); 231 | 232 | 233 | // paragraph 234 | auto par_transitions 235 | = builder.make_transition(); 236 | 237 | fea_event(par_onenter, [](auto& m) { 238 | static_assert(assert_val, 239 | "We've been very critical of you in the " 240 | "past."); 241 | return m.template trigger(); 242 | }); 243 | fea_event(par_onupdate, [](auto&) { return 3; }); 244 | fea_event(par_onexit, 245 | [](auto&) { static_assert(assert_val, "And we still are."); }); 246 | 247 | auto par_events 248 | = builder.make_event(par_onenter) 249 | .make_event(par_onupdate) 250 | .make_event(par_onexit); 251 | 252 | auto par_state 253 | = builder.make_state(par_transitions, par_events); 254 | 255 | 256 | // outro 257 | fea_event(outro_onenter, [](auto&) { 258 | static_assert(assert_val, "But look how you've grown!"); 259 | }); 260 | fea_event(outro_onupdate, [](auto&) { return 42; }); 261 | 262 | auto outro_events 263 | = builder.make_event(outro_onenter) 264 | .make_event(outro_onupdate); 265 | 266 | auto outro_state 267 | = builder.make_state(builder.empty_t(), outro_events); 268 | 269 | // Tada! 270 | auto machine = builder.make_machine(intro_state, debug_state, release_state, 271 | par_state, outro_state) 272 | .init(); 273 | 274 | // And we can get constexpr values from update, generated conditionally at 275 | // compile time. 276 | constexpr auto result = machine.update(); 277 | static_assert(result == 42, "Wrong answer to life."); 278 | } 279 | 280 | template 281 | struct my_test1 { 282 | 283 | static constexpr auto my_val = Machine::update(); 284 | }; 285 | 286 | template 287 | struct my_test2 { 288 | 289 | static constexpr auto my_val = Machine::update(); 290 | }; 291 | 292 | TEST(constexpr_fsm, generate_tuple) { 293 | enum class transition { flip_flop, count }; 294 | enum class state { gen_string, gen_int, count }; 295 | 296 | fea::fsm_builder builder; 297 | 298 | auto string_transitions 299 | = builder.make_transition(); 300 | 301 | fea_event(string_enter, [](auto& m, auto val) { 302 | using tup_t = std::decay_t; 303 | if constexpr (std::tuple_size_v >= 10) { 304 | return val(); 305 | } else { 306 | 307 | return m.template trigger([=]() { 308 | return std::tuple_cat(val(), std::make_tuple("a string")); 309 | }); 310 | } 311 | }); 312 | 313 | auto string_events 314 | = builder.make_event(string_enter); 315 | 316 | auto string_state = builder.make_state( 317 | string_transitions, string_events); 318 | 319 | 320 | auto int_transitions = builder.make_transition(); 322 | 323 | fea_event(int_enter, [](auto& m, auto val) { 324 | return m.template trigger( 325 | [=]() { return std::tuple_cat(val(), std::make_tuple(42)); }); 326 | }); 327 | 328 | auto int_events = builder.make_event(int_enter); 329 | 330 | auto int_state 331 | = builder.make_state(int_transitions, int_events); 332 | 333 | constexpr auto tup 334 | = builder.make_machine(string_state, int_state).init([]() { 335 | return std::make_tuple("prime"); 336 | }); 337 | 338 | // Print the tuple 339 | // std::apply([](auto&... vals) { ((std::cout << vals << std::endl), ...); 340 | // }, tup); 341 | 342 | constexpr auto test_tup = std::make_tuple("prime", "a string", 42, 343 | "a string", 42, "a string", 42, "a string", 42, "a string", 42); 344 | 345 | using machine_tup_t = std::decay_t; 346 | using expected_tup_t = std::decay_t; 347 | 348 | static_assert(std::tuple_size_v< 349 | machine_tup_t> == std::tuple_size_v, 350 | "unit test failed : tuples are different sizes"); 351 | 352 | static_assert(std::is_same_v, 353 | "unit test failed : tuples are different types"); 354 | } 355 | } // namespace 356 | -------------------------------------------------------------------------------- /include/fea_state_machines/fsm.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2020, Philippe Groarke 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | #pragma once 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | #if !defined(FEA_FSM_NOTHROW) 41 | #include 42 | #endif 43 | 44 | namespace fea { 45 | /* 46 | A small, fast and simple stack based fsm. 47 | Not as small as inlined simple fsm, but easier to work with and debug. 48 | 49 | Features : 50 | - OnEnter, OnUpdate, OnExit. 51 | - OnEnterFrom, OnExitTo. 52 | Overrides event behavior when coming from/going to specified states or 53 | transitions. 54 | - Supports user arguments in the callbacks (explained below). 55 | //- DelayedTrigger. // Removed till bug fix 56 | // Trigger will happen next time you call fsm::update. 57 | - Define FEA_FSM_NOTHROW to assert instead of throw. 58 | - Does NOT provide a "get_current_state" function. 59 | Checking the current state of an fsm is a major smell and usually points 60 | to either a misuse, misunderstanding or incomplete implementation of the 61 | fsm. Do not do that, rethink your states and transitions instead. 62 | 63 | Callbacks : 64 | - The last argument of your callback is always a ref to the fsm itself. 65 | This is useful for retriggering and when you store fsms in containers. 66 | You can use auto& to simplify your callback signature. 67 | [](your_args..., auto& mymachine){} 68 | 69 | - Pass your event signature at the end of the fsm and fsm_state template. 70 | These will be passed on to your callbacks when you call update or 71 | trigger. 72 | For example : 73 | fsm; 74 | Callback signature is: 75 | [](int, bool&, const void*, auto& machine){} 76 | - The state machine is passed at the end to allow you to call member 77 | functions directly. 78 | For example : 79 | fsm 80 | Call your triggers passing the object pointer as the first argument, and 81 | the member functions will be called. 82 | machine.trigger(&obj, 42); 83 | 84 | 85 | Notes : 86 | - Uses std::function. If you can't have that, use inlined fsms instead. 87 | - Throws on unhandled transition. 88 | You must explicitly add re-entrant transitions or ignored transitions 89 | (by providing empty callbacks). IMHO this is one of the bigest source of 90 | bugs and broken behavior when working with FSMs. Throwing makes 91 | debugging much faster and easier. 92 | 93 | TODO : 94 | - Yield/History state 95 | - Anonymous transition (1 runtime transition per state). 96 | - Auto transition guards? 97 | - Guard transitions maybe? 98 | */ 99 | 100 | enum class fsm_event : uint8_t { 101 | on_enter_from, 102 | on_enter, 103 | on_update, 104 | on_exit, 105 | on_exit_to, 106 | count, 107 | }; 108 | 109 | template 110 | struct fsm; 111 | 112 | template 113 | struct fsm_state; 114 | 115 | template 117 | struct fsm_state { 118 | using fsm_t = fsm; 119 | using fsm_func_t = std::function; 120 | 121 | fsm_state() { 122 | std::fill(_transitions.begin(), _transitions.end(), StateEnum::count); 123 | } 124 | 125 | // Add your event implementation. 126 | template 127 | void add_event(fsm_func_t&& func) { 128 | static_assert(Event == fsm_event::on_enter 129 | || Event == fsm_event::on_exit 130 | || Event == fsm_event::on_update, 131 | "add_event : wrong template resolution called"); 132 | 133 | if constexpr (Event == fsm_event::on_enter) { 134 | _on_enter_func = std::move(func); 135 | } else if constexpr (Event == fsm_event::on_update) { 136 | _on_update_func = std::move(func); 137 | } else if constexpr (Event == fsm_event::on_exit) { 138 | _on_exit_func = std::move(func); 139 | } 140 | } 141 | 142 | template 143 | void add_event(fsm_func_t&& func) { 144 | static_assert(Event == fsm_event::on_enter_from 145 | || Event == fsm_event::on_exit_to, 146 | "add_event : must use on_enter_from or on_exit_to when " 147 | "custumizing on transition"); 148 | 149 | if constexpr (Event == fsm_event::on_enter_from) { 150 | std::get(_on_enter_from_state_funcs) 151 | = std::move(func); 152 | } else if constexpr (Event == fsm_event::on_exit_to) { 153 | std::get(_on_exit_to_state_funcs) = std::move(func); 154 | } 155 | } 156 | 157 | template 158 | void add_event(fsm_func_t&& func) { 159 | static_assert(Event == fsm_event::on_enter_from 160 | || Event == fsm_event::on_exit_to, 161 | "add_event : must use on_enter_from or on_exit_to when " 162 | "custumizing on transition"); 163 | 164 | if constexpr (Event == fsm_event::on_enter_from) { 165 | std::get(_on_enter_from_transition_funcs) 166 | = std::move(func); 167 | } else if constexpr (Event == fsm_event::on_exit_to) { 168 | std::get(_on_exit_to_transition_funcs) 169 | = std::move(func); 170 | } 171 | } 172 | 173 | // Handle transition to a specified state. 174 | template 175 | void add_transition() { 176 | static_assert(Transition != TransitionEnum::count, 177 | "fsm_state : bad transition"); 178 | static_assert(State != StateEnum::count, "fsm_state : bad state"); 179 | std::get(_transitions) = State; 180 | } 181 | 182 | // Used internally to get which state is associated to the provided 183 | // transition. 184 | template 185 | StateEnum transition_target() const { 186 | assert(std::get(_transitions) != StateEnum::count 187 | && "fsm_state : unhandled transition"); 188 | 189 | #if !defined(FEA_FSM_NOTHROW) 190 | if (std::get(_transitions) == StateEnum::count) { 191 | throw std::invalid_argument{ "fsm_state : unhandled transition" }; 192 | } 193 | #endif 194 | 195 | return std::get(_transitions); 196 | } 197 | 198 | // Used internally, executes a specific event. 199 | template 200 | auto execute_event([[maybe_unused]] StateEnum to_from_state, 201 | [[maybe_unused]] TransitionEnum to_from_transition, fsm_t& machine, 202 | FuncArgs... func_args) { 203 | static_assert(Event != fsm_event::on_enter_from, 204 | "state : do not execute on_enter_from, use on_enter instead " 205 | "and provide to_from_state"); 206 | static_assert(Event != fsm_event::on_exit_to, 207 | "state : do not execute on_exit_to, use on_exit instead and " 208 | "provide to_from_state"); 209 | 210 | static_assert(Event != fsm_event::count, "fsm_state : invalid event"); 211 | 212 | 213 | // Check the event, call the appropriate user functions if it is stored. 214 | if constexpr (Event == fsm_event::on_enter) { 215 | if (to_from_state != StateEnum::count 216 | && _on_enter_from_state_funcs[size_t(to_from_state)]) { 217 | // has enter_from state 218 | std::invoke(_on_enter_from_state_funcs[size_t(to_from_state)], 219 | func_args..., machine); 220 | 221 | } else if (to_from_transition != TransitionEnum::count 222 | && _on_enter_from_transition_funcs[size_t( 223 | to_from_transition)]) { 224 | // has enter_from transition 225 | std::invoke(_on_enter_from_transition_funcs[size_t( 226 | to_from_transition)], 227 | func_args..., machine); 228 | 229 | } else if (_on_enter_func) { 230 | std::invoke(_on_enter_func, func_args..., machine); 231 | } 232 | 233 | } else if constexpr (Event == fsm_event::on_update) { 234 | if (_on_update_func) { 235 | return std::invoke(_on_update_func, func_args..., machine); 236 | } 237 | } else if constexpr (Event == fsm_event::on_exit) { 238 | if (to_from_state != StateEnum::count 239 | && _on_exit_to_state_funcs[size_t(to_from_state)]) { 240 | // has exit_to 241 | std::invoke(_on_exit_to_state_funcs[size_t(to_from_state)], 242 | func_args..., machine); 243 | 244 | } else if (to_from_transition != TransitionEnum::count 245 | && _on_exit_to_transition_funcs[size_t( 246 | to_from_transition)]) { 247 | // has exit_to 248 | std::invoke(_on_exit_to_transition_funcs[size_t( 249 | to_from_transition)], 250 | func_args..., machine); 251 | 252 | } else if (_on_exit_func) { 253 | std::invoke(_on_exit_func, func_args..., machine); 254 | } 255 | } 256 | } 257 | 258 | private: 259 | std::array _transitions; 260 | fsm_func_t _on_enter_func; 261 | fsm_func_t _on_update_func; 262 | fsm_func_t _on_exit_func; 263 | 264 | std::array _on_enter_from_state_funcs; 265 | std::array _on_exit_to_state_funcs; 266 | 267 | std::array 268 | _on_enter_from_transition_funcs; 269 | std::array 270 | _on_exit_to_transition_funcs; 271 | 272 | // TBD, makes it heavy but helps debuggability 273 | // const char* _name; 274 | }; 275 | 276 | template 278 | struct fsm { 279 | using state_t = fsm_state; 280 | using fsm_func_t = typename state_t::fsm_func_t; 281 | 282 | // Here, we use move semantics not for performance (it doesn't do anything). 283 | // It is to make it clear to the user he cannot modify the state anymore. 284 | // The fsm gobbles the state. 285 | template 286 | void add_state(state_t&& state) { 287 | static_assert(State != StateEnum::count, "fsm : bad state"); 288 | 289 | std::get(_states) = std::move(state); 290 | _state_valid[size_t(State)] = true; 291 | 292 | if (_default_state == StateEnum::count) { 293 | _default_state = State; 294 | } 295 | } 296 | 297 | // Set starting state. 298 | // By default, the first added state is used. 299 | template 300 | void set_start_state() { 301 | static_assert(State != StateEnum::count, "fsm : bad state"); 302 | _default_state = State; 303 | } 304 | 305 | template 306 | void set_finish_state() { 307 | static_assert(State != StateEnum::count, "fsm : bad state"); 308 | _finish_state = State; 309 | } 310 | 311 | bool finished() const { 312 | if (_finish_state != StateEnum::count) { 313 | return _finish_state == _current_state; 314 | } 315 | return false; 316 | } 317 | 318 | void reset() { 319 | _current_state = StateEnum::count; 320 | } 321 | 322 | // TODO : Fix retrigger on_exit. 323 | //// First come first served. 324 | //// Trigger will be called next update(...). 325 | //// Calling this prevents subsequent triggers to be executed. 326 | //// Allows more relaxed trigger argument requirements. 327 | // template 328 | // void delayed_trigger() { 329 | // if (_has_delayed_trigger) 330 | // return; 331 | 332 | // _has_delayed_trigger = true; 333 | // _delayed_trigger_func = [](fsm& f, FuncArgs... func_args) { 334 | // f._has_delayed_trigger = false; 335 | // f.trigger(func_args...); 336 | // }; 337 | //} 338 | 339 | // Trigger a transition. 340 | // Throws on bad transition (or asserts, if you defined FEA_FSM_NOTHROW). 341 | // If you had previously called delayed_trigger, this 342 | // won't do anything. 343 | template 344 | void trigger(FuncArgs... func_args) { 345 | if (_has_delayed_trigger) 346 | return; 347 | 348 | maybe_init(func_args...); 349 | 350 | StateEnum from_state_e = _current_state; 351 | state_t& from_state = get_state(_current_state); 352 | 353 | StateEnum to_state_e 354 | = from_state.template transition_target(); 355 | state_t& to_state = get_state(to_state_e); 356 | 357 | // Only execute on_exit if we aren't in a trigger from on_exit. 358 | if (!_in_on_exit) { 359 | _in_on_exit = true; 360 | 361 | // Can recursively call trigger. We must handle that. 362 | from_state.template execute_event( 363 | to_state_e, Transition, *this, func_args...); 364 | 365 | if (_in_on_exit == false) { 366 | // Exit has triggered transition. Abort. 367 | return; 368 | } 369 | } 370 | _in_on_exit = false; 371 | 372 | _current_state = to_state_e; 373 | 374 | // Always execute on_enter. 375 | to_state.template execute_event( 376 | from_state_e, Transition, *this, func_args...); 377 | } 378 | 379 | // Update the fsm. 380 | // Calls on_update on the current state. 381 | // Processes delay_trigger if that was called. 382 | FuncRet update(FuncArgs... func_args) { 383 | while (_has_delayed_trigger) { 384 | std::invoke(_delayed_trigger_func, func_args..., *this); 385 | } 386 | 387 | maybe_init(func_args...); 388 | 389 | return get_state(_current_state) 390 | .template execute_event(StateEnum::count, 391 | TransitionEnum::count, *this, func_args...); 392 | } 393 | 394 | // Get the specified state. 395 | template 396 | const state_t& state() const { 397 | return std::get(_states); 398 | } 399 | template 400 | state_t& state() { 401 | return std::get(_states); 402 | } 403 | 404 | private: 405 | void maybe_init(FuncArgs... func_args) { 406 | if (_current_state != StateEnum::count) 407 | return; 408 | 409 | _current_state = _default_state; 410 | _states[size_t(_current_state)] 411 | .template execute_event(StateEnum::count, 412 | TransitionEnum::count, *this, func_args...); 413 | } 414 | 415 | const state_t& get_state(StateEnum s) const { 416 | assert(s != StateEnum::count && "fsm : Accessing invalid state."); 417 | #if !defined(FEA_FSM_NOTHROW) 418 | if (s == StateEnum::count) { 419 | throw std::runtime_error{ "fsm : Accessing invalid state." }; 420 | } 421 | #endif 422 | 423 | assert(_state_valid[size_t(s)] 424 | && "fsm : Accessing invalid state, did you forget to add a " 425 | "state?"); 426 | 427 | #if !defined(FEA_FSM_NOTHROW) 428 | if (!_state_valid[size_t(s)]) { 429 | throw std::runtime_error{ 430 | "fsm : Accessing invalid state, did you forget to add a state?" 431 | }; 432 | } 433 | #endif 434 | 435 | return _states[size_t(s)]; 436 | } 437 | state_t& get_state(StateEnum s) { 438 | return const_cast( 439 | static_cast(this)->get_state(s)); 440 | } 441 | 442 | std::array _states; 443 | std::bitset _state_valid; 444 | StateEnum _current_state = StateEnum::count; 445 | StateEnum _default_state = StateEnum::count; 446 | StateEnum _finish_state = StateEnum::count; 447 | 448 | bool _in_on_exit = false; 449 | 450 | fsm_func_t _delayed_trigger_func = {}; 451 | bool _has_delayed_trigger = false; 452 | }; 453 | 454 | template 455 | struct fsm_builder; 456 | 457 | template 459 | struct fsm_builder { 460 | static constexpr auto make_state() { 461 | return fsm_state{}; 462 | } 463 | 464 | static constexpr fsm 465 | make_machine() { 466 | return fsm{}; 467 | } 468 | }; 469 | } // namespace fea 470 | -------------------------------------------------------------------------------- /include/fea_state_machines/constexpr_fsm.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2020, Philippe Groarke 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | #pragma once 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | #if !defined(FEA_FSM_NOTHROW) 42 | #include 43 | #endif 44 | 45 | /* 46 | A compile-time executable very simple fsm. 47 | https://philippegroarke.com/posts/2020/constexpr_fsm/ 48 | 49 | Features : 50 | - OnEnter, OnUpdate, OnExit. 51 | - OnEnterFrom, OnExitTo. 52 | Overrides event behavior when coming from/going to specified states. 53 | - Supports user arguments in the callbacks, as the fsm stores your lambda 54 | directly. 55 | - static_asserts wherever possible. 56 | - Does NOT provide a "get_current_state" function. 57 | Checking the current state of an fsm is a major smell and usually points 58 | to either a misuse, misunderstanding or incomplete implementation of the 59 | fsm. Do not do that, rethink your states and transitions instead. 60 | 61 | Callbacks : 62 | - The first argument of your callback is always a ref to the fsm itself. 63 | This is useful for retriggering and when you store fsms in containers. 64 | You can use auto& to simplify your callback signature. 65 | [](auto& mymachine){} 66 | 67 | - Pass your own types at the end of the fsm and fsm_state template. 68 | These will be passed on to your callbacks when you call update or 69 | trigger. 70 | For example : fsm; 71 | Callback signature is: 72 | [](auto& machine, int, bool&, const void*){} 73 | 74 | 75 | Notes : 76 | - Uses tuple of Func types. 77 | - You *must* return the results of trigger. 78 | - You *must* capture the trigger results, this is your new state. 79 | 80 | TODO : 81 | - If this turns out to be useful, adding the following seems reasonable. 82 | - Transition guards. 83 | - Yield transitions (aka history state). 84 | */ 85 | 86 | #define FEA_TOKENPASTE(x, y) x##y 87 | #define FEA_TOKENPASTE2(x, y) FEA_TOKENPASTE(x, y) 88 | #define fea_event(name, f) \ 89 | struct FEA_TOKENPASTE2( \ 90 | FEA_TOKENPASTE2(fea_event_builder_, name), __LINE__) { \ 91 | using is_event_builder [[maybe_unused]] = int; \ 92 | static constexpr auto unpack() { \ 93 | return f; \ 94 | } \ 95 | } name 96 | 97 | 98 | namespace fea { 99 | namespace cexpr { 100 | namespace detail { 101 | template 102 | struct event_ { 103 | using type = void; 104 | }; 105 | 106 | template 107 | struct is_event_builder : public std::false_type {}; 108 | 109 | template 110 | struct is_event_builder::type> 111 | : public std::true_type {}; 112 | 113 | 114 | template 115 | struct tuple_idx { 116 | static_assert(!std::is_same_v>, 117 | "could not find T in given Tuple"); 118 | 119 | // static constexpr size_t value = std::numeric_limits::max(); 120 | }; 121 | template 122 | struct tuple_idx> { 123 | static constexpr size_t value = 0; 124 | }; 125 | template 126 | struct tuple_idx> { 127 | static constexpr size_t value 128 | = 1 + tuple_idx>::value; 129 | }; 130 | 131 | template 132 | inline constexpr size_t tuple_idx_v = tuple_idx::value; 133 | 134 | 135 | template 136 | struct tuple_contains; 137 | template 138 | struct tuple_contains> : std::false_type {}; 139 | template 140 | struct tuple_contains> 141 | : tuple_contains> {}; 142 | template 143 | struct tuple_contains> : std::true_type {}; 144 | 145 | template 146 | inline constexpr bool tuple_contains_v = tuple_contains::value; 147 | 148 | 149 | template 150 | constexpr auto tuple_expander5000_impl(Func func, std::index_sequence) { 151 | return func(std::integral_constant{}...); 152 | } 153 | template 154 | constexpr auto tuple_expander5000(Func func) { 155 | return tuple_expander5000_impl(func, std::make_index_sequence()); 156 | } 157 | 158 | 159 | template 160 | inline auto runtime_get(Func func, Tuple& tup, size_t idx) { 161 | if (N == idx) { 162 | return func(std::get(tup)); 163 | } 164 | 165 | if constexpr (N + 1 < std::tuple_size_v) { 166 | return runtime_get(func, tup, idx); 167 | } 168 | } 169 | 170 | 171 | template 172 | constexpr auto static_for(Func func, std::index_sequence) { 173 | return (func(std::integral_constant{}), ...); 174 | } 175 | 176 | template 177 | constexpr void static_for(Func func) { 178 | return static_for(func, std::make_index_sequence()); 179 | } 180 | 181 | 182 | // Pass in your tuple_map_key type. 183 | // Build with multiple tuple, T>... 184 | template 185 | struct tuple_map { 186 | // Search by non-type template. 187 | template 188 | static constexpr const auto& find() { 189 | constexpr size_t idx = tuple_idx_v; 190 | return std::get(_values); 191 | } 192 | // template 193 | // static constexpr auto& find() { 194 | // constexpr size_t idx = tuple_idx_v; 195 | // return std::get(_values); 196 | //} 197 | 198 | template 199 | static constexpr bool contains() { 200 | // Just to make sure we are in constexpr land. 201 | constexpr bool ret = tuple_contains_v; 202 | return ret; 203 | } 204 | 205 | private: 206 | // tuple> 207 | // Used to find the index of the value inside the other tuple. 208 | static constexpr auto _keys = KeysBuilder::unpack(); 209 | 210 | // tuple 211 | // Your values. 212 | static constexpr auto _values = ValuesBuilder::unpack(); 213 | 214 | using keys_tup_t = std::decay_t; 215 | using values_tup_t = std::decay_t; 216 | }; 217 | 218 | template 219 | constexpr auto make_tuple_map() { 220 | // At compile time, take the tuple of tuple coming from the 221 | // builder, iterate through it, grab the first elements of 222 | // nested tuples (the key) and put that in a new tuple, grab 223 | // the second elements and put that in another tuple. 224 | 225 | // Basically, go from tuple...> to 226 | // tuple, tuple> 227 | // which is our map basically. 228 | 229 | struct keys_tup { 230 | static constexpr auto unpack() { 231 | constexpr size_t tup_size 232 | = std::tuple_size_v; 233 | 234 | return detail::tuple_expander5000([]( 235 | auto... Idxes) constexpr { 236 | constexpr auto tup_of_tups = Builder::unpack(); 237 | 238 | // Gets all the keys. 239 | return std::make_tuple(std::get<0>( 240 | std::get(tup_of_tups))...); 241 | }); 242 | } 243 | }; 244 | 245 | struct vals_tup { 246 | static constexpr auto unpack() { 247 | constexpr size_t tup_size 248 | = std::tuple_size_v; 249 | 250 | return detail::tuple_expander5000([]( 251 | auto... Idxes) constexpr { 252 | constexpr auto tup_of_tups = Builder::unpack(); 253 | 254 | // Gets all the values. 255 | return std::make_tuple(std::get<1>( 256 | std::get(tup_of_tups))...); 257 | }); 258 | } 259 | }; 260 | 261 | return tuple_map{}; 262 | } 263 | 264 | } // namespace detail 265 | 266 | 267 | enum class fsm_event : uint8_t { 268 | on_enter_from, 269 | on_enter, 270 | on_update, 271 | on_exit, 272 | on_exit_to, 273 | count, 274 | }; 275 | 276 | // Fsm keys are used to lookup your transitions and your states. 277 | 278 | // Transition Key will query tuple map with Transition to get the target 279 | // state. 280 | template 281 | struct fsm_transition_key {}; 282 | 283 | // Event key will query the tuple map with Event and possibly a To/From 284 | // state to execute your event. 285 | template 286 | struct fsm_event_key {}; 287 | 288 | // State key is used to find states in the state machine itself. 289 | template 290 | struct fsm_state_key {}; 291 | 292 | template 294 | struct fsm_state { 295 | static constexpr StateEnum state_e = State; 296 | 297 | template 298 | static constexpr StateEnum transition_target() { 299 | return _transitions.template find< 300 | fsm_transition_key>(); 301 | } 302 | 303 | template 305 | static constexpr auto execute_event([[maybe_unused]] Machine& machine, 306 | [[maybe_unused]] FuncArgs&&... func_args) { 307 | static_assert(Event != fsm_event::on_enter_from, 308 | "state : do not execute on_enter_from, use on_enter instead " 309 | "and provide to_from_state"); 310 | static_assert(Event != fsm_event::on_exit_to, 311 | "state : do not execute on_exit_to, use on_exit instead and " 312 | "provide to_from_state"); 313 | 314 | static_assert(Event != fsm_event::count, "fsm_state : invalid event"); 315 | 316 | // Check the event, call the appropriate user functions if it is 317 | // stored. 318 | 319 | // on_enter and on_enter_from 320 | if constexpr (Event == fsm_event::on_enter) { 321 | // Build our lookup key for the on_enter_from event. 322 | using enter_from_key_t = fsm_event_key; 324 | // Build our lookup key for the on_enter event. 325 | using enter_key_t = fsm_event_key; 327 | 328 | // Encourage the compiler, he needs some positive reinforcement 329 | // after all this. 330 | constexpr bool has_enter_from 331 | = _events.template contains(); 332 | [[maybe_unused]] constexpr bool has_enter 333 | = _events.template contains(); 334 | 335 | if constexpr (FromToState != StateEnum::count && has_enter_from) { 336 | // Invoke with machine as last argument to support calling 337 | // member functions on object. 338 | constexpr auto& f = _events.template find(); 339 | 340 | // std::invoke is not constexpr. 341 | return f(machine, std::forward(func_args)...); 342 | 343 | } else if constexpr (has_enter) { 344 | constexpr auto& f = _events.template find(); 345 | return f(machine, std::forward(func_args)...); 346 | } 347 | 348 | // on_update 349 | } else if constexpr (Event == fsm_event::on_update) { 350 | // Lookup for on_update 351 | using update_key_t = fsm_event_key; 353 | constexpr bool has_update 354 | = _events.template contains(); 355 | 356 | if constexpr (has_update) { 357 | constexpr auto& f = _events.template find(); 358 | return f(machine, std::forward(func_args)...); 359 | } 360 | 361 | // on_exit and on_exit_to 362 | } else if constexpr (Event == fsm_event::on_exit) { 363 | // Lookup for on_exit_to 364 | using exit_to_key_t = fsm_event_key; 366 | 367 | // Lookup for on_exit 368 | using exit_key_t = fsm_event_key; 370 | 371 | constexpr bool has_exit_to 372 | = _events.template contains(); 373 | [[maybe_unused]] constexpr bool has_exit 374 | = _events.template contains(); 375 | 376 | if constexpr (FromToState != StateEnum::count && has_exit_to) { 377 | constexpr auto& f = _events.template find(); 378 | return f(machine, std::forward(func_args)...); 379 | 380 | } else if constexpr (has_exit) { 381 | constexpr auto& f = _events.template find(); 382 | return f(machine, std::forward(func_args)...); 383 | } 384 | } 385 | } 386 | 387 | private: 388 | static constexpr auto _transitions = TransitionMap::unpack(); 389 | static constexpr auto _events = EventMap::unpack(); 390 | }; 391 | 392 | 393 | template 395 | struct fsm { 396 | using fsm_t = fsm; 398 | 399 | using fsm_inexit_t = fsm; 401 | using fsm_notinexit_t = fsm; 403 | 404 | 405 | template 406 | [[nodiscard]] static constexpr auto init(FuncArgs&&... func_args) { 407 | static_assert(CurrentState == StateEnum::count, 408 | "CurrentState is valid, did you already call init?"); 409 | 410 | // needs init 411 | constexpr auto& s 412 | = _states.template find>(); 413 | using new_fsm_t = fsm; 415 | constexpr auto passed_in = new_fsm_t{}; 416 | 417 | using func_ret_t 418 | = decltype(s.template execute_event( 419 | passed_in, std::forward(func_args)...)); 420 | 421 | // When triggering inside on_enter, the user must return the new fsm. 422 | // If the event returns void, no trigger happened. 423 | if constexpr (!std::is_same_v) { 424 | return s.template execute_event( 425 | passed_in, std::forward(func_args)...); 426 | } else { 427 | s.template execute_event( 428 | passed_in, std::forward(func_args)...); 429 | return passed_in; 430 | } 431 | } 432 | 433 | template 434 | [[nodiscard]] static constexpr auto trigger(FuncArgs&&... func_args) { 435 | static_assert(CurrentState != StateEnum::count, 436 | "CurrentState is invalid, did you forget to call init?"); 437 | 438 | static_assert( 439 | InOnExit == false, "cannot trigger transition in on_exit"); 440 | 441 | // do normal trigger... 442 | // if anything can be considered normal at this point... 443 | // Here is when I started questioning my life decisions... 444 | constexpr StateEnum from_state = CurrentState; 445 | 446 | constexpr StateEnum to_state 447 | = _states.template find>() 448 | .template transition_target(); 449 | 450 | // Call on_exit 451 | constexpr auto& from_s 452 | = _states.template find>(); 453 | 454 | constexpr auto passed_in_exit = fsm_inexit_t{}; 455 | from_s.template execute_event( 456 | passed_in_exit, std::forward(func_args)...); 457 | 458 | // Call on_enter and update current state. 459 | using new_fsm_t = fsm; 461 | 462 | constexpr auto passed_in_enter = new_fsm_t{}; 463 | constexpr auto& to_s 464 | = _states.template find>(); 465 | 466 | using func_ret_t = decltype( 467 | to_s.template execute_event( 468 | passed_in_enter, std::forward(func_args)...)); 469 | 470 | // When triggering inside on_enter, the user must return the new fsm. 471 | // If the event returns void, no trigger happened, state stays the same. 472 | if constexpr (!std::is_same_v) { 473 | // static_assert(detail::is_same_templated(), 474 | // "You cannot return custom values in on_enter, on_exit. " 475 | // "Only from update."); 476 | 477 | // Return new state. 478 | return to_s.template execute_event( 479 | passed_in_enter, std::forward(func_args)...); 480 | } else { 481 | to_s.template execute_event( 482 | passed_in_enter, std::forward(func_args)...); 483 | // Return 'current' state. 484 | return passed_in_enter; 485 | } 486 | } 487 | 488 | 489 | template 490 | static constexpr auto update(FuncArgs&&... func_args) { 491 | static_assert(CurrentState != StateEnum::count, 492 | "CurrentState is invalid, did you forget to call init?"); 493 | 494 | constexpr auto passed_in = fsm_t{}; 495 | constexpr auto& s = _states.template find< 496 | fsm_state_key>(); 497 | 498 | return s.template execute_event( 499 | passed_in, std::forward(func_args)...); 500 | } 501 | 502 | private: 503 | static constexpr auto _states = StateMap::unpack(); 504 | }; 505 | 506 | 507 | template 509 | struct fsm_transition_builder { 510 | template 511 | static constexpr auto make_transition() { 512 | using parent_t = fsm_transition_builder; 514 | 515 | return fsm_transition_builder{}; 517 | } 518 | 519 | static constexpr auto unpack() { 520 | if constexpr (!std::is_same_v) { 521 | return std::tuple_cat( 522 | std::make_tuple(std::tuple{ 523 | fsm_transition_key{}, ToS }), 524 | Parent::unpack()); 525 | } else { 526 | return std::make_tuple(std::tuple{ 527 | fsm_transition_key{}, ToS }); 528 | } 529 | } 530 | }; 531 | 532 | 533 | template 535 | struct fsm_event_builder { 536 | template 538 | static constexpr auto make_event([[maybe_unused]] NewFunc newfunc) { 539 | 540 | #if defined(_MSC_VER) && defined(FEA_FSM_NO_EVENT_WRAPPER) 541 | // Only works on MSVC. 542 | // There is apparently a proposal to remove the restrictions on static 543 | // constexpr variables in static constexpr functions. 544 | 545 | // Store the func here temporarily until we unpack this stuff. 546 | // Since the function is templated on NewFunc type, we shouldn't get 547 | // collisions with other make_event calls. 548 | // aka famous last words 549 | 550 | static constexpr auto f = newfunc; 551 | struct func_wrapper { 552 | static constexpr auto unpack() { 553 | return f; 554 | } 555 | }; 556 | 557 | using parent_t = fsm_event_builder; 559 | return fsm_event_builder{}; 561 | #else 562 | // For now, use macro :( 563 | static_assert(detail::is_event_builder::value, 564 | "use 'fea_event' macro to create events. ex : " 565 | "fea_event(event_name, [](auto&){});"); 566 | using parent_t = fsm_event_builder; 568 | return fsm_event_builder{}; 570 | #endif 571 | } 572 | 573 | static constexpr auto unpack() { 574 | if constexpr (!std::is_same_v) { 575 | return std::tuple_cat( 576 | std::make_tuple(std::tuple{ 577 | fsm_event_key{}, 578 | Func::unpack() }), 579 | Parent::unpack()); 580 | } else { 581 | return std::make_tuple( 582 | std::tuple{ fsm_event_key{}, 583 | Func::unpack() }); 584 | } 585 | } 586 | }; // namespace cexpr 587 | 588 | template 589 | struct fsm_builder { 590 | static constexpr auto empty_t() { 591 | return fsm_transition_builder{}; 593 | } 594 | 595 | static constexpr auto empty_e() { 596 | struct func_wrapper { 597 | static constexpr auto unpack() { 598 | return [](auto&) {}; 599 | } 600 | }; 601 | return fsm_event_builder{}; 603 | } 604 | 605 | template 606 | static constexpr auto make_transition() { 607 | static_assert( 608 | FromTransition != TransitionEnum::count, "invalid transition"); 609 | static_assert(ToState != StateEnum::count, "invalid state"); 610 | 611 | return fsm_transition_builder{}; 613 | } 614 | 615 | template 617 | static constexpr auto make_event([[maybe_unused]] Func func) { 618 | static_assert(Event != fsm_event::count, "invalid event"); 619 | 620 | #if defined(_MSC_VER) && defined(FEA_FSM_NO_EVENT_WRAPPER) 621 | // This only works on MSVC. 622 | // There is a proposal to remove the restrictions on static 623 | // cosntexpr variables in static constexpr functions. 624 | 625 | static constexpr auto f = func; 626 | struct func_wrapper { 627 | static constexpr auto unpack() { 628 | return f; 629 | } 630 | }; 631 | 632 | return fsm_event_builder{}; 634 | #else 635 | // For now, use macro :( 636 | static_assert(detail::is_event_builder::value, 637 | "use 'fea_event' macro to create events. ex : " 638 | "fea_event(event_name, [](auto&){});"); 639 | return fsm_event_builder{}; 640 | #endif 641 | } 642 | 643 | template 644 | static constexpr auto make_state(TransitionBuilder, EventBuilder) { 645 | struct t_map { 646 | static constexpr auto unpack() { 647 | return detail::make_tuple_map(); 648 | } 649 | }; 650 | 651 | struct e_map { 652 | static constexpr auto unpack() { 653 | return detail::make_tuple_map(); 654 | } 655 | }; 656 | 657 | return fsm_state{}; 658 | } 659 | 660 | template 661 | static constexpr auto make_machine(State1, States...) { 662 | 663 | struct s_map { 664 | struct StateBuilder { 665 | static constexpr auto unpack() { 666 | return std::make_tuple( 667 | std::make_tuple( 668 | fsm_state_key{}, 669 | State1{}), 670 | std::make_tuple( 671 | fsm_state_key{}, 672 | States{})...); 673 | } 674 | }; 675 | 676 | static constexpr auto unpack() { 677 | return detail::make_tuple_map(); 678 | } 679 | }; 680 | 681 | return fsm{}; 683 | } 684 | }; 685 | 686 | } // namespace cexpr 687 | } // namespace fea 688 | -------------------------------------------------------------------------------- /tests/hfsm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace { 6 | TEST(hfsm, basics) { 7 | 8 | enum class transition : size_t { 9 | do_walk, 10 | do_walk_crouch, 11 | do_walk_normal, 12 | do_run, 13 | do_run_sub, 14 | do_jump, 15 | count, 16 | }; 17 | std::array transition_names{ 18 | "do_walk", 19 | "do_walk_crouch", 20 | "do_walk_normal", 21 | "do_run", 22 | "do_run_sub", 23 | "do_jump", 24 | }; 25 | 26 | enum class state : size_t { 27 | walk, 28 | walk_crouch, 29 | walk_normal, 30 | run, 31 | run_sub, 32 | run_sub_sub, 33 | run_sub_sub_sub, 34 | run_sub_sub_sub_sub, 35 | jump, 36 | count, 37 | }; 38 | 39 | struct test_me { 40 | size_t enter_num = 0; 41 | size_t update_num = 0; 42 | size_t exit_num = 0; 43 | size_t enter_from_num = 0; 44 | size_t exit_to_num = 0; 45 | }; 46 | test_me state_test; 47 | 48 | 49 | bool state_machine_ready = false; 50 | bool do_something = false; 51 | fea::hfsm smachine{}; 52 | 53 | 54 | fea::hfsm_state walk_normal_state{ state::walk_normal, 55 | "walk_normal" }; 56 | walk_normal_state.add_event( 57 | [&](auto&) { state_test.enter_num++; }); 58 | walk_normal_state.add_event([&](auto&) { 59 | state_test.update_num++; 60 | if (do_something) { 61 | smachine.trigger(); 62 | return; 63 | } 64 | if (state_machine_ready) { 65 | smachine.trigger(); 66 | return; 67 | } 68 | }); 69 | walk_normal_state.add_event( 70 | [&](auto&) { state_test.exit_num++; }); 71 | walk_normal_state.add_event( 72 | [&](auto&) { state_test.enter_from_num++; }); 73 | walk_normal_state 74 | .add_event( 75 | [&](auto&) { state_test.enter_from_num++; }, true); 76 | walk_normal_state 77 | .add_event( 78 | [&](auto&) { state_test.exit_to_num++; }); 79 | 80 | walk_normal_state 81 | .add_transition(); 82 | 83 | walk_normal_state.enable_parent_update(); 84 | 85 | walk_normal_state.init(); 86 | 87 | walk_normal_state.execute_event( 88 | state::run, smachine); 89 | walk_normal_state.execute_event( 90 | state::jump, smachine); 91 | walk_normal_state.execute_event( 92 | state::walk, smachine); 93 | walk_normal_state.execute_event( 94 | state::walk_normal, smachine); 95 | walk_normal_state.execute_event( 96 | state::walk_crouch, smachine); 97 | 98 | EXPECT_EQ(state_test.enter_num, 3u); 99 | EXPECT_EQ(state_test.enter_from_num, 2u); 100 | EXPECT_EQ(state_test.update_num, 0u); 101 | EXPECT_EQ(state_test.exit_num, 0u); 102 | EXPECT_EQ(state_test.exit_to_num, 0u); 103 | 104 | 105 | fea::hfsm_state walk_crouch_state{ state::walk_crouch, 106 | "walk_crouch" }; 107 | walk_crouch_state.add_event( 108 | [&](auto&) { state_test.enter_num++; }); 109 | walk_crouch_state.add_event([&](auto&) { 110 | state_test.update_num++; 111 | if (state_machine_ready) 112 | smachine.trigger(); 113 | }); 114 | walk_crouch_state.add_event( 115 | [&](auto&) { state_test.exit_num++; }); 116 | walk_crouch_state.add_event( 117 | [&](auto&) { state_test.enter_from_num++; }); 118 | walk_crouch_state 119 | .add_event( 120 | [&](auto&) { state_test.enter_from_num++; }, true); 121 | walk_crouch_state 122 | .add_event( 123 | [&](auto&) { state_test.exit_to_num++; }, true); 124 | walk_crouch_state.add_event( 125 | [&, v = false](auto&) mutable { 126 | state_test.exit_to_num++; 127 | if (state_machine_ready && !v) { 128 | v = true; 129 | smachine.trigger(); 130 | } 131 | }, 132 | true); 133 | 134 | walk_crouch_state 135 | .add_transition(); 136 | walk_crouch_state.add_transition(); 137 | walk_crouch_state.add_transition(); 138 | walk_crouch_state.enable_parent_update(); 139 | 140 | 141 | fea::hfsm_state walk_state{ state::walk, "walk" }; 142 | walk_state.add_event( 143 | [&](auto&) { state_test.enter_num++; }); 144 | 145 | walk_state.add_event( 146 | [&](auto&) { state_test.enter_from_num++; }); 147 | walk_state.add_event( 148 | [&](auto&) { state_test.enter_from_num++; }); 149 | 150 | walk_state.add_event([&](auto&) { 151 | state_test.update_num++; 152 | if (do_something) { 153 | do_something = false; 154 | smachine.trigger(); 155 | } 156 | }); 157 | 158 | walk_state.add_event( 159 | [&](auto&) { state_test.exit_num++; }); 160 | 161 | walk_state.add_event( 162 | [&](auto&) { state_test.exit_to_num++; }); 163 | 164 | // walk_state.add_transition(); 165 | walk_state.add_transition(); 166 | walk_state.add_transition(); 167 | 168 | walk_state.add_substate(std::move(walk_normal_state)); 169 | walk_state.add_substate(std::move(walk_crouch_state)); 170 | 171 | walk_state.init(); 172 | 173 | state_test = {}; 174 | walk_state.execute_event(state::run, smachine); 175 | walk_state.execute_event(state::jump, smachine); 176 | walk_state.execute_event(state::walk, smachine); 177 | walk_state.execute_event( 178 | state::walk_normal, smachine); 179 | walk_state.execute_event( 180 | state::walk_crouch, smachine); 181 | EXPECT_EQ(state_test.enter_num, 3u); 182 | EXPECT_EQ(state_test.enter_from_num, 2u); 183 | EXPECT_EQ(state_test.update_num, 0u); 184 | EXPECT_EQ(state_test.exit_num, 0u); 185 | EXPECT_EQ(state_test.exit_to_num, 0u); 186 | 187 | walk_state.execute_event( 188 | state::count, smachine); 189 | EXPECT_EQ(state_test.enter_num, 3u); 190 | EXPECT_EQ(state_test.enter_from_num, 2u); 191 | EXPECT_EQ(state_test.update_num, 1u); 192 | EXPECT_EQ(state_test.exit_num, 0u); 193 | EXPECT_EQ(state_test.exit_to_num, 0u); 194 | 195 | walk_state.execute_event(state::run, smachine); 196 | walk_state.execute_event(state::jump, smachine); 197 | walk_state.execute_event(state::walk, smachine); 198 | walk_state.execute_event( 199 | state::walk_normal, smachine); 200 | walk_state.execute_event( 201 | state::walk_crouch, smachine); 202 | EXPECT_EQ(state_test.enter_num, 3u); 203 | EXPECT_EQ(state_test.enter_from_num, 2u); 204 | EXPECT_EQ(state_test.update_num, 1u); 205 | EXPECT_EQ(state_test.exit_num, 4u); 206 | EXPECT_EQ(state_test.exit_to_num, 1u); 207 | 208 | fea::hfsm_state::tranny_info tg; 209 | walk_state.transition(tg); 210 | EXPECT_EQ(tg.to, state::count); 211 | tg = {}; 212 | walk_state.transition(tg); 213 | EXPECT_EQ(tg.to, state::run); 214 | tg = {}; 215 | walk_state.transition(tg); 216 | EXPECT_EQ(tg.to, state::jump); 217 | 218 | smachine.add_state(std::move(walk_state)); 219 | 220 | fea::hfsm_state run_subsubsubsub_state{ 221 | state::run_sub_sub_sub_sub, "run_subsubsubsubstate" 222 | }; 223 | run_subsubsubsub_state.add_event( 224 | [&](auto&) { state_test.enter_num++; }); 225 | run_subsubsubsub_state.add_event([&](auto&) { 226 | state_test.update_num++; 227 | if (state_machine_ready) 228 | smachine.trigger(); 229 | }); 230 | run_subsubsubsub_state.add_event( 231 | [&](auto&) { state_test.exit_num++; }); 232 | 233 | fea::hfsm_state run_subsubsub_state{ 234 | state::run_sub_sub_sub, "run_subsubsubstate" 235 | }; 236 | run_subsubsub_state.add_event( 237 | [&](auto&) { state_test.enter_num++; }); 238 | run_subsubsub_state.add_event([&](auto&) { 239 | state_test.update_num++; 240 | if (state_machine_ready) 241 | smachine.trigger(); 242 | }); 243 | run_subsubsub_state.add_event( 244 | [&](auto&) { state_test.exit_num++; }); 245 | 246 | run_subsubsub_state.add_substate( 247 | std::move(run_subsubsubsub_state)); 248 | 249 | fea::hfsm_state run_subsub_state{ state::run_sub_sub, 250 | "run_subsubstate" }; 251 | run_subsub_state.add_event( 252 | [&](auto&) { state_test.enter_num++; }); 253 | run_subsub_state.add_event([&](auto&) { 254 | state_test.update_num++; 255 | if (state_machine_ready) 256 | smachine.trigger(); 257 | }); 258 | run_subsub_state.add_event( 259 | [&](auto&) { state_test.exit_num++; }); 260 | 261 | run_subsub_state.add_substate( 262 | std::move(run_subsubsub_state)); 263 | 264 | fea::hfsm_state run_sub_state{ state::run_sub, 265 | "run_substate" }; 266 | run_sub_state.add_event( 267 | [&](auto&) { state_test.enter_num++; }); 268 | run_sub_state.add_event([&](auto&) { 269 | state_test.update_num++; 270 | if (state_machine_ready) 271 | smachine.trigger(); 272 | }); 273 | run_sub_state.add_event( 274 | [&](auto&) { state_test.exit_num++; }); 275 | 276 | run_sub_state.add_substate(std::move(run_subsub_state)); 277 | 278 | fea::hfsm_state run_state{ state::run, "run" }; 279 | run_state.add_event( 280 | [&](auto&) { state_test.enter_num++; }); 281 | 282 | run_state.add_event( 283 | [&](auto&) { state_test.enter_from_num++; }); 284 | 285 | run_state.add_event([&](auto&) { 286 | state_test.update_num++; 287 | if (state_machine_ready) 288 | smachine.trigger(); 289 | }); 290 | 291 | run_state.add_event( 292 | [&](auto&) { state_test.exit_num++; }); 293 | 294 | run_state.add_event( 295 | [&](auto&) { state_test.exit_to_num++; }); 296 | 297 | run_state.add_transition(); 298 | run_state.add_transition(); 299 | 300 | run_state.add_substate(std::move(run_sub_state)); 301 | 302 | run_state.init(); 303 | 304 | state_test = {}; 305 | run_state.execute_event(state::run, smachine); 306 | run_state.execute_event(state::jump, smachine); 307 | run_state.execute_event(state::walk, smachine); 308 | run_state.execute_event( 309 | state::walk_normal, smachine); 310 | run_state.execute_event( 311 | state::walk_crouch, smachine); 312 | EXPECT_EQ(state_test.enter_num, 4u); 313 | EXPECT_EQ(state_test.enter_from_num, 1u); 314 | EXPECT_EQ(state_test.update_num, 0u); 315 | EXPECT_EQ(state_test.exit_num, 0u); 316 | EXPECT_EQ(state_test.exit_to_num, 0u); 317 | 318 | run_state.execute_event(state::run, smachine); 319 | run_state.execute_event(state::jump, smachine); 320 | run_state.execute_event(state::walk, smachine); 321 | run_state.execute_event( 322 | state::walk_normal, smachine); 323 | run_state.execute_event( 324 | state::walk_crouch, smachine); 325 | EXPECT_EQ(state_test.enter_num, 4u); 326 | EXPECT_EQ(state_test.enter_from_num, 1u); 327 | EXPECT_EQ(state_test.update_num, 5u); // parent update not enabled 328 | EXPECT_EQ(state_test.exit_num, 0u); 329 | EXPECT_EQ(state_test.exit_to_num, 0u); 330 | 331 | run_state.execute_event(state::run, smachine); 332 | run_state.execute_event(state::jump, smachine); 333 | run_state.execute_event(state::walk, smachine); 334 | run_state.execute_event( 335 | state::walk_normal, smachine); 336 | run_state.execute_event( 337 | state::walk_crouch, smachine); 338 | EXPECT_EQ(state_test.enter_num, 4u); 339 | EXPECT_EQ(state_test.enter_from_num, 1u); 340 | EXPECT_EQ(state_test.update_num, 5u); 341 | EXPECT_EQ(state_test.exit_num, 4u); 342 | EXPECT_EQ(state_test.exit_to_num, 1u); 343 | 344 | tg = {}; 345 | run_state.transition(tg); 346 | EXPECT_EQ(tg.to, state::walk); 347 | tg = {}; 348 | run_state.transition(tg); 349 | EXPECT_EQ(tg.to, state::count); 350 | tg = {}; 351 | run_state.transition(tg); 352 | EXPECT_EQ(tg.to, state::jump); 353 | 354 | smachine.add_state(std::move(run_state)); 355 | 356 | 357 | fea::hfsm_state jump_state{ state::jump, "jump" }; 358 | jump_state.add_event( 359 | [&](auto&) { state_test.enter_num++; }); 360 | jump_state.add_event([&](auto&) { 361 | state_test.update_num++; 362 | if (state_machine_ready) 363 | smachine.trigger(); 364 | }); 365 | jump_state.add_event( 366 | [&](auto&) { state_test.exit_num++; }); 367 | jump_state.add_event( 368 | [&](auto&) { state_test.enter_from_num++; }); 369 | jump_state.add_event( 370 | [&](auto&) { state_test.enter_from_num++; }); 371 | 372 | jump_state.add_transition(); 373 | jump_state.add_transition(); 374 | jump_state.add_guard_transition( 375 | [&]() { return false; }); 376 | jump_state.add_guard_transition( 377 | [&]() { return true; }); 378 | jump_state.add_guard_transition( 379 | [&]() { return true; }); 380 | 381 | state_test = {}; 382 | jump_state.execute_event(state::run, smachine); 383 | jump_state.execute_event(state::jump, smachine); 384 | jump_state.execute_event(state::walk, smachine); 385 | jump_state.execute_event( 386 | state::walk_normal, smachine); 387 | jump_state.execute_event( 388 | state::walk_crouch, smachine); 389 | EXPECT_EQ(state_test.enter_num, 3u); 390 | EXPECT_EQ(state_test.enter_from_num, 2u); 391 | EXPECT_EQ(state_test.update_num, 0u); 392 | EXPECT_EQ(state_test.exit_num, 0u); 393 | EXPECT_EQ(state_test.exit_to_num, 0u); 394 | 395 | jump_state.execute_event(state::run, smachine); 396 | jump_state.execute_event(state::jump, smachine); 397 | jump_state.execute_event(state::walk, smachine); 398 | jump_state.execute_event( 399 | state::walk_normal, smachine); 400 | jump_state.execute_event( 401 | state::walk_crouch, smachine); 402 | EXPECT_EQ(state_test.enter_num, 3u); 403 | EXPECT_EQ(state_test.enter_from_num, 2u); 404 | EXPECT_EQ(state_test.update_num, 5u); 405 | EXPECT_EQ(state_test.exit_num, 0u); 406 | EXPECT_EQ(state_test.exit_to_num, 0u); 407 | 408 | jump_state.execute_event(state::run, smachine); 409 | jump_state.execute_event(state::jump, smachine); 410 | jump_state.execute_event(state::walk, smachine); 411 | jump_state.execute_event( 412 | state::walk_normal, smachine); 413 | jump_state.execute_event( 414 | state::walk_crouch, smachine); 415 | EXPECT_EQ(state_test.enter_num, 3u); 416 | EXPECT_EQ(state_test.enter_from_num, 2u); 417 | EXPECT_EQ(state_test.update_num, 5u); 418 | EXPECT_EQ(state_test.exit_num, 5u); 419 | EXPECT_EQ(state_test.exit_to_num, 0u); 420 | 421 | tg = {}; 422 | jump_state.transition(tg); 423 | EXPECT_EQ(tg.to, state::walk_crouch); 424 | tg = {}; 425 | jump_state.transition(tg); 426 | EXPECT_EQ(tg.to, state::run); 427 | tg = {}; 428 | jump_state.transition(tg); 429 | EXPECT_EQ(tg.to, state::count); 430 | 431 | smachine.add_state(std::move(jump_state)); 432 | 433 | state_machine_ready = true; // for tests 434 | smachine.add_transition_names(transition_names); 435 | // smachine.enable_print(); 436 | smachine.update(); 437 | smachine.update(); 438 | do_something = true; 439 | smachine.update(); 440 | smachine.update(); 441 | smachine.update(); 442 | smachine.update(); 443 | smachine.update(); 444 | smachine.update(); 445 | smachine.update(); 446 | } 447 | 448 | TEST(hfsm, parallel) { 449 | using namespace fea; 450 | enum class transition : size_t { 451 | do_walk, 452 | do_run, 453 | do_head_idle, 454 | do_head_look, 455 | count, 456 | }; 457 | 458 | enum class state : size_t { 459 | movement, 460 | walk, 461 | run, 462 | head, 463 | head_idle, 464 | head_look, 465 | count, 466 | }; 467 | 468 | size_t enters = 0; 469 | size_t updates = 0; 470 | 471 | hfsm_state walk_state{ state::walk, "walk" }; 472 | walk_state.add_event([&](auto&) { ++enters; }); 473 | walk_state.add_event([&](auto&) { ++updates; }); 474 | walk_state.add_transition(); 475 | 476 | hfsm_state run_state{ state::run, "run" }; 477 | run_state.add_event([&](auto&) { ++updates; }); 478 | run_state.add_transition(); 479 | 480 | hfsm_state movement_state{ state::movement, "movement" }; 481 | movement_state.add_event([&](auto&) { ++updates; }); 482 | movement_state.add_substate(std::move(walk_state)); 483 | movement_state.add_substate(std::move(run_state)); 484 | 485 | hfsm_state head_idle_state{ state::head_idle, 486 | "head_idle" }; 487 | head_idle_state.add_event([&](auto&) { ++enters; }); 488 | head_idle_state.add_event([&](auto&) { ++updates; }); 489 | head_idle_state 490 | .add_transition(); 491 | head_idle_state.add_auto_transition_guard( 492 | []() { return true; }); 493 | 494 | hfsm_state head_look_state{ state::head_look, 495 | "head_look" }; 496 | head_look_state.add_event([&](auto&) { ++enters; }); 497 | head_look_state.add_event([&](auto&) { ++updates; }); 498 | head_idle_state 499 | .add_transition(); 500 | 501 | hfsm_state head_state{ state::head, "head" }; 502 | head_state.add_event([&](auto&) { ++updates; }); 503 | head_state.add_substate(std::move(head_idle_state)); 504 | head_state.add_substate(std::move(head_look_state)); 505 | 506 | hfsm smachine; 507 | smachine.add_state(std::move(movement_state)); 508 | 509 | hfsm smachine2; 510 | smachine2.add_state(std::move(head_state)); 511 | 512 | smachine.add_parallel_hfsm(std::move(smachine2)); 513 | 514 | // smachine.enable_print(); 515 | smachine.update(); 516 | smachine.update(); 517 | smachine.update(); 518 | 519 | EXPECT_EQ(enters, 3u); 520 | EXPECT_EQ(updates, 5u); 521 | } 522 | 523 | TEST(hfsm, auto_transition_guards) { 524 | using namespace fea; 525 | enum class transition : size_t { 526 | do_walk, 527 | do_run, 528 | count, 529 | }; 530 | 531 | enum class state : size_t { 532 | walk, 533 | run, 534 | count, 535 | }; 536 | 537 | bool auto_guard = false; 538 | size_t enters = 0; 539 | size_t updates = 0; 540 | size_t exits = 0; 541 | 542 | hfsm_state walk_state{ state::walk, "walk" }; 543 | walk_state.add_event([&](auto&) { ++enters; }); 544 | walk_state.add_event([&](auto&) { ++updates; }); 545 | walk_state.add_event([&](auto&) { ++exits; }); 546 | 547 | walk_state.add_transition(); 548 | walk_state.add_auto_transition_guard( 549 | [&]() { return auto_guard; }); 550 | 551 | hfsm_state run_state{ state::run, "run" }; 552 | run_state.add_event( 553 | [&](auto&) { ++enters; }); 554 | run_state.add_event([&](auto&) { ++updates; }); 555 | run_state.add_event([&](auto&) { ++exits; }); 556 | 557 | run_state.add_transition(); 558 | run_state.add_auto_transition_guard( 559 | [&]() { return auto_guard; }); 560 | 561 | hfsm smachine{}; 562 | smachine.add_state(std::move(walk_state)); 563 | smachine.add_state(std::move(run_state)); 564 | 565 | // smachine.enable_print(); 566 | smachine.update(); 567 | 568 | EXPECT_EQ(enters, 1u); 569 | EXPECT_EQ(updates, 1u); 570 | EXPECT_EQ(exits, 0u); 571 | 572 | auto_guard = true; 573 | smachine.update(); 574 | 575 | EXPECT_EQ(enters, 2u); 576 | EXPECT_EQ(updates, 1u); 577 | EXPECT_EQ(exits, 1u); 578 | 579 | auto_guard = false; 580 | smachine.update(); 581 | 582 | EXPECT_EQ(enters, 2u); 583 | EXPECT_EQ(updates, 2u); 584 | EXPECT_EQ(exits, 1u); 585 | 586 | auto_guard = true; 587 | smachine.update(); 588 | 589 | EXPECT_EQ(enters, 3u); 590 | EXPECT_EQ(updates, 2u); 591 | EXPECT_EQ(exits, 2u); 592 | } 593 | 594 | TEST(hfsm, history_transition_guards) { 595 | using namespace fea; 596 | enum class transition : size_t { 597 | do_walk, 598 | do_run, 599 | do_jump, 600 | yield, 601 | count, 602 | }; 603 | 604 | enum class state : size_t { 605 | walk, 606 | run, 607 | jump, 608 | count, 609 | }; 610 | 611 | size_t enters = 0; 612 | size_t enters_from_run = 0; 613 | size_t enters_from_walk = 0; 614 | size_t updates = 0; 615 | size_t exits = 0; 616 | size_t exits_to_run = 0; 617 | size_t exits_to_walk = 0; 618 | 619 | hfsm smachine{}; 620 | 621 | hfsm_state walk_state{ state::walk, "walk" }; 622 | walk_state.add_event([&](auto&) { ++enters; }); 623 | walk_state.add_event([&](auto&) { ++updates; }); 624 | walk_state.add_event([&](auto&) { ++exits; }); 625 | 626 | walk_state.add_transition(); 627 | walk_state.add_transition(); 628 | 629 | hfsm_state run_state{ state::run, "run" }; 630 | run_state.add_event([&](auto&) { ++enters; }); 631 | run_state.add_event([&](auto&) { ++updates; }); 632 | run_state.add_event([&](auto&) { ++exits; }); 633 | 634 | run_state.add_transition(); 635 | run_state.add_transition(); 636 | 637 | hfsm_state jump_state{ state::jump, "jump" }; 638 | jump_state.add_event( 639 | [&](auto&) { ++enters_from_walk; }); 640 | jump_state.add_event( 641 | [&](auto&) { ++enters_from_run; }); 642 | jump_state.add_event([&](auto&) { 643 | ++updates; 644 | smachine.trigger(); 645 | }); 646 | jump_state.add_event( 647 | [&](auto&) { ++exits_to_walk; }); 648 | jump_state.add_event( 649 | [&](auto&) { ++exits_to_run; }); 650 | 651 | jump_state.add_yield_transition(); 652 | 653 | smachine.add_state(std::move(walk_state)); 654 | smachine.add_state(std::move(run_state)); 655 | smachine.add_state(std::move(jump_state)); 656 | 657 | // smachine.enable_print(); 658 | smachine.update(); 659 | 660 | EXPECT_EQ(enters, 1u); 661 | EXPECT_EQ(enters_from_walk, 0u); 662 | EXPECT_EQ(enters_from_run, 0u); 663 | EXPECT_EQ(updates, 1u); 664 | EXPECT_EQ(exits, 0u); 665 | EXPECT_EQ(exits_to_walk, 0u); 666 | EXPECT_EQ(exits_to_run, 0u); 667 | 668 | smachine.trigger(); 669 | smachine.update(); 670 | 671 | EXPECT_EQ(enters, 1u); 672 | EXPECT_EQ(enters_from_walk, 1u); 673 | EXPECT_EQ(enters_from_run, 0u); 674 | EXPECT_EQ(updates, 1u); 675 | EXPECT_EQ(exits, 1u); 676 | EXPECT_EQ(exits_to_walk, 0u); 677 | EXPECT_EQ(exits_to_run, 0u); 678 | 679 | smachine.update(); 680 | 681 | EXPECT_EQ(enters, 2u); 682 | EXPECT_EQ(enters_from_walk, 1u); 683 | EXPECT_EQ(enters_from_run, 0u); 684 | EXPECT_EQ(updates, 2u); 685 | EXPECT_EQ(exits, 1u); 686 | EXPECT_EQ(exits_to_walk, 1u); 687 | EXPECT_EQ(exits_to_run, 0u); 688 | 689 | smachine.trigger(); 690 | smachine.update(); 691 | 692 | EXPECT_EQ(enters, 3u); 693 | EXPECT_EQ(enters_from_walk, 1u); 694 | EXPECT_EQ(enters_from_run, 0u); 695 | EXPECT_EQ(updates, 2u); 696 | EXPECT_EQ(exits, 2u); 697 | EXPECT_EQ(exits_to_walk, 1u); 698 | EXPECT_EQ(exits_to_run, 0u); 699 | 700 | smachine.update(); 701 | EXPECT_EQ(enters, 3u); 702 | EXPECT_EQ(enters_from_walk, 1u); 703 | EXPECT_EQ(enters_from_run, 0u); 704 | EXPECT_EQ(updates, 3u); 705 | EXPECT_EQ(exits, 2u); 706 | EXPECT_EQ(exits_to_walk, 1u); 707 | EXPECT_EQ(exits_to_run, 0u); 708 | 709 | smachine.trigger(); 710 | smachine.update(); 711 | 712 | EXPECT_EQ(enters, 3u); 713 | EXPECT_EQ(enters_from_walk, 1u); 714 | EXPECT_EQ(enters_from_run, 1u); 715 | EXPECT_EQ(updates, 3u); 716 | EXPECT_EQ(exits, 3u); 717 | EXPECT_EQ(exits_to_walk, 1u); 718 | EXPECT_EQ(exits_to_run, 0u); 719 | 720 | smachine.update(); 721 | EXPECT_EQ(enters, 4u); 722 | EXPECT_EQ(enters_from_walk, 1u); 723 | EXPECT_EQ(enters_from_run, 1u); 724 | EXPECT_EQ(updates, 4u); 725 | EXPECT_EQ(exits, 3u); 726 | EXPECT_EQ(exits_to_walk, 1u); 727 | EXPECT_EQ(exits_to_run, 1u); 728 | } 729 | 730 | TEST(hfsm, func_arguments) { 731 | using namespace fea; 732 | enum class transition : size_t { 733 | do_walk, 734 | do_run, 735 | count, 736 | }; 737 | 738 | enum class state : size_t { 739 | walk, 740 | run, 741 | count, 742 | }; 743 | 744 | size_t enters = 0; 745 | size_t updates = 0; 746 | size_t exits = 0; 747 | 748 | hfsm smachine{}; 749 | hfsm_state walk_state{ state::walk, "walk" }; 750 | walk_state.add_event([&](auto&, int& v) { 751 | ++v; 752 | ++enters; 753 | }); 754 | walk_state.add_event([&](auto&, int& v) { 755 | ++v; 756 | ++updates; 757 | }); 758 | walk_state.add_event([&](auto&, int& v) { 759 | ++v; 760 | ++exits; 761 | }); 762 | 763 | walk_state.add_transition(); 764 | 765 | hfsm_state run_state{ state::run, "run" }; 766 | run_state.add_event( 767 | [&](auto&, int& v) { 768 | ++v; 769 | ++enters; 770 | }); 771 | run_state.add_event([&](auto&, int& v) { 772 | ++v; 773 | ++updates; 774 | }); 775 | run_state.add_event([&](auto&, int& v) { 776 | ++v; 777 | ++exits; 778 | }); 779 | 780 | run_state.add_transition(); 781 | 782 | smachine.add_state(std::move(walk_state)); 783 | smachine.add_state(std::move(run_state)); 784 | 785 | // smachine.enable_print(); 786 | 787 | int test = 0; 788 | smachine.update(test); 789 | 790 | EXPECT_EQ(test, 2); 791 | EXPECT_EQ(enters, 1u); 792 | EXPECT_EQ(updates, 1u); 793 | EXPECT_EQ(exits, 0u); 794 | 795 | smachine.trigger(test); 796 | EXPECT_EQ(test, 2); 797 | 798 | smachine.update(test); 799 | 800 | EXPECT_EQ(test, 4); 801 | EXPECT_EQ(enters, 2u); 802 | EXPECT_EQ(updates, 1u); 803 | EXPECT_EQ(exits, 1u); 804 | } 805 | } // namespace 806 | -------------------------------------------------------------------------------- /include/fea_state_machines/hfsm.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) 2020, Philippe Groarke 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | #pragma once 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | // TODO : 46 | // - sanity check 47 | 48 | namespace fea { 49 | /* 50 | A large and feature-full heap based hfsm. 51 | https://statecharts.github.io/ 52 | 53 | Features : 54 | - OnEnter, OnUpdate, OnExit. 55 | - OnEnterFrom, OnExitTo. 56 | You decide if you override event behavior when coming from/going to 57 | specified states. 58 | - Supports user arguments in the callbacks (explained below). 59 | - State hierarchies : The "main" feature of a state chart. 60 | - Transition guards : These only transition if their predicate 61 | evaluates to true. 62 | - Auto transition guards : These automatically transition when you 63 | call update (before it) if their predicate evaluates to true. 64 | - Parallel states : Different state hierarchies running in parallel. 65 | - Yield transitions (aka history state) : This transition will return to the 66 | previous state. 67 | - Does NOT provide a "get_current_state" function. 68 | Checking the current state of an fsm is a major smell and usually points 69 | to either a misuse, misunderstanding or incomplete implementation of the 70 | fsm. Do not do that, rethink your states and transitions instead. 71 | 72 | Callbacks : 73 | - The first argument of your callback is always a ref to the fsm itself. 74 | This is useful for retriggering and when you store fsms in containers. 75 | You can use auto& to simplify your callback signature. 76 | [](auto& mymachine){} 77 | 78 | - Pass your own types at the end of the fsm and fsm_state template. 79 | These will be passed on to your callbacks when you call update or 80 | trigger. 81 | For example : fsm; 82 | Callback signature is: 83 | [](auto& machine, int, bool&, const void*){} 84 | 85 | 86 | Notes : 87 | - Uses std::function and heap. 88 | - Throws on unhandled transition. 89 | You must explicitly add re-entrant transitions or ignored transitions 90 | (by providing empty callbacks). IMHO this is one of the bigest source of 91 | bugs and broken behavior when working with FSMs. Throwing makes 92 | debugging much faster and easier. 93 | 94 | */ 95 | 96 | 97 | namespace detail { 98 | #if !defined(NDEBUG) 99 | inline constexpr bool debug_build = true; 100 | #else 101 | inline constexpr bool debug_build = false; 102 | #endif 103 | 104 | template 105 | constexpr void static_for(Func func, std::index_sequence) { 106 | (std::invoke(func, std::integral_constant{}), ...); 107 | } 108 | 109 | template 110 | constexpr void static_for(Func func) { 111 | static_for(func, std::make_index_sequence()); 112 | } 113 | 114 | template 115 | struct on_exit { 116 | on_exit(Func func) 117 | : _func(func) { 118 | } 119 | 120 | ~on_exit() { 121 | std::invoke(_func); 122 | } 123 | 124 | private: 125 | Func _func; 126 | }; 127 | } // namespace detail 128 | 129 | 130 | template 131 | struct hfsm; 132 | 133 | enum class hfsm_event : size_t { 134 | on_enter, 135 | on_update, 136 | on_exit, 137 | simple_events_count, 138 | on_enter_from, 139 | on_exit_to, 140 | total_count, 141 | }; 142 | 143 | template 144 | struct hfsm_state { 145 | using hfsm_t = hfsm; 146 | using hfsm_func_t = std::function; 147 | using hfsm_guard_func_t = std::function; 148 | 149 | struct tranny_info { 150 | std::vector exit_hierarchy; 151 | StateEnum from{ StateEnum::count }; 152 | StateEnum to{ StateEnum::count }; 153 | bool yield{ false }; 154 | bool internal_transition{ false }; 155 | }; 156 | 157 | hfsm_state(StateEnum s, const char* name) 158 | : _state(s) 159 | , _name(name) { 160 | _substate_indexes.fill(std::numeric_limits::max()); 161 | } 162 | 163 | // Called internally, no need to call this. 164 | void init() { 165 | _current_substate = _default_substate; 166 | if (_current_substate != StateEnum::count) { 167 | current_substate().init(); 168 | } 169 | } 170 | 171 | template 172 | void add_substate(hfsm_state&& state) { 173 | assert(std::get(_substate_indexes) 174 | == std::numeric_limits::max() 175 | && "state : substate already exists"); 176 | 177 | std::get(_substate_indexes) = _substates.size(); 178 | _substates.push_back(std::move(state)); 179 | 180 | if (_default_substate == StateEnum::count) { 181 | _default_substate = State; 182 | } 183 | } 184 | 185 | // By default, uses the first added substate as the default substate. 186 | template 187 | void add_default_substate() { 188 | _default_substate = State; 189 | } 190 | 191 | // call_general_event enables enter_from and exit_to to call their 192 | // respective generalized versions. For ex : 193 | // on_enter_from would call on_enter before itself. 194 | // on_exit_to would call on_exit before itself. 195 | // The order ensures the generalized version doesn't override the 196 | // specialized one. 197 | template 198 | void add_event(hfsm_func_t&& func, 199 | [[maybe_unused]] bool call_general_event = false) { 200 | if constexpr (Event == hfsm_event::on_enter_from) { 201 | static_assert(State != StateEnum::count, 202 | "state : must provide enter_from state when adding " 203 | "on_enter_from event"); 204 | 205 | // Want this? 206 | if (std::get(_enter_from_exists)) { 207 | throw std::invalid_argument{ 208 | "state : on_enter_from already exists for selected state" 209 | }; 210 | } 211 | 212 | std::get(_enter_from_events) = std::move(func); 213 | std::get(_enter_from_exists) = true; 214 | std::get(_enter_from_calls_on_enter) 215 | = call_general_event; 216 | 217 | } else if constexpr (Event == hfsm_event::on_exit_to) { 218 | static_assert(State != StateEnum::count, 219 | "state : must provide exit_to state when adding on_exit_to " 220 | "event"); 221 | 222 | if (std::get(_exit_to_exists)) { 223 | throw std::invalid_argument{ 224 | "state : on_exit_to already exists for selected state" 225 | }; 226 | } 227 | 228 | std::get(_exit_to_events) = std::move(func); 229 | std::get(_exit_to_exists) = true; 230 | std::get(_exit_to_calls_on_exit) 231 | = call_general_event; 232 | 233 | } else { 234 | assert(!call_general_event 235 | && "state : call_general_event is only valid for " 236 | "on_enter_from and on_exit_to events"); 237 | 238 | static_assert(State == StateEnum::count, 239 | "state : no need to provide state for on_enter, on_update " 240 | "and on_exit events"); 241 | if (std::get(_simple_event_exists)) { 242 | throw std::invalid_argument{ "state : event already exists" }; 243 | } 244 | 245 | std::get(_simple_events) = std::move(func); 246 | std::get(_simple_event_exists) = true; 247 | } 248 | } 249 | 250 | template 251 | void add_transition() { 252 | static_assert(Transition != TransitionEnum::count, 253 | "state : invalid transition"); 254 | 255 | // Want this? 256 | if (std::get(_transition_exists)) { 257 | throw std::invalid_argument{ 258 | "state : transition already exists for selected state" 259 | }; 260 | } 261 | 262 | if (std::get(_is_yield_transition)) { 263 | throw std::invalid_argument{ 264 | "state : transition predefined as yield transition" 265 | }; 266 | } 267 | 268 | std::get(_transitions) = State; 269 | std::get(_transition_exists) = true; 270 | } 271 | 272 | // Only takes transition if predicate evaluates to true. 273 | // Prioritized over normal transition, executed in order of addition. 274 | // You can still add a normal transition as a fallback mechanism. 275 | template 276 | void add_guard_transition(hfsm_guard_func_t&& func) { 277 | static_assert(Transition != TransitionEnum::count, 278 | "state : invalid transition"); 279 | 280 | std::get(_guard_transitions) 281 | .push_back({ std::move(func), std::move(State) }); 282 | std::get(_guard_transition_exists) = true; 283 | } 284 | 285 | // Checked before on_update and will call the transition automatically. 286 | // May skip on_update. Is checked on all states in the hierarchy (parents 287 | // first). 288 | // Must provide a valid transition. 289 | template 290 | void add_auto_transition_guard(hfsm_guard_func_t&& func) { 291 | static_assert(Transition != TransitionEnum::count, 292 | "state : invalid transition"); 293 | 294 | if (!std::get(_transition_exists)) { 295 | throw std::invalid_argument{ "state : transition doesn't exist" }; 296 | } 297 | 298 | std::get(_auto_transition_guards) 299 | .push_back(std::move(func)); 300 | } 301 | 302 | // A history transition returns to the previous state, whichever one it 303 | // is. 304 | template 305 | void add_yield_transition() { 306 | static_assert(Transition != TransitionEnum::count, 307 | "state : invalid transition"); 308 | 309 | if (std::get(_is_yield_transition)) { 310 | throw std::invalid_argument{ 311 | "state : transition is already set to yield" 312 | }; 313 | } 314 | 315 | if (std::get(_transition_exists)) { 316 | throw std::invalid_argument{ 317 | "state : transition already exists as non yield transition" 318 | }; 319 | } 320 | 321 | std::get(_is_yield_transition) = true; 322 | } 323 | 324 | // Depth-first 325 | template 326 | void transition(tranny_info& tg, FuncArgs... func_args) { 327 | static_assert(Transition != TransitionEnum::count, 328 | "state : invalid transition"); 329 | 330 | if (_current_substate != StateEnum::count) { 331 | current_substate().template transition( 332 | tg, func_args...); 333 | } 334 | 335 | assert(!(tg.yield && tg.internal_transition) 336 | && "state : can't yield and internally transition at the same " 337 | "time"); 338 | 339 | if (tg.yield) { 340 | tg.exit_hierarchy.push_back(this); 341 | return; 342 | } 343 | 344 | if (tg.internal_transition) { 345 | return; 346 | } 347 | 348 | // Return if a child handled transition. 349 | if (tg.to != StateEnum::count) { 350 | if (_substate_indexes[size_t(tg.to)] 351 | != std::numeric_limits::max()) { 352 | tg.internal_transition = true; 353 | tg.exit_hierarchy.push_back(this); 354 | } 355 | return; 356 | } 357 | 358 | if (std::get(_guard_transition_exists)) { 359 | for (const auto& [func, to_state] : 360 | _guard_transitions[size_t(Transition)]) { 361 | if (std::invoke(func, func_args...)) { 362 | tg.from = _state; 363 | tg.to = to_state; 364 | tg.exit_hierarchy.push_back(this); 365 | return; 366 | } 367 | } 368 | } 369 | 370 | if (std::get(_transition_exists)) { 371 | tg.from = _state; 372 | tg.to = std::get(_transitions); 373 | tg.exit_hierarchy.push_back(this); 374 | return; 375 | } 376 | 377 | if (std::get(_is_yield_transition)) { 378 | tg.from = _state; 379 | tg.exit_hierarchy.push_back(this); 380 | tg.yield = true; 381 | return; 382 | } 383 | } 384 | 385 | // State is ignored for unrelated events. 386 | template 387 | void execute_event([[maybe_unused]] StateEnum to_from_state, 388 | hfsm_t& machine, FuncArgs... func_args) { 389 | static_assert(Event != hfsm_event::on_enter_from, 390 | "state : do not execute on_enter_from, use on_enter instead"); 391 | static_assert(Event != hfsm_event::on_exit_to, 392 | "state : do not execute on_exit_to, use on_exit instead"); 393 | 394 | if constexpr (Event == hfsm_event::on_enter) { 395 | if (to_from_state != StateEnum::count 396 | && _enter_from_exists[size_t(to_from_state)]) { 397 | std::invoke(_enter_from_events[size_t(to_from_state)], machine, 398 | func_args...); 399 | } else if (std::get(_simple_event_exists)) { 400 | std::invoke(std::get(_simple_events), machine, 401 | func_args...); 402 | } 403 | } else if constexpr (Event == hfsm_event::on_exit) { 404 | if (to_from_state != StateEnum::count 405 | && _exit_to_exists[size_t(to_from_state)]) { 406 | std::invoke(_exit_to_events[size_t(to_from_state)], machine, 407 | func_args...); 408 | } else if (std::get(_simple_event_exists)) { 409 | std::invoke(std::get(_simple_events), machine, 410 | func_args...); 411 | } 412 | 413 | } else if constexpr (Event == hfsm_event::on_update) { 414 | if (std::get(_simple_event_exists)) { 415 | std::invoke(std::get(_simple_events), machine, 416 | func_args...); 417 | } 418 | } 419 | } 420 | 421 | const auto& auto_transition_guards() const { 422 | return _auto_transition_guards; 423 | } 424 | 425 | void enable_parent_update() { 426 | _parent_update = true; 427 | } 428 | bool parent_update_enabled() const { 429 | return _parent_update; 430 | } 431 | 432 | template 433 | bool handles_event( 434 | [[maybe_unused]] StateEnum to_from_state = StateEnum::count) const { 435 | if constexpr (Event == hfsm_event::on_enter_from) { 436 | if (to_from_state == StateEnum::count) 437 | return false; 438 | 439 | return _enter_from_exists[size_t(to_from_state)]; 440 | } else if constexpr (Event == hfsm_event::on_exit_to) { 441 | if (to_from_state == StateEnum::count) { 442 | return false; 443 | } 444 | return _exit_to_exists[size_t(to_from_state)]; 445 | } else { 446 | return std::get(_simple_event_exists); 447 | } 448 | } 449 | 450 | bool enter_from_calls_on_enter(StateEnum from) const { 451 | if (from == StateEnum::count) 452 | return false; 453 | 454 | return _enter_from_calls_on_enter[size_t(from)] 455 | && std::get(_simple_event_exists); 456 | } 457 | bool exit_to_calls_on_exit(StateEnum to) const { 458 | if (to == StateEnum::count) 459 | return false; 460 | 461 | return _exit_to_calls_on_exit[size_t(to)] 462 | && std::get(_simple_event_exists); 463 | } 464 | 465 | StateEnum state() const { 466 | return _state; 467 | } 468 | 469 | std::string_view name() const { 470 | return { _name }; 471 | } 472 | 473 | void current_states(std::vector& states, 474 | bool depth_first = false) const { 475 | if (depth_first) { 476 | if (_current_substate != StateEnum::count) { 477 | current_substate().current_states(states, depth_first); 478 | } 479 | states.push_back(this); 480 | } else { 481 | states.push_back(this); 482 | if (_current_substate != StateEnum::count) { 483 | current_substate().current_states(states, depth_first); 484 | } 485 | } 486 | } 487 | 488 | void current_states( 489 | std::vector& states, bool depth_first = false) { 490 | if (depth_first) { 491 | if (_current_substate != StateEnum::count) { 492 | current_substate().current_states(states); 493 | } 494 | states.push_back(this); 495 | } else { 496 | states.push_back(this); 497 | if (_current_substate != StateEnum::count) { 498 | current_substate().current_states(states); 499 | } 500 | } 501 | } 502 | 503 | // void parent_state(StateEnum s) { 504 | // assert(s != StateEnum::count); 505 | //} 506 | 507 | void all_states(std::vector& states, 508 | bool depth_first = false) const { 509 | if (depth_first) { 510 | for (const hfsm_state& s : _substates) { 511 | s.all_states(states, depth_first); 512 | } 513 | states.push_back(this); 514 | } else { 515 | states.push_back(this); 516 | for (const hfsm_state& s : _substates) { 517 | s.all_states(states, depth_first); 518 | } 519 | } 520 | } 521 | 522 | void all_states( 523 | std::vector& states, bool depth_first = false) { 524 | if (depth_first) { 525 | for (hfsm_state& s : _substates) { 526 | s.all_states(states, depth_first); 527 | } 528 | states.push_back(this); 529 | } else { 530 | states.push_back(this); 531 | for (hfsm_state& s : _substates) { 532 | s.all_states(states, depth_first); 533 | } 534 | } 535 | } 536 | 537 | void default_states(std::vector& states, 538 | bool depth_first = false) const { 539 | if (depth_first) { 540 | if (_default_substate != StateEnum::count) { 541 | _substates[_substate_indexes[size_t(_default_substate)]] 542 | .default_states(states, depth_first); 543 | } 544 | states.push_back(this); 545 | } else { 546 | states.push_back(this); 547 | if (_default_substate != StateEnum::count) { 548 | _substates[_substate_indexes[size_t(_default_substate)]] 549 | .default_states(states, depth_first); 550 | } 551 | } 552 | } 553 | 554 | void default_states( 555 | std::vector& states, bool depth_first = false) { 556 | if (depth_first) { 557 | if (_default_substate != StateEnum::count) { 558 | _substates[_substate_indexes[size_t(_default_substate)]] 559 | .default_states(states, depth_first); 560 | } 561 | states.push_back(this); 562 | } else { 563 | states.push_back(this); 564 | if (_default_substate != StateEnum::count) { 565 | _substates[_substate_indexes[size_t(_default_substate)]] 566 | .default_states(states, depth_first); 567 | } 568 | } 569 | } 570 | 571 | const hfsm_state* substate(StateEnum s) const { 572 | if (_substate_indexes[size_t(s)] 573 | == std::numeric_limits::max()) { 574 | throw std::invalid_argument{ 575 | "state : trying to access invalid state" 576 | }; 577 | } 578 | return &_substates[_substate_indexes[size_t(s)]]; 579 | } 580 | hfsm_state* substate(StateEnum s) { 581 | return const_cast( 582 | static_cast(this)->substate(s)); 583 | } 584 | 585 | void current_substate(StateEnum s) { 586 | if (_substate_indexes[size_t(s)] 587 | == std::numeric_limits::max()) { 588 | throw std::invalid_argument{ 589 | "state : trying to access invalid state" 590 | }; 591 | } 592 | _current_substate = s; 593 | } 594 | 595 | private: 596 | const hfsm_state& current_substate() const { 597 | return _substates[_substate_indexes[size_t(_current_substate)]]; 598 | } 599 | hfsm_state& current_substate() { 600 | return _substates[_substate_indexes[size_t(_current_substate)]]; 601 | } 602 | 603 | StateEnum _state; 604 | StateEnum _current_substate{ StateEnum::count }; 605 | StateEnum _default_substate{ StateEnum::count }; 606 | const char* _name; 607 | 608 | std::array 609 | _simple_events{}; 610 | std::array 611 | _simple_event_exists{}; 612 | 613 | std::array _enter_from_events{}; 614 | std::array _enter_from_exists{}; 615 | std::array _enter_from_calls_on_enter{}; 616 | 617 | std::array _exit_to_events{}; 618 | std::array _exit_to_exists{}; 619 | std::array _exit_to_calls_on_exit{}; 620 | 621 | std::array _transitions{}; 622 | std::array _transition_exists{}; 623 | 624 | std::array>, 625 | size_t(TransitionEnum::count)> 626 | _guard_transitions{}; 627 | std::array _guard_transition_exists{}; 628 | 629 | std::array, size_t(TransitionEnum::count)> 630 | _auto_transition_guards{}; 631 | 632 | std::array _is_yield_transition{}; 633 | 634 | std::vector _substates; 635 | std::array _substate_indexes; 636 | 637 | bool _parent_update{ false }; 638 | 639 | using transition_underlying_t = 640 | typename std::underlying_type_t; 641 | using state_underlying_t = typename std::underlying_type_t; 642 | 643 | static_assert(std::is_enum_v, 644 | "state : template parameter TransitionEnum must be enum class"); 645 | static_assert(std::is_enum_v, 646 | "state : template parameter StateEnum must be enum class"); 647 | 648 | static_assert(std::is_unsigned_v, 649 | "state : TransitionEnum underlying type must be unsigned"); 650 | static_assert(std::is_unsigned_v, 651 | "state : StateEnum underlying type must be unsigned"); 652 | 653 | static_assert(size_t(TransitionEnum::count) != 0, 654 | "state : TransitionEnum must declare count and count must not " 655 | "be " 656 | "0"); 657 | static_assert(size_t(StateEnum::count) != 0, 658 | "state : StateEnum must declare count and count must not be 0"); 659 | }; 660 | 661 | template 662 | struct hfsm { 663 | using state_t = hfsm_state; 664 | using hfsm_func_t = typename state_t::hfsm_func_t; 665 | using auto_t_guard_arr 666 | = std::array, 667 | size_t(TransitionEnum::count)>; 668 | 669 | hfsm() { 670 | _state_indexes.fill(std::numeric_limits::max()); 671 | _state_names.resize(size_t(StateEnum::count), ""); 672 | _transition_names.fill(nullptr); 673 | _state_topmost_parents.fill(StateEnum::count); 674 | } 675 | 676 | template 677 | void add_state(state_t&& state) { 678 | if (std::get(_state_indexes) 679 | != std::numeric_limits::max()) { 680 | throw std::invalid_argument{ "hfsm : state already exists" }; 681 | } 682 | 683 | if (State != state.state()) { 684 | throw std::invalid_argument{ 685 | "hfsm : misconfigured state, state enum mismatch" 686 | }; 687 | } 688 | 689 | std::get(_state_indexes) = _states.size(); 690 | _states.push_back(std::move(state)); 691 | 692 | std::vector states; 693 | _states.back().all_states(states); 694 | for (const state_t* s : states) { 695 | _state_names[size_t(s->state())] = s->name(); 696 | // if (s->state() != State) { 697 | _state_topmost_parents[size_t(s->state())] = State; 698 | //} 699 | } 700 | 701 | if (_default_state == StateEnum::count) { 702 | _default_state = State; 703 | } 704 | } 705 | 706 | void add_parallel_hfsm(hfsm&& machine) { 707 | _parallel_machines.push_back(std::move(machine)); 708 | } 709 | 710 | // By default, uses the first added state as the default substate. 711 | template 712 | void add_default_state() { 713 | _default_state = State; 714 | } 715 | 716 | // Optional, will use provided transition names when printing. 717 | void add_transition_names( 718 | std::array names) { 719 | _transition_names = std::move(names); 720 | } 721 | 722 | template 723 | void trigger(FuncArgs... func_args) { 724 | static_assert(Transition != TransitionEnum::count, 725 | "hfsm : invalid transition"); 726 | auto g = detail::on_exit{ [this]() { _in_transition_guard = false; } }; 727 | 728 | maybe_init(func_args...); 729 | 730 | _current_tranny_info = {}; 731 | current_state().template transition( 732 | _current_tranny_info, func_args...); 733 | 734 | if (_current_tranny_info.to == StateEnum::count 735 | && !_current_tranny_info.yield) { 736 | throw std::invalid_argument{ 737 | "hfsm : current state doesn't handle transition" 738 | }; 739 | } 740 | 741 | if (_print) { 742 | if (_transition_names[size_t(Transition)] != nullptr) { 743 | if (_in_transition_guard) { 744 | printf("--- %s%s ---\n", "transition guard triggered : ", 745 | _transition_names[size_t(Transition)]); 746 | } else { 747 | printf("\n--- %s%s ---\n", "triggered : ", 748 | _transition_names[size_t(Transition)]); 749 | } 750 | } else { 751 | if (_in_transition_guard) { 752 | printf("--- %s%zu ---\n", "transition guard triggered : ", 753 | size_t(Transition)); 754 | } else { 755 | printf("\n--- %s%zu ---\n", 756 | "triggered : ", size_t(Transition)); 757 | } 758 | } 759 | } 760 | _transition_to_handle = Transition; 761 | 762 | for (auto& sub : _parallel_machines) { 763 | sub._in_transition_guard = _in_transition_guard; 764 | // auto subg = sg::make_scope_guard( 765 | // [&]() { sub._in_transition_guard = false; }); 766 | 767 | sub.template trigger(func_args...); 768 | } 769 | } 770 | 771 | void update(FuncArgs... func_args) { 772 | maybe_init(func_args...); 773 | 774 | if (_print) { 775 | if (_in_parallel) 776 | printf("\n--- parallel update ---\n"); 777 | else 778 | printf("\n--- update ---\n"); 779 | } 780 | 781 | std::vector states; 782 | current_state().current_states(states, true); 783 | 784 | std::vector update_states; 785 | 786 | // gather valid states 787 | for (size_t i = 0; i < states.size(); ++i) { 788 | update_states.push_back(states[i]); 789 | if (!states[i]->parent_update_enabled()) { 790 | break; 791 | } 792 | } 793 | std::reverse(update_states.begin(), update_states.end()); 794 | 795 | std::vector update_events; 796 | enqueue_update(update_events, update_states); 797 | execute_events(update_events, func_args...); 798 | 799 | //_in_parallel = true; 800 | // auto g = sg::make_scope_guard([this]() { _in_parallel = false; }); 801 | 802 | for (auto& sub : _parallel_machines) { 803 | sub._in_parallel = true; 804 | auto g = detail::on_exit{ [&]() { sub._in_parallel = false; } }; 805 | sub.update(func_args...); 806 | } 807 | } 808 | 809 | std::string_view state_name(StateEnum s) const { 810 | return _state_names[size_t(s)]; 811 | } 812 | 813 | const std::vector& state_names() const { 814 | return _state_names; 815 | } 816 | 817 | StateEnum current_state() const { 818 | return _current_state; 819 | } 820 | 821 | // Also enables print on parallel machines. 822 | void enable_print() { 823 | _print = true; 824 | for (auto& sub : _parallel_machines) { 825 | sub.enable_print(); 826 | } 827 | } 828 | void disable_print() { 829 | _print = false; 830 | for (auto& sub : _parallel_machines) { 831 | sub.enable_print(); 832 | } 833 | } 834 | 835 | private: 836 | void enqueue_enter(std::vector& events, 837 | const std::vector& states, StateEnum to_from_state) { 838 | 839 | for (state_t* s : states) { 840 | bool call_generalized = s->enter_from_calls_on_enter(to_from_state); 841 | if (call_generalized) { 842 | events.push_back([s, this](hfsm&, FuncArgs... func_args) { 843 | _indentation += indentation_size; 844 | maybe_print(hfsm_event::on_enter, s); 845 | 846 | s->template execute_event( 847 | StateEnum::count, *this, func_args...); 848 | }); 849 | } 850 | 851 | events.push_back([s, to_from_state, indent = !call_generalized, 852 | this](hfsm&, FuncArgs... func_args) { 853 | if (indent) { 854 | _indentation += indentation_size; 855 | } 856 | maybe_print(hfsm_event::on_enter, s, to_from_state); 857 | s->template execute_event( 858 | to_from_state, *this, func_args...); 859 | }); 860 | } 861 | } 862 | 863 | void enqueue_update(std::vector& events, 864 | const std::vector& states) { 865 | for (state_t* s : states) { 866 | events.push_back([s, this](hfsm&, FuncArgs... func_args) { 867 | execute_auto_transition_guards( 868 | s->auto_transition_guards(), func_args...); 869 | 870 | if (_transition_to_handle != TransitionEnum::count) { 871 | return; 872 | } 873 | 874 | maybe_print(hfsm_event::on_update, s); 875 | s->template execute_event( 876 | StateEnum::count, *this, func_args...); 877 | }); 878 | } 879 | } 880 | 881 | void enqueue_exit(std::vector& events, 882 | const std::vector& states, StateEnum to_from_state) { 883 | 884 | for (state_t* s : states) { 885 | bool call_generalized = s->exit_to_calls_on_exit(to_from_state); 886 | if (call_generalized) { 887 | events.push_back([s, this](hfsm&, FuncArgs... func_args) { 888 | maybe_print(hfsm_event::on_exit, s); 889 | 890 | s->template execute_event( 891 | StateEnum::count, *this, func_args...); 892 | }); 893 | } 894 | 895 | events.push_back( 896 | [s, to_from_state, this](hfsm&, FuncArgs... func_args) { 897 | maybe_print(hfsm_event::on_exit, s, to_from_state); 898 | 899 | s->template execute_event( 900 | to_from_state, *this, func_args...); 901 | 902 | if (_transition_to_handle == TransitionEnum::count) { 903 | _indentation -= indentation_size; 904 | } 905 | }); 906 | } 907 | } 908 | 909 | void execute_auto_transition_guards( 910 | const auto_t_guard_arr& t_guards, FuncArgs... func_args) { 911 | bool found = false; 912 | 913 | detail::static_for([&](auto idx) { 914 | if (found) 915 | return; 916 | 917 | constexpr size_t c_idx = decltype(idx)::value; 918 | for (const auto& func : std::get(t_guards)) { 919 | if (std::invoke(func, func_args...)) { 920 | found = true; 921 | // constexpr TransitionEnum t 922 | // = static_cast(decltype(idx)::value); 923 | _in_transition_guard = true; 924 | trigger(func_args...); 925 | break; 926 | } 927 | } 928 | }); 929 | } 930 | 931 | void execute_events( 932 | std::vector& update_events, FuncArgs... func_args) { 933 | 934 | for (size_t i = 0; i < update_events.size(); ++i) { 935 | std::invoke(update_events[i], *this, func_args...); 936 | 937 | if (_transition_to_handle == TransitionEnum::count) 938 | continue; 939 | 940 | _transition_to_handle = TransitionEnum::count; 941 | update_events.erase( 942 | update_events.begin() + i + 1, update_events.end()); 943 | 944 | if (_current_tranny_info.internal_transition) { 945 | state_t* parent = _current_tranny_info.exit_hierarchy.back(); 946 | _current_tranny_info.exit_hierarchy.pop_back(); 947 | 948 | enqueue_exit(update_events, _current_tranny_info.exit_hierarchy, 949 | _current_tranny_info.to); 950 | 951 | state_t* child = parent->substate(_current_tranny_info.to); 952 | 953 | update_events.push_back( 954 | [parent, to_state = _current_tranny_info.to]( 955 | hfsm&, FuncArgs...) { 956 | parent->current_substate(to_state); 957 | }); 958 | 959 | std::vector enter_states{ child }; 960 | enqueue_enter( 961 | update_events, enter_states, _current_tranny_info.from); 962 | 963 | continue; 964 | } 965 | 966 | if (_current_tranny_info.yield) { 967 | _current_tranny_info.to = _history_state; 968 | } 969 | 970 | std::vector exit_states; 971 | current_state().current_states(exit_states, true); 972 | enqueue_exit(update_events, exit_states, _current_tranny_info.to); 973 | 974 | update_events.push_back([to_state = _current_tranny_info.to, this]( 975 | hfsm&, FuncArgs...) { 976 | current_state(_state_topmost_parents[size_t(to_state)]); 977 | }); 978 | 979 | 980 | std::vector enter_states; 981 | topmost_state(_current_tranny_info.to).default_states(enter_states); 982 | enqueue_enter( 983 | update_events, enter_states, _current_tranny_info.from); 984 | } 985 | } 986 | 987 | void maybe_init(FuncArgs... func_args) { 988 | assert(_states.size() != 0 && "hfsm : did you forget to add states?"); 989 | 990 | if (_current_state != StateEnum::count) 991 | return; 992 | 993 | // Pretty heave, only run in debug 994 | if constexpr (detail::debug_build) { 995 | // just check the first init 996 | if (!_in_parallel) { 997 | std::vector names = _state_names; 998 | 999 | if (_parallel_machines.size() != 0) { 1000 | for (const auto& machine : _parallel_machines) { 1001 | names.insert(names.end(), machine.state_names().begin(), 1002 | machine.state_names().end()); 1003 | } 1004 | 1005 | names.erase(std::remove_if(names.begin(), names.end(), 1006 | [](const std::string_view& str) { 1007 | return str == ""; 1008 | }), 1009 | names.end()); 1010 | assert(names.size() == size_t(StateEnum::count)); 1011 | } 1012 | 1013 | for (const auto& name : names) { 1014 | if (name == "") { 1015 | throw std::invalid_argument{ "hfsm : missing states" }; 1016 | } 1017 | 1018 | size_t num_name = 0; 1019 | std::for_each( 1020 | names.begin(), names.end(), [&](const auto& n) { 1021 | if (n == name) 1022 | ++num_name; 1023 | }); 1024 | assert(num_name == 1 1025 | && "hfsm : states have duplicate names"); 1026 | } 1027 | } 1028 | } 1029 | 1030 | if (_print) { 1031 | if (_in_parallel) 1032 | printf("\n--- parallel init ---\n"); 1033 | else 1034 | printf("\n--- init ---\n"); 1035 | } 1036 | 1037 | current_state(_default_state); 1038 | current_state().init(); 1039 | std::vector enter_states; 1040 | current_state().current_states(enter_states); 1041 | 1042 | std::vector init_events; 1043 | enqueue_enter(init_events, enter_states, StateEnum::count); 1044 | execute_events(init_events, func_args...); 1045 | } 1046 | 1047 | void maybe_print(hfsm_event ev, const state_t* from, 1048 | StateEnum to = StateEnum::count) { 1049 | if (!_print) 1050 | return; 1051 | 1052 | assert(_indentation >= 0); 1053 | 1054 | const char* ev_name = nullptr; 1055 | 1056 | if (ev == hfsm_event::on_update) { 1057 | if (!from->template handles_event()) 1058 | return; 1059 | 1060 | ev_name = "on_update"; 1061 | printf("%*s%s : %s\n", _indentation, "", from->name().data(), 1062 | ev_name); 1063 | } else if (ev == hfsm_event::on_enter 1064 | || ev == hfsm_event::on_enter_from) { 1065 | if (from->template handles_event(to)) { 1066 | ev_name = "on_enter_from"; 1067 | printf("%*s%s : %s : %s\n", _indentation, "", 1068 | from->name().data(), ev_name, state_name(to).data()); 1069 | } else if (from->template handles_event()) { 1070 | ev_name = "on_enter"; 1071 | printf("%*s%s : %s\n", _indentation, "", from->name().data(), 1072 | ev_name); 1073 | } 1074 | } else if (ev == hfsm_event::on_exit || ev == hfsm_event::on_exit_to) { 1075 | if (from->template handles_event(to)) { 1076 | ev_name = "on_exit_to"; 1077 | printf("%*s%s : %s : %s\n", _indentation, "", 1078 | from->name().data(), ev_name, state_name(to).data()); 1079 | } else if (from->template handles_event()) { 1080 | ev_name = "on_exit"; 1081 | printf("%*s%s : %s\n", _indentation, "", from->name().data(), 1082 | ev_name); 1083 | } 1084 | } 1085 | } 1086 | 1087 | state_t& current_state() { 1088 | size_t idx = _state_indexes[size_t(_current_state)]; 1089 | assert(idx != std::numeric_limits::max()); 1090 | 1091 | return _states[idx]; 1092 | } 1093 | void current_state(StateEnum state) { 1094 | _history_state = _current_state; 1095 | _current_state = state; 1096 | current_state().init(); 1097 | } 1098 | 1099 | state_t& topmost_state(StateEnum state) { 1100 | StateEnum topmost = _state_topmost_parents[size_t(state)]; 1101 | assert(topmost != StateEnum::count); 1102 | 1103 | size_t idx = _state_indexes[size_t(topmost)]; 1104 | assert(idx != std::numeric_limits::max()); 1105 | 1106 | return _states[idx]; 1107 | } 1108 | 1109 | StateEnum _current_state{ StateEnum::count }; 1110 | StateEnum _history_state{ StateEnum::count }; 1111 | StateEnum _default_state{ StateEnum::count }; 1112 | TransitionEnum _transition_to_handle{ TransitionEnum::count }; 1113 | typename state_t::tranny_info _current_tranny_info; 1114 | 1115 | std::vector _states{}; 1116 | std::vector _state_names{}; 1117 | std::array _state_indexes; 1118 | std::array _state_topmost_parents; 1119 | std::array _transition_names; 1120 | 1121 | bool _print{ false }; 1122 | 1123 | int indentation_size = 4; 1124 | int _indentation{ -indentation_size }; 1125 | 1126 | // required for msvc fuckup 1127 | bool _in_parallel{ false }; 1128 | bool _in_transition_guard{ false }; 1129 | 1130 | std::vector _parallel_machines; 1131 | }; 1132 | 1133 | } // namespace fea 1134 | --------------------------------------------------------------------------------