Skip to content

back11: Dropped transition depending on transition table row order #252

Description

@praetorian20

Hi,
I have a state machine that transitions from state s0 -> s2 upon receipt of one or more events, but there are two possible paths for reaching s2. If the context contains a set flag, the transition order must be s0 -> s1 -> s2 - in this case 2 identical events are received, each must trigger a state transition. If the context flag is not set, a single event will be received, which must cause a transition from s0 -> s2 in a single step.

I've found I can only get this to work if I define the transition_table rows in a specific order, the unconstrained s1 -> s2 transition row must be defined prior to the constrained rows that define s0 -> s1 and s1 -> s2. Here's the example code in compiler explorer (I'll also paste it below): https://godbolt.org/z/8h7P6dG6o I've tested this with Boost 1.91 also, result is the same.

Is there a way to make it work with the s1 -> s2 row defined last? I find that ordering to be more intuitive than the one that works.

#include <boost/msm/back11/state_machine.hpp>
#include <boost/msm/front/state_machine_def.hpp>
#include <boost/msm/front/euml/common.hpp>
#include <boost/msm/front/euml/operator.hpp>
#include <boost/mpl/vector.hpp>
#include <boost/version.hpp>

#include <cstdio>
#include <cstdlib>

struct go_event {};

struct context
{
   bool flagged = true;
};

struct is_flagged
{
   template<typename Event, typename Fsm, typename SourceState, typename TargetState>
   bool operator()(Event const&, Fsm& fsm, SourceState&, TargetState&)
   {
      return fsm.ctx.flagged;
   }
};

using not_flagged = boost::msm::front::euml::Not_<is_flagged>;

struct s0 : boost::msm::front::state<> {};
struct s1 : boost::msm::front::state<> {};
struct s2 : boost::msm::front::state<> {};

using none = boost::msm::front::none;

// Working case: the s1 -> s2 unconditional transition is declared first
struct working_fsm_ : boost::msm::front::state_machine_def<working_fsm_>
{
   context ctx;
   bool    no_transition_fired = false;

   using initial_state = s0;

   template<typename Start, typename Event, typename Next, typename Action, typename Guard>
   using row = boost::msm::front::Row<Start, Event, Next, Action, Guard>;

   struct transition_table
   : boost::mpl::vector<
           row<s1, go_event, s2, none, none>,
           row<s0, go_event, s2, none, not_flagged>,
           row<s0, go_event, s1, none, is_flagged>
   >
   {};

   template<typename Event, typename Fsm>
   void no_transition(Event const&, Fsm&, int)
   {
      no_transition_fired = true;
   }
};
using working_fsm = boost::msm::back11::state_machine<working_fsm_>;

// Broken case: same as working, except s1 -> s2 transition is declared last,
// which is the more intuitive ordering
struct broken_fsm_ : boost::msm::front::state_machine_def<broken_fsm_>
{
   context ctx;
   bool    no_transition_fired = false;

   using initial_state = s0;

   template<typename Start, typename Event, typename Next, typename Action, typename Guard>
   using row = boost::msm::front::Row<Start, Event, Next, Action, Guard>;

   struct transition_table
   : boost::mpl::vector<
           row<s0, go_event, s2, none, not_flagged>,
           row<s0, go_event, s1, none, is_flagged>,
           row<s1, go_event, s2, none, none>
   >
   {};

   template<typename Event, typename Fsm>
   void no_transition(Event const&, Fsm&, int)
   {
      no_transition_fired = true;
   }
};
using broken_fsm = boost::msm::back11::state_machine<broken_fsm_>;

template<typename Fsm>
int current_state_id(Fsm const& fsm)
{
   return *fsm.current_state();
}

int main()
{
   std::printf("Boost version: %s\n", BOOST_LIB_VERSION);

   // working_fsm: s0 -[flagged]-> s1 -> s2
   {
      working_fsm fsm;
      fsm.start();
      fsm.process_event(go_event{});  // expect s0 -> s1
      int after_first = current_state_id(fsm);
      fsm.process_event(go_event{});  // expect s1 -> s2
      int after_second = current_state_id(fsm);

      std::printf(
            "[working] after 1st event: state_id=%d after 2nd event: state_id=%d "
            "no_transition_fired=%d\n",
            after_first,
            after_second,
            fsm.no_transition_fired);

      if(fsm.no_transition_fired)
      {
         std::printf("[working] unexpected fail\n");
      }
   }

   // --- broken_fsm: s0 -[flagged]-> s1 -> s1
   {
      broken_fsm fsm;
      fsm.start();
      fsm.process_event(go_event{});  // expect s0 -> s1
      int after_first = current_state_id(fsm);
      fsm.process_event(go_event{});  // expect s1 -> s2, but no_transition() is called
      int after_second = current_state_id(fsm);

      std::printf(
            "[broken] after 1st event: state_id=%d after 2nd event: state_id=%d "
            "no_transition_fired=%d\n",
            after_first,
            after_second,
            fsm.no_transition_fired);

      if(!fsm.no_transition_fired)
      {
         std::printf("[broken] Unexpected pass\n");
      }
      else
      {
         std::printf(
               "[broken] Expected fail: transition from s1 -> s2 didn't happen\n");
      }
   }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions