c++boostboost-sml

visualize boost sml state machine


I am building a larger state machine in boost sml for the first time and need a way to visualize (e.g. export to graphviz) the whole state machine. Any idea how one could do that? Is there any way to iterate over the structure of the state machine and print it?


Solution

  • Disclaimer: I have zero experience with SML (had to find where it lives).

    First Attempt, Mmmm

    However, trying some things with the visit_current_states interface, I came up with this ... not so nice implementation that maps out a (composite) state machine, given a list of events:

    #include <boost/sml.hpp>
    
    namespace sml = boost::sml;
    namespace aux = sml::aux;
    
    struct e1 {};
    struct e2 {};
    struct e3 {};
    struct e4 {};
    struct e5 {};
    
    struct sub {
        auto operator()() const {
            using namespace sml;
            // clang-format off
            return make_transition_table(
                 *"idle"_s + event<e3> = "sub1"_s
                , "sub1"_s + event<e4> = X
            );
            // clang-format on
        }
    };
    
    struct composite {
        auto operator()() const {
            using namespace sml;
            // clang-format off
            return make_transition_table(
                 *"idle"_s   + event<e1> = "s1"_s
                , "s1"_s     + event<e2> = state<sub>
                , state<sub> + event<e5> = X
            );
            // clang-format on
        }
    };
    
    #include <boost/hana.hpp>
    #include <boost/core/demangle.hpp>
    #include <iostream>
    #include <iomanip>
    
    namespace mapper {
        namespace hana = boost::hana;
        using namespace std::string_literals;
        using boost::core::demangle;
        
        template <typename F> struct ycombine {
            ycombine(F f):f(f) {}
            F f;
            template <typename... A>
            auto operator()(A... a) const { return f(*this, a...); };
        };
    
        hana::tuple<e1,e2,e3,e4,e5> events;
    
        template <class TSM> class Vis {
          public:
            explicit Vis(const TSM& sm, std::string prefix = "") : sm_{ sm }, prefix(prefix) {}
    
            template <class TSub>
            void operator()(aux::string<boost::sml::sm<TSub>>) const {
                auto subname = aux::get_type_name<TSub>();
    
                Vis nvis(sm_, prefix + '/' + subname);
                sm_.template visit_current_states<aux::identity<TSub>>(nvis);
                prefix = nvis.prefix;
            }
    
            template <class TState> void operator()(TState state) const {
                prefix += "/"s + state.c_str();
            }
    
          private:
            const TSM& sm_;
          public:
            mutable std::string prefix;
        };
    
        template <typename SM>
        std::string get_current(SM const& sm) {
            Vis<SM> v{sm};
            sm.visit_current_states(v);
            return v.prefix;
        };
    }
    
    int main() {
        using namespace mapper;
    
        auto recurse = ycombine {
            [](auto self, auto sm) {
                hana::for_each(events, [=](auto ev) {
                    auto clone = sm;
                    auto from = get_current(clone);
                    clone.process_event(ev);
                    auto to = get_current(clone);
    
                    if (from != to) {
                        std::cout
                            << std::quoted(from) << " -> "
                            << std::quoted(to) 
                            << " [label=" << std::quoted(demangle(typeid(ev).name())) << "]\n";
    
                        self(clone);
                    }
                });
            } };
    
        std::cout << "digraph {\n";
        recurse(sml::sm<composite>{});
        std::cout << "}\n";
    }
    

    Though I have no idea how useful this is, at least I could render the result with graphviz:

    enter image description here

    Second Attempt - READ THE FINE MANUAL :derp:

    It looks like there was a cleaner way to do these things, especially the presence of the nested typedef transitions when reading the latest examples:

    // $CXX -std=c++14 plant_uml.cpp
    #include <boost/sml.hpp>
    #include <cassert>
    #include <iostream>
    #include <string>
    #include <typeinfo>
    
    namespace sml = boost::sml;
    
    struct e1 {};
    struct e2 {};
    struct e3 {};
    struct e4 {};
    
    struct guard {
      bool operator()() const { return true; }
    } guard;
    
    struct action {
      void operator()() {}
    } action;
    
    struct plant_uml {
      auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
           *"idle"_s + event<e1> = "s1"_s
          , "s1"_s + event<e2> [ guard ] / action = "s2"_s
          , "s2"_s + event<e3> [ guard ] = "s1"_s
          , "s2"_s + event<e4> / action = X
        );
      }
    };
    
    template <class T>
    void dump_transition() noexcept {
      auto src_state = std::string{sml::aux::string<typename T::src_state>{}.c_str()};
      auto dst_state = std::string{sml::aux::string<typename T::dst_state>{}.c_str()};
      if (dst_state == "X") {
        dst_state = "[*]";
      }
    
      if (T::initial) {
        std::cout << "[*] --> " << src_state << std::endl;
      }
    
      std::cout << src_state << " --> " << dst_state;
    
      const auto has_event = !sml::aux::is_same<typename T::event, sml::anonymous>::value;
      const auto has_guard = !sml::aux::is_same<typename T::guard, sml::front::always>::value;
      const auto has_action = !sml::aux::is_same<typename T::action, sml::front::none>::value;
    
      if (has_event || has_guard || has_action) {
        std::cout << " :";
      }
    
      if (has_event) {
        std::cout << " " << boost::sml::aux::get_type_name<typename T::event>();
      }
    
      if (has_guard) {
        std::cout << " [" << boost::sml::aux::get_type_name<typename T::guard::type>() << "]";
      }
    
      if (has_action) {
        std::cout << " / " << boost::sml::aux::get_type_name<typename T::action::type>();
      }
    
      std::cout << std::endl;
    }
    
    template <template <class...> class T, class... Ts>
    void dump_transitions(const T<Ts...>&) noexcept {
      int _[]{0, (dump_transition<Ts>(), 0)...};
      (void)_;
    }
    
    template <class SM>
    void dump(const SM&) noexcept {
      std::cout << "@startuml" << std::endl << std::endl;
      dump_transitions(typename SM::transitions{});
      std::cout << std::endl << "@enduml" << std::endl;
    }
    
    int main() {
      sml::sm<plant_uml> sm;
      dump(sm);
    }
    

    The output, for inspiration:

    @startuml
    
    [*] --> idle
    idle --> s1 : e1
    s1 --> s2 : e2 [guard] / action
    s2 --> s1 : e3 [guard]
    s2 --> terminate : e4 / action
    
    @enduml
    

    Obviously, not graphviz, but actually looks more "trustworthy" due to the fact that