fsm_gen

FSM (finite state machine) DSL for code generation (c++ at the moment)


Keywords
fsm, code_generator, finite_state_machine, cpp, dsl, generator, rust
License
GPL-3.0

Documentation

FSM generator

Installation

cargo install -f fsm_gen

You can download from repository and build and install as usual

cargo build --release
cargo install --path . -f

Versions

0.6

  • cpp generated code with option to move to error state
  • Instead of languages, now we speak about templates

0.5

  • moved to tera templates
  • added dot files generation

0.4

  • error redefinied action on cpp (watch example iceberg)
  • fix error when running on current directory
  • Add full proposed file for hand written .h and .cpp on generated one

0.3

  • multiline guards with no status name
  • multi guard and multi actions
  • '_' on input means any other input
  • negative guards
  • if no transition, it will generate an error
  • updated rust-peg lib and working with macros
  • if execption is throwed, we will go to error transition
  • better error information
    • on parsing show line
    • check orphans status
  • makefile install, test cpp

0.2

  • Actions
  • private hpp (hand file)
  • Template functions to specialize on transaction change
  • Anonymous namespace for functions
  • Add comments support on fsm grammar

TODO

  • Error transition is special

  • Inprove exceptions control

  • web ui for templates

  • read template from file

  • Update idata in order to use ispush with btrees and more

  • Complete the cpp example and update on README.md

  • Check fsm format

    • detect duplicated states
    • detect duplicated inputs and guards
  • output with signals

  • Add templates

Aim

All computing processes consist of receiving an input and processing it by generating an output. From the simplest to the most complex.

Sometimes, the process depends on the context and it is necessary to manage a state.

Messages and state management, therefore, are two fundamental elements of any software process.

That's why I wrote two external DSLs long time ago for both elements (which I still use in production and are a great help).

In this repository I rewrite one of them, the code generator for a state machine.

At the moment it generates C++ code (my most immediate target in production).

You can have data (fields) on inputs structs, and also on each status

To explain the system, I will use the example in cpp_test/fsm

This example is about writing a system that will handle login requests.

First the server will be asked for a password.

This key will be used to encode the username and password (this one will be passed through a hash function) in the login request.

This encoding will be irreversible (hash function). The server will perform the same operation (starting from the hash of the password) to verify the validity.

If it is OK, it will send a login confirmation.

The diagram would look like this:

Basic diagram

A list of transitions could be written as:

//  Example of fsm to manage login
//  on server side

[init]
    rq_key          ->  w_login     /   send_key
    timer           ->  init

[w_login]
    rq_login        ->  login       /   send_login
    timer
        & timeout   ->  error
                    ->  w_login
[login]
    rq_logout       ->  logout      /   send_logout
    heartbeat       ->  login       /   update_hb
    timer
        & timeout   ->  logout
                    ->  login

[logout]
    timer           ->  logout

[error]
    _               ->  error

And this is the input for this tool to generate code

In fact, even the previous diagram has been generated from this DSL (it generated a graphviz dot file)

Elements

States

[init]
    ...

init, w_login, login ... are the states

Depending on the input and the state (with its values as will be seen later), the system will change to a new state.

Transition

    rq_key                  ->  w_login

If we receive an input (in this case rq_key) we go to next state (w_login)

Input

The elements received by the status machine.

    INPUT
      v
    rq_key                  ->  w_login

In the example they are rq_key, rq_login, rq_logout, heartbeat and timer.

Guards

Functions that will be called depending on the status and input to decide the way forward.

                    GUARD
                     v
    rq_login    &   valid   ->  login       /   send_login

You can have more than one guard

    rq_login    &   valid  &  guard2  ->  login       /   send_login

When you apply different options guards (or combination)...

In the example we have valid, timeout, ontime.

    rq_login
        &   valid   ->  login       /   send_login
                    ->  logout      /   log_err

It could be written with negative guard

    rq_login
        &   !valid  ->  logout      /   log_err
                    ->  login       /   send_login

Final status

Behind the -> arrow is the state we will change to.

                               FINAL_STATUS
                                   v
    rq_key                  ->  w_login

Actions

We can define an action to be performed when executing a transaction.

