Do you like Finite State Machines?

If you do, you probably have heard of ragel a tool that lets you generate super efficient code following a FSM definition. Ragel is super useful for many things. I have used it to implement “shortcodes”, which is a feature you may know if you have used Hugo.

In general, it’s super cool software, and for some types of problems it will generate the fastest, most efficient code possible.

Well, I have made it spit crystal. Not perfect, some of the examples make the codegen explode (clang.rl example). But I am working on it.

What does this mean? Well, it means this is a crystal RPN calculator:

# Reverse Polish Notation Calculator
# Copyright (c) 2010 J.A. Roberts Tunney
# MIT License
#
# Ported to Crystal from the Go original
#
# To compile:
#
#   ragel -Y -o rpn.cr rpn.rl
#   crystal build rpn.cr -o rpn
#   ./rpn
#
# To show a diagram of your state machine:
#
#   ragel -V -Y -p -o rpn.dot rpn.rl

%%{
  machine rpn;
  write data;
}%%

class RPNCalculator
  @items : Array(Int32)
  @count : Int32

  def initialize
    @items = Array(Int32).new(128, 0)
    @count = 0
  end

  def pop : Int32
    @count -= 1
    @items[@count]
  end

  def push(v : Int32)
    @items[@count] = v
    @count += 1
  end
end

def abs(v : Int32) : Int32
  v < 0 ? -v : v
end

def rpn_eval(data : String) : Tuple(Int32, String?)
  cs, p, pe = 0, 0, data.size
  mark = 0
  st = RPNCalculator.new

  %%{
    action mark { mark = p }
    action push {
      x = data[mark...p].to_i
      st.push(x)
    }
    action add {
      y, x = st.pop, st.pop
      st.push(x + y)
    }
    action sub {
      y, x = st.pop, st.pop
      st.push(x - y)
    }
    action mul {
      y, x = st.pop, st.pop
      st.push(x * y)
    }
    action div {
      y, x = st.pop, st.pop
      st.push((x / y).to_i32)
    }
    action abs { st.push(abs(st.pop)) }
    action abba { st.push(666) }

    stuff  = digit+ >mark %push
           | '+' @add
           | '-' @sub
           | '*' @mul
           | '/' @div
           | 'abs' %abs
           | 'add' %add
           | 'abba' %abba
           ;

    main := ( space | stuff space )* ;

    write init;
    write exec;
  }%%

  if cs < Rpn_first_final
    if p == pe
      return {0, "unexpected eof"}
    else
      return {0, "error at position #{p}"}
    end
  end

  if st.@count == 0
    return {0, "rpn stack empty on result"}
  end

  {st.pop, nil}
end

# Test cases
tests = [
  {"666\n", 666},
  {"666 111\n", 111},
  {"4 3 add\n", 7},
  {"4 3 +\n", 7},
  {"4 3 -\n", 1},
  {"4 3 *\n", 12},
  {"6 2 /\n", 3},
  {"0 3 -\n", -3},
  {"0 3 - abs\n", 3},
  {" 2  2 + 3 - \n", 1},
  {"10 7 3 2 * - +\n", 11},
  {"abba abba add\n", 1332},
]

fail_tests = [
  {"\n", "rpn stack empty on result"},
]

rc = 0

tests.each do |test|
  input, expected = test
  res, err = rpn_eval(input)
  if err
    STDERR.puts "FAIL rpn(#{input.inspect}) #{err}"
    rc = 1
  elsif res != expected
    STDERR.puts "FAIL rpn(#{input.inspect}) -> #{res.inspect} != #{expected.inspect}"
    rc = 1
  else
    STDERR.puts "SUCCESS rpn(#{input.inspect}) -> #{res.inspect}"
  end
end

fail_tests.each do |test|
  input, expected_err = test
  res, err = rpn_eval(input)
  if err.nil?
    STDERR.puts "FAIL rpn(#{input.inspect}) -> #{res.inspect} should fail: #{expected_err.inspect}"
    rc = 1
  elsif err != expected_err
    STDERR.puts "FAIL rpn(#{input.inspect}) #{err.inspect} should be #{expected_err.inspect}"
    rc = 1
  end
end

exit rc

And this is a diagram of the FSM:

3 Likes

Looks like nobody likes them but just in case: this is functional enough I am using it for an actual project and the bugs are not coming from the crystal backend :smiley:

For my part, I’m interested but don’t have practical experience with state machines, so you just sent me down a research rabbit hole… Luckily, I always appreciate a research rabbit hole, so thank you! :joy:

Research rabbit holes are one of the main effects of state machines :-D

Look at russ cox’s articles on regexes for a nice start

1 Like

I like them… I’ve experimented in parsers and such in the past…. so, I would check this out…

1 Like

I do love Finite State Machines!

Perhaps a bit off topic but..

I have used that design pattern the last 50 years in mine professional and retired carreer in several usecases.
There are several way to describe an FSM, like regexp etc. Plain handwritten source is sometimes enough.

I think some people stop using it when they try to get o proper and useful graph!
My advice is don’t rely on graphs that much. Do trace behavior around states during stepwise development.

The pattern is powerful for detecting errors and imperfections. But they will be very easy to correct I will say.

I have been using FSM for design and implement client-sever application and many interactions finger/mouse on grahpic apps.

At one moment in my carreer we made a FSM for a central server which dealt with shipments.
Then we come up with an idea to INVERT the FSM to get a proper machine at a client side!

I made a tool for this. There is a lot of writings too around the topic. You can find here

In any case try use the pattern FSM when solving challenges in any scale. Start with small examples

I did something similar. I will push mine to GitHub this week Maybe we can collaborate and ask to include in ragel proper.

I also enjoy FSM a lot. I find it a very convenient way to express flows of course. And also used them to express UI tools in Digital photogrammetric plotter | Manas.Tech , and Biomedical Signals Processing | Manas.Tech .

I did became a bit skeptical about tools around them. There are always exceptions and some paper cuts around maintenance: how to ensure expanded methods are removed/marked if the fsm description does not need them anymore.

@willy610 on the client/server, you might enjoy the concept if choreographic programming. There you describe the interaction and project both client and server. Amazon.com: Introduction to Choreographies eBook : Montesi, Fabrizio: Kindle Store