boost-ext / sml

C++14 State Machine library
https://boost-ext.github.io/sml
Boost Software License 1.0
1.1k stars 173 forks source link

No autotransition in sub statemachine #544

Open mechatheo opened 1 year ago

mechatheo commented 1 year ago

Today I wanted to update the Boost::SML library in our codebase, we used version 1.1.0 and I wanted to switch to 1.1.6. We have an extensive unit test suite and one of the tests failed.

We have the following state machine:

image

With a nested Playing machine:

image

All transitions are automatic, no events are needed. So in the unit test, you typically would set the result of the dice throw up front and when the state machine is instantiated, the test expected it to get into one of the final states immediately. So X or Success

This used to be the case with version 1.1.0, now that I'm trying to upgrade to 1.1.6 the endstate is Playing.Initial. This is especially weird to me, as the initial should always auto-transition.

We generate our state machines from the UML, here is the corresponding amalgamated code:

#ifndef SUBSTATEMACHINEEXITPOINT_STATE_MACHINE_HPP
#define SUBSTATEMACHINEEXITPOINT_STATE_MACHINE_HPP

#include <queue>

namespace SubStateMachineExitPoint {

class UserData
{
    friend struct Callbacks;
    friend struct Machine;

public:
    int m_dice{1};
};

struct Callbacks
{
public:
    Callbacks(UserData& userData) : m_data(userData){}
    bool IsEven() { return m_data.m_dice % 2 == 0; }
    UserData& m_data; 
};

namespace _private {
namespace Events {
struct Lose : public boost::sml::utility::id_impl<2147483646>
{
    static auto c_str() { return "Lose";}
};
struct Anonymous : public boost::sml::utility::id_impl<2147483645>
{
    static auto c_str() { return "Anonymous";}
};
} // namespace Events

struct Lose
{
    static auto c_str() { return "Lose"; }
};
} // namespace _private

namespace Playing {

/* states */
struct Initial
{
    static auto c_str() { return "Initial"; }
};

struct ThrowDice
{
    static auto c_str() { return "ThrowDice"; }
};

/* transition table */
struct Machine
{
    static auto c_str() { return "Playing"; }
    explicit Machine(UserData* data) : m_calls(*data) {}

    auto operator()() const
    {
        using namespace boost;
        using namespace boost::sml;
        using namespace Events;
        /* clang-format off */
        return make_transition_table(
            *sml::state<Initial> = sml::state<ThrowDice>,
            sml::state<ThrowDice> [([=](){return !m_calls.IsEven();})] = sml::state<_private::Lose>,
            sml::state<_private::Lose> / sml::process(_private::Events::Lose{}) = sml::X,
            sml::state<ThrowDice> [([=](){return m_calls.IsEven();})] / process(_private::Events::Anonymous{}) = sml::X
        );
        /* clang-format on */
    }

    mutable Callbacks m_calls;
};

} // namespace Playing

/* states */
struct Initial
{
    static auto c_str() { return "Initial"; }
};

struct Success
{
    static auto c_str() { return "Success";}
};

/* transition table */
struct Machine
{
    static auto c_str() { return "SubStateMachineExitPoint"; }

    explicit Machine(UserData* data) : m_calls(*data) {}

    auto operator()() const
    {
        using namespace boost;
        using namespace boost::sml;
        using namespace Events;
        /* clang-format off */
        return make_transition_table(
            sml::state<Playing::Machine> + sml::event<_private::Events::Lose> = sml::X,
            *sml::state<Initial> = sml::state<Playing::Machine>,
            sml::state<Playing::Machine> + sml::event<_private::Events::Anonymous> = sml::state<Success>
        );
        /* clang-format on */
    }

    mutable Callbacks m_calls;
};

template <typename... SMPolicies>
auto MakeMachine(UserData& data) //-> boost::sml::sm<Machine, SMPolicies...>
{
    return boost::sml::sm<Machine, boost::sml::process_queue<std::queue>, SMPolicies...>{
        Machine{&data}, Playing::Machine{&data}};
}

template <typename Logger, typename... SMPolicies>
auto MakeMachine(
    UserData& data,
    Logger& logger) //-> boost::sml::sm<Machine, boost::sml::logger<Logger>, SMPolicies...>
{
    return boost::sml::sm<Machine, boost::sml::process_queue<std::queue>,
                          boost::sml::logger<Logger>, SMPolicies...>{
        Machine{&data}, Playing::Machine{&data}, logger};
}

} // namespace SubStateMachineExitPoint

#endif /* SUBSTATEMACHINEEXITPOINT_STATE_MACHINE_HPP */

the now-failing test then goes like this:

TEST(BoostSML, SubStateMachineExitPoint)
{
    using namespace SubStateMachineExitPoint;
    UserData data;
    data.m_dice = 2;
    auto winner = MakeMachine(data);
    EXPECT_TRUE(winner.is(state<Success>));

    data.m_dice = 1;
    auto looser = MakeMachine(data);
    EXPECT_TRUE(looser.is(sml::X));
}

@krzysztof-jusiak any ideas on that?

Specifications

Rijom commented 1 year ago

Reminds me a bit of this issue I was working on a while ago: https://github.com/boost-ext/sml/pull/385

kpierczy commented 7 months ago

After some digging I think it was introduced with #331 . It can be fixed by removing: transitions_sub<sm<TSM>>::execute specialization for anonymous events. This however restores the guards-call-multiplication behaviour. I'm not sure how to fix it yet.