This will be after the final state and '/'.

                                                ACTION
                                                  v
    rq_login                ->  logout      /   log_err

You can have more than one action

    rq_login                ->  logout      /   log_err  action2

In this example we have send_key, send_login...

Special transition

In all states it is necessary to consider all inputs.

But it is very common that many transitions are the same (generally error cases).

This is marked with the input _

Consider the this example status:

[init]
    rq_key                      ->  w_login     /   send_key
    timer                       ->  init
    _                           ->  logout      /   log_err

_ means... any other input...

Therefore considering all possible inputs in this state

error status and implicit transitions

error is a special status

You can move to error status explicitly.

Any transition no defined, will finis on error status.

You can also put some verifications on transiction funcion, and incase of fail, you can move to error (even when is not explicitly writted on fsm)

This is so, because checking the params, is so commont that adding guards for it, would generate a lot of sound

In our example...

[init]
    rq_key          ->  w_login     /   send_key
    timer           ->  init

There are no transations for rq_login and rq_logout. Both are implicit and is equivalent to...

[init]
    rq_key          ->  w_login     /   send_key
    timer           ->  init
    _               ->  error

This is the transition function control (obiosly you can specialize as you wish)

    //  status change functions
    template <typename FROM, typename IN, typename TO>
    std::variant<TO, st_error_t> fromin2(const FROM &, const IN &) {
        //  here you could check the params and decide to go to error
        //  instead to the programmed trasnsition
        ...
    }

And this is when you explicitly write a transition who finished on error status

    template <typename FROM, typename IN>
    st_error_t fromin2error(const FROM &, const IN &) {
      return st_error_t{...};
    }

Another way to end on error transition. If an exception is thrown while the input is being procesed...

  [...]
  } catch (...) {}

  auto nw_st_info = fromin2error<st_init_t, in_rq_key_t>(this->info, in);
  log("[init] rq_key error/default -> error", in, info, nw_st_info);
  return std::make_shared<error>(nw_st_info);

Comments

Comments starts at // and continues to the end of line

Usage

To get help...

fsm_gen --help
> fsm_gen -h

fsm_gen 0.6.1
jleahred

    Generate code from a simple fsm file
    To check the supported templates  --show_templs
    

USAGE:
    fsm_gen [FLAGS] [OPTIONS] [fsm_files]...

FLAGS:
    -d, --dot-graphviz    Generate graphviz dot file
    -h, --help            Prints help information
        --help-cpp        Give me some information about generating cpp files
    -s, --show-templs     Show supported template generators
    -V, --version         Prints version information

OPTIONS:
    -T, --threads <n_threads>    Number of threads to use. 0 means one per core  ;-) [default: 0]
    -t, --templ <templ>          Template to generate code (show available --show-templs) [default: cpp]

ARGS:
    <fsm_files>...    List of fsm files to be processed

The default template is c++ (and at the moment the only one)

You can run:

fsm_gen login.fsm

And it will generate the c++

You can pass a list of fsm files

fsm_gen login.fsm  test/seller.fsm test/test2/lift.fsm

The code will be generated on same directory of original .fsm file

If your shell supports it, you could run...

