erikzenker / hsm

Finite state machine library based on the boost hana meta programming library. It follows the principles of the boost msm and boost sml libraries, but tries to reduce own complex meta programming code to a minimum.
MIT License
187 stars 18 forks source link

[BUG] on_exit never call for initial state #188

Open dfleury2 opened 9 months ago

dfleury2 commented 9 months ago

Hi, Describe the bug The on_exit is never called for the initial state neither in the main state nor a substate. But the on_entry is sometimes call.

To Reproduce

    #include <hsm/hsm.h>

    #include <iostream>

    #define ON_ENTRY(STATE)                                                                                                                   \
        static constexpr auto on_entry()                                                                                                      \
        {                                                                                                                                     \
            return [](const auto& event, const auto& source, const auto& target) { std::cout << "      -- ENTRY: " << #STATE << std::endl; }; \
        }

    #define ON_EXIT(STATE)                                                                                                                   \
        static constexpr auto on_exit()                                                                                                      \
        {                                                                                                                                    \
            return [](const auto& event, const auto& source, const auto& target) { std::cout << "      -- EXIT: " << #STATE << std::endl; }; \
        }

    #define ON(STATE)   \
        ON_ENTRY(STATE) \
        ON_EXIT(STATE)

    namespace ee {
    struct Idle {
        ON(Idle);
    };

    struct A0 {
        ON(A0);
    };
    // --------------------------------------------------------------------------
    // States
    struct A1 {
        ON(A1);
    };

    struct A2 {
        ON(A2);
    };

    // --------------------------------------------------------------------------
    // Events
    struct press {};
    struct start {};
    struct stop {};

    // --------------------------------------------------------------------------
    // Guard
    const auto success = [](auto /*event*/, auto /*source*/, auto /*target*/) { return true; };

    // --------------------------------------------------------------------------
    // Actions
    const auto log = [](auto event, auto source, auto target, const char* msg = "") {
        std::cout << msg << typeid(source).name() << " + " << typeid(event).name() << " = " << typeid(target).name() << std::endl;
    };

    // --------------------------------------------------------------------------
    // State machines
    struct SubState {
        static constexpr auto make_transition_table()
        {
            // clang-format off
                return hsm::transition_table(
                    // Source              + Event            [Guard]   / Action  = Target
                    // +-------------------+------------------+---------+---------+----------------------+
                    * hsm::state<A0>       + hsm::event<press>          / log     = hsm::state<A1>,
                      hsm::state<A1>       + hsm::event<press>          / log     = hsm::state<A2>,
                      hsm::state<A2>       + hsm::event<press>          / log     = hsm::state<A0>
                    );

            // clang-format on
        }

        ON(SubState);

        static constexpr auto on_unexpected_event()
        {
            return [](auto& event, const auto& state) { log(event, state, state, "unexpected event: "); };
        }
    };

    struct Initial {

        static constexpr auto make_transition_table()
        {
            // clang-format off
                return hsm::transition_table(
                    // Source              + Event            [Guard]   / Action  = Target
                    // +-------------------+------------------+---------+---------+----------------------+
                    * hsm::state<Idle>       + hsm::event<start>          / log     = hsm::state<SubState>,
                      hsm::state<SubState>   + hsm::event<stop>           / log     = hsm::state<Idle>
                    );

            // clang-format on
        }

        ON(Initial);

        static constexpr auto on_unexpected_event()
        {
            return [](auto& event, const auto& state) { log(event, state, state, "unexpected event: "); };
        }
    };

    }   // namespace ee

    int main()
    {
        std::cout << "------------------------------------------- Initial" << std::endl;
        {
            hsm::sm<ee::Initial> fsm;

            fsm.process_event(ee::start{});

            for (int i = 0; i < 4; ++i) {
                std::cout << "----- process_event(press) ----- " << std::endl;
                fsm.process_event(ee::press{});
            }

            fsm.process_event(ee::stop{});
        }

        std::cout << "------------------------------------------- SubState" << std::endl;
        {
            hsm::sm<ee::SubState> fsm;

            for (int i = 0; i < 4; ++i) {
                std::cout << "----- process_event(press) ----- " << std::endl;
                fsm.process_event(ee::press{});
            }
        }
    }

Expected behavior On_exit called every time a state is exited (at least when the on_entry was called)

Additional context

output:

------------------------------------------- Initial (with a substate) struct ee::Idle + struct ee::start = struct ee::A0 -- ENTRY: SubState -- ENTRY: A0 ----- process_event(press) ----- here expecting EXIT: A0

struct ee::A0 + struct ee::press = struct ee::A1 -- ENTRY: A1 ----- process_event(press) ----- -- EXIT: A1 struct ee::A1 + struct ee::press = struct ee::A2 -- ENTRY: A2 ----- process_event(press) ----- -- EXIT: A2 struct ee::A2 + struct ee::press = struct ee::A0 -- ENTRY: A0 ----- process_event(press) -----

here expecting EXIT: A0

struct ee::A0 + struct ee::press = struct ee::A1 -- ENTRY: A1 -- EXIT: SubState
__<- here the sate on_exit is called which is great struct ee::SubState + struct ee::stop = struct ee::Idle -- ENTRY: Idle
<- here ENTRY is called but was not called at the start, nor the EXIT is called__

------------------------------------------- SubState ----- process_event(press) ----- here, may be EXIT expected, may by ENTRY too for A0 struct ee::A0 + struct ee::press = struct ee::A1 -- ENTRY: A1 ----- process_event(press) ----- -- EXIT: A1 struct ee::A1 + struct ee::press = struct ee::A2 -- ENTRY: A2 ----- process_event(press) ----- -- EXIT: A2 struct ee::A2 + struct ee::press = struct ee::A0 -- ENTRY: A0 ----- process_event(press) ----- here ENTRY: A0 was called, but not EXIT struct ee::A0 + struct ee::press = struct ee::A1 -- ENTRY: A1

Unfortunately, the code itself is beeyond my template/boost.Hana knowledge, If I have some tips to find the issue or change the behavior, may be I can help.

Regards,

dfleury2 commented 9 months ago

After some investigations, I found something that can help (at least for on_exit), in resolve_state.h, function resolveExitAction,

    if constexpr (transition.internal()) {
        return [](auto&&...) {};
    } else if constexpr (has_exit_action(transition.source())) {
        return get_exit_action(transition.source());
    }
    // -----------------------
    // Adding a check for initial state and underlying source state
    else if constexpr (is_initial_state(transition.source()) && has_exit_action(resolveSrc(transition))) {
        return get_exit_action(resolveSrc(transition));
    }
    // -----------------------
    else if constexpr (hasPseudoExitAction) {
        return get_exit_action(resolveSrcParent(transition));
    } else {
        return [](auto&&...) {};
    }

Do you have any opinion about this ?

[EDIT] a simplification do the job, using resolveSrc on transition instead of transition.source()

        if constexpr (transition.internal()) {
            return [](auto&&...) {};
        } else if constexpr (has_exit_action(resolveSrc(transition))) {
            return get_exit_action(resolveSrc(transition));
        } else if constexpr (has_pseudo_exit_action(transition)) {
            return get_exit_action(resolveSrcParent(transition));
        } else {
            return [](auto&&...) {};
        }