boostorg / boostorg/msm

back11: Dropped transition depending on transition table row order

Open
#252 2 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
C++
Stars
40
Forks
59
PR merge metrics
No merged PRs in 30d

Description

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.

```c++
#include
#include
#include
#include
#include
#include

#include
#include

struct go_event {};

struct context
{
bool flagged = true;
};

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

using not_flagged = boost::msm::front::euml::Not_;

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
{
context ctx;
bool no_transition_fired = false;

using initial_state = s0;

template
using row = boost::msm::front::Row;

struct transition_table
: boost::mpl::vector<
row,
row,
row
>
{};

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

// 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
{
context ctx;
bool no_transition_fired = false;

using initial_state = s0;

template
using row = boost::msm::front::Row;

struct transition_table
: boost::mpl::vector<
row,
row,
row
>
{};

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

template
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");
}
}
}

```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with boost/msm/back11/state_machine.hpp and the transition_table and Row entry points shown in the reproducer. Build and run the Compiler Explorer example, or an equivalent local case, with both transition-row orders; done means the s1 -> s2 transition reaches s2 in both orders without calling no_transition().

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.