fsm_gen **/*.fsm

C++ code generation

Starting from the example login.fsm the system will create...

    fsm_login_gen.h
    fsm_login_gen.cpp

You don't have to modify these files.

You have to write your code on next files...

    fsm_login_types.h
    fsm_login_private.hpp

These two files will be created if don't exist as a reference

A empty reference code for these two files will be added on _gen files as comments.

Files dependency:

cpp_files_depen

fsm_login_gen.h

Full code on cpp_test/fsm/fsm_login_gen.h

We are informed on when it was created.

This file is the starting point to use or extend the generated status machine in your program.

//  generated automatically  2019-03-22 11:24:40
//  do not modify it manually

Headers and namespaces based on filename

#pragma once

#include <iostream>
#include <memory>

namespace login {

Internal class forward declaration

class BaseState;
typedef std::shared_ptr<BaseState> SState;

The fsm class you have to instantiate or extend

//  -------------------
//      F S M
class Fsm {
public:
  Fsm();
  ~Fsm();

  void process(const heartbeat_t& in);
  void process(const rq_key_t& in);
  void process(const rq_login_t& in);
  void process(const rq_logout_t& in);
  void process(const timer_t& in);

  ...
}

fsm_login_gen.cpp

Full code on cpp_test/fsm/fsm_login_gen.cpp

You must not modify this file, and it's not necessary to know much about it

fsm_login_types.h

Full code on cpp_test/fsm/fsm_login_types.h

In this file you have to declare the status info types, and input types

If the file doesn't exist, it will be created with empty data types

//  Code generated automatically to be filled manually
//  This file will not be updated by generator
//  It's created just the first time as a reference
//  Generator will allways create a  .reference file to help with
//  new methods and so

On namespace based on fsm file, we have the two types to declare

You can, of course, complete these types on cpp file if necessary

namespace login {

  //  status info types
  struct st_init_t{};
  struct st_w_login_t{};
  struct st_login_t{};
  struct st_logout_t{};
  struct st_error_t{};


  //  input types
  struct in_heartbeat_t {};
  struct in_rq_key_t {};
  struct in_rq_login_t {};
  struct in_rq_logout_t {};
  struct in_timer_t {};


} // namespace login
#endif // FSM_LOGIN_H

fsm_login_private.hpp

Full code on cpp_test/fsm/fsm_login_private.hpp

This is the other file you have to maintain by hand.

//  Code generated automatically to be filled manually
//  This file will not be updated by generator
//  It's created just the first time as a reference
//  Generator will allways create a  .reference file to help with
//  new methods and so

//  This file will be included in _gen.cpp
//  (anywhere else)

//  to make happy some IDEs
#include "fsm_login_types.h"
#include "fsm_login_gen.h"

namespace {
    using namespace login;

As you can see, it is on anonymous namespace.

This file is private, it will be included by _gen.cpp. Defining an anonymous namespace, we keep this implementation as private. Even more important, the compiler will alert us if we forget an implementation, also if one is not necessary.

Log and status change transitions are templates.

This is a log example:

[init] rq_key -> w_login
[w_login] rq_login(valid) -> login
[login] rq_logout -> logout

In this way, you can specialize or generalize as much as you want.

    //  log
    template <typename IN, typename INIT_ST, typename END_ST>
    void log(const std::string &txt_trans, const IN &, const INIT_ST &,
            const END_ST &) {
        std::cout << txt_trans << std::endl;
    }

    //  status change functions
    template <typename FROM, typename IN, typename TO>
    std::variant<TO, st_error_t> fromin2(const FROM &, const IN &) {
        //  you can specialize this generic function
        return TO{};
    }
    template <typename FROM, typename IN>
    st_error_t fromin2error(const FROM &, const IN &) {
      return st_error_t{};
    }

First parameter in log, is an string with transition change information (initial transition, input, guard if so, final transition)

Next, we have the guards and actions functions.

    //  guards
    bool valid(const in_rq_login_t& /*in*/, const  st_w_login_t& /*st_info*/) { return true; }
    bool timeout(const in_timer_t& /*in*/, const  st_w_login_t& /*st_info*/) { return true; }
    bool timeout(const in_timer_t& /*in*/, const  st_login_t& /*st_info*/) { return true; }


    //  actions
    void act_send_key(const st_init_t& /*st_orig*/, const in_rq_key_t& /*in*/, const  st_w_login_t& /*st_dest*/) {}
    void act_send_login(const st_w_login_t& /*st_orig*/, const in_rq_login_t& /*in*/, const  st_login_t& /*st_dest*/) {}
    void act_send_logout(const st_login_t& /*st_orig*/, const in_rq_logout_t& /*in*/, const  st_logout_t& /*st_dest*/) {}
    void act_update_hb(const st_login_t& /*st_orig*/, const in_heartbeat_t& /*in*/, const  st_login_t& /*st_dest*/) {}


} // namespace anonymous

Diagrams source

digraph G {
rankdir = BT;
  node [shape=plaintext fontname="Arial"];

main -> "fsm_login_gen.h"

"fsm_login_gen.cpp" -> "fsm_login_gen.h"

"fsm_login_gen.h" -> "fsm_login_types.h"

}