boost-ext / sml

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

What would be the execution for "racing" transition entries? #541

Open wuyuanyi135 opened 1 year ago

wuyuanyi135 commented 1 year ago

I am trying to implement a composite state machine that process a Data event in two stages. Each stage will consume a certain amount of count in the event. If the count exceeds what the current stage needs, it will re-process (defer) the event for next round.

The code is attached below:

#include "boost/sml.hpp"
#include "iostream"
#include <deque>
#include <queue>
using namespace std;
using namespace boost::sml;
struct Data {
  int count{};
};
struct Stop {};
struct Idle {};
struct SubSM {
  struct Stage1 {};
  struct Stage2 {};
  int stage1_cnt{};
  int stage2_cnt{};

  bool stage1_guard() {
    cout << "stage 1 check\n";
    return stage1_cnt == 0;
  }

  bool stage2_guard() {
    cout << "stage 2 check\n";
    return stage2_cnt == 0;
  }

  void init() {
    stage1_cnt = 10;
    stage2_cnt = 30;
  }
  void process_stage1(const Data &evt, back::defer<Data> defer_event) {
    cout << "In stage 1, Received " << evt.count << "\n";
    if (stage1_cnt >= evt.count  ) {
      stage1_cnt -= evt.count;
    } else {
      cout << "In stage 1, reprocess " << evt.count - stage1_cnt << "\n";
      defer_event(Data{evt.count - stage1_cnt});
      stage1_cnt = 0;
    }
  }

  void process_stage2(const Data &evt, back::defer<Data> defer_event) {
    cout << "In stage 2, Received " << evt.count << "\n";
    if (stage2_cnt >= evt.count  ) {
      stage2_cnt -= evt.count;
    } else {
      cout << "In stage 2, reprocess " << evt.count - stage2_cnt << "\n";
      defer_event(Data{evt.count - stage2_cnt});
      stage2_cnt = 0;
    }
  }

  auto operator()() {
    return make_transition_table(
            // clang-format off
             state<Stage1>[&SubSM::stage1_guard] / []{cout << "Transiting to stage 2\n"; } = state<Stage2>
            ,state<Stage1> + on_entry<_> / &SubSM::init,
            *state<Stage1> + event<Data> / &SubSM::process_stage1
            ,state<Stage2>[&SubSM::stage2_guard] / []{cout << "Leaving stage 2\n"; } = state<Stage1>
            ,state<Stage2> + event<Data>[!wrap(&SubSM::stage2_guard)]/ &SubSM::process_stage2
//          ,state<Stage2> + event<Data> / &SubSM::process_stage2
            // clang-format on
    );
  }
};
struct CompositeSM {
  auto operator()() {
    return make_transition_table(
            // clang-format off
       *state<Idle> + event<Data> / defer = state<SubSM>
       ,state<Data> + event<Stop> = state<Idle>
            // clang-format on
    );
  }
};

int main() {

  SubSM sub_sm;
  CompositeSM composite_sm;

  sm<CompositeSM, defer_queue<std::deque>, process_queue<std::queue>> sm{sub_sm, composite_sm};

  sm.process_event(Data{199});
  return 0;
}

My problem is that, at line 62, if I use ,state<Stage2> + event<Data> / &SubSM::process_stage2 instead of the guarded one ,state<Stage2> + event<Data>[!wrap(&SubSM::stage2_guard)]/ &SubSM::process_stage2, the deferred event never trigger the stage2->stage1 transition even though the transition condition has met.

However, if looking at the line 59, the stage1->stage2 transition without a guard can correctly trigger state transition. But for stage2, without the guard it will fall into the infinite loop of processing event without even check whether stage2 has complete.

Is there an explanation to the different behaviors?

Schoppenglas commented 1 year ago

I think u are using wrong DSL for transition table, like in #542:

*state<Stage1>[&SubSM::stage1_guard] / []{cout << "Transiting to stage 2\n"; } = state<Stage2>
(...)
,state<Stage2>[&SubSM::stage2_guard] / []{cout << "Leaving stage 2\n"; } = state<Stage1>

There is no anonymous transition with guard (see Tutorial/3. Create a transition table or Examples/Transitions)

src_state [guard] = dst_state

It seems that guards can only be used with an event:

state + event [guard]
src_state + event [guard] / action = dst_state
Schoppenglas commented 1 year ago

Does this code what u expected? As in #542, I added event before guard and emitted event twice.

#include <boost/sml.hpp>
#include <iostream>
#include <deque>
#include <queue>

using namespace boost::sml;

struct Data {
  int count{};
};

struct Stop {};
struct Idle {};
struct SubSM {
  struct Stage1 {};
  struct Stage2 {};
  int stage1_cnt{};
  int stage2_cnt{};

  bool stage1_guard() {
    std::cout << "stage 1 check\n";
    return stage1_cnt == 0;
  }

  bool stage2_guard() {
    std::cout << "stage 2 check\n";
    return stage2_cnt == 0;
  }

  void init() {
    stage1_cnt = 10;
    stage2_cnt = 30;
  }
  void process_stage1(const Data &evt, back::defer<Data> defer_event) {
    std::cout << "In stage 1, Received " << evt.count << "\n";
    if (stage1_cnt >= evt.count) {
      stage1_cnt -= evt.count;
    } else {
      std::cout << "In stage 1, reprocess " << evt.count - stage1_cnt << "\n";
      defer_event(Data{evt.count - stage1_cnt});
      defer_event(Data{evt.count - stage1_cnt});
      stage1_cnt = 0;
    }
  }

  void process_stage2(const Data &evt, back::defer<Data> defer_event) {
    std::cout << "In stage 2, Received " << evt.count << "\n";
    if (stage2_cnt >= evt.count) {
      stage2_cnt -= evt.count;
    } else {
      std::cout << "In stage 2, reprocess " << evt.count - stage2_cnt << "\n";
      defer_event(Data{evt.count - stage2_cnt});
      defer_event(Data{evt.count - stage2_cnt});
      stage2_cnt = 0;
    }
  }

  auto operator()() {
    return make_transition_table(
            // clang-format off
             state<Stage1> + event<Data>[&SubSM::stage1_guard] / []{std::cout << "Transiting to stage 2\n"; } = state<Stage2>
            ,state<Stage1> + on_entry<_> / &SubSM::init,
            *state<Stage1> + event<Data> / &SubSM::process_stage1
            ,state<Stage2> + event<Data>[&SubSM::stage2_guard] / []{std::cout << "Leaving stage 2\n"; } = state<Stage1>
            ,state<Stage2> + event<Data> / &SubSM::process_stage2
            // clang-format on
    );
  }
};
struct CompositeSM {
  auto operator()() {
    return make_transition_table(
            // clang-format off
       *state<Idle> + event<Data> / defer = state<SubSM>
       ,state<Data> + event<Stop> = state<Idle>
            // clang-format on
    );
  }
};

int main() {

  SubSM sub_sm;
  CompositeSM composite_sm;

  sm<CompositeSM, defer_queue<std::deque>, process_queue<std::queue>> sm{sub_sm, composite_sm};

  sm.process_event(Data{199});
  return 0;
}
wuyuanyi135 commented 1 year ago

@Schoppenglas Many thanks for your explanation! I was expecting that the misuse could be caught by the compiler. I will check if this is the solution these days.

wuyuanyi135 commented 1 year ago

@Schoppenglas Thanks. I read through the examples and tutorials and it seems indeed no anonymous transition with a guard (also the doc.)

My question is now how to optimize the guard function? For example, if the guard is heavy (say, decoding the event data), negating it results in another invocation and it seems inefficient. What is the recommended practice is this case? Should I use eval to first perform some action to cache the result and reuse them in the following guards?

Schoppenglas commented 1 year ago

@wuyuanyi135 Personally I would not do heavy calculations in guards. It is not wrong, but not my personal preference. Maybe u can rethink ur whole structure. Or maybe state machine is not the best method to solve ur problem. And of course, perform heavy calculations only once and cache the result.

Schoppenglas commented 1 year ago

@wuyuanyi135 I have rewritten ur example. I think it is much more straight forward now and expresses more what it is supposed to do. Tell me ur opionion on this.

#include <boost/sml.hpp>
#include <iostream>
#include <cassert>
#include <deque>

using namespace boost::sml;

struct Data {
  int count{};
};

struct Stop {};
struct Idle {};
struct SubSM {
  struct Stage1 {};
  struct Stage2 {};

  const int stage1_cnt{10};
  const int stage2_cnt{30};

  bool stage1_guard(const Data& e) {
    std::cout << "Stage 1 check\n";
    return e.count >= stage1_cnt;
  }

  bool stage2_guard(const Data& e) {
    std::cout << "Stage 2 check\n";
    return e.count >= stage2_cnt;
  }

  void process_stage1(const Data &e, back::defer<Data> defer_event) {
    std::cout << "In stage 1, received " << e.count << "\n";
    defer_event(Data{e.count - stage1_cnt});
    std::cout << "In stage 1, reprocess " << e.count - stage1_cnt << "\n";
    std::cout << "Transiting to stage 2\n";
  }

  void process_stage1_2(const Data &e) {
    std::cout << "In stage 1, received " << e.count << "\n";
    std::cout << "Leaving SubSM\n";
  }

  void process_stage2(const Data &e, back::defer<Data> defer_event) {
    std::cout << "In stage 2, received " << e.count << "\n";
    defer_event(Data{e.count - stage2_cnt});
    std::cout << "In stage 2, reprocess " << e.count - stage2_cnt << "\n";
    std::cout << "Leaving stage 2\n";
  }

  void process_stage2_2(const Data &e) {
    std::cout << "In stage 2, received " << e.count << "\n";
    std::cout << "Leaving SubSM\n";
  }

  auto operator()() {
    return make_transition_table(
      // clang-format off
      *state<Stage1> + event<Data>[&SubSM::stage1_guard] / &SubSM::process_stage1 = state<Stage2>
      ,state<Stage1> + event<Data> / &SubSM::process_stage1_2 = X
      ,state<Stage2> + event<Data>[&SubSM::stage2_guard] / &SubSM::process_stage2 = state<Stage1>
      ,state<Stage2> + event<Data> / &SubSM::process_stage2_2 = X
      // clang-format on
    );
  }
};
struct CompositeSM {
  auto operator()() {
    return make_transition_table(
      // clang-format off
      *state<Idle> + event<Data> / defer = state<SubSM>
      ,state<SubSM> + event<Stop> / []{ std::cout << "Leaving CompositeSM\n"; } = X
      // clang-format on
    );
  }
};

int main() {
  SubSM sub_sm;
  CompositeSM composite_sm;
  sm<CompositeSM, defer_queue<std::deque>> sm{sub_sm, composite_sm};
  sm.process_event(Data{199});
  sm.process_event(Stop{});
  assert(sm.is(X));
  return 0;
}