The Elixir implementation of the Finite State Language: describe a process as a finite state machine — states, transitions, on_events and its selective receive, stay, goto back, sub-FSMs and cooperative shutdown, service building blocks, a live registry, a sequence journal and its PlantUML and Mermaid renderers — and plug a protocol into it.

defmodule Turnstile do
  use FSL.Machine

  config label: "gate 3"

  state initial_state do
    goto locked
  end

  state locked do
    on_events do
      {:coin, _} -> goto unlocked, "paid"
      {:push, _} -> stay("still locked")
    after
      60_000 -> scenario_success("nobody came")
    end
  end

  state unlocked do
    on_events do
      {:push, _} -> goto locked, "went through"
    after
      10_000 -> goto locked, "timed out"
    end
  end
end

FSL.Runner.run_instance(Turnstile)

The core depends on nothing but Logger and OTP. FSL.HTTP is the one module that needs a client, and Req is declared optional: true for it.

Install

def deps do
  [{:fsl, "~> 0.2", hex: :finite_state_language}]
end

Three names for one thing: finite_state_language is the hex package, :fsl is the OTP application, FSL.* is what the code writes. The dep line has to carry the first two precisely because they differ — the atom names the application mix builds and resolves, hex: names the package it is fetched from. Naming only the package would send mix looking for a finite_state_language.app that no build produces, and mix deps.get would fail on the first try. Documentation is on hexdocs.

Run something

mix run samples/fishing.exs

Bob goes fishing. Ducks paddle past, the line snags, two fish bite, and he has 400 ms to strike each time — his hands, which are a sub-FSM with a reaction time of their own, take 250 ms. The trip prints itself as a Mermaid diagram you can paste into a GitHub comment. samples/ is the file to read first, and it is commented for that.

The governing idea

The context belongs to FSL, and a protocol binding extends it. The six fields a machine keeps about itself — lasterr, errorreason, currentstate, laststate, parent_pid, appdata — are FSL.Context; a binding splices them into its own struct and adds what a session of its protocol holds.

Everything the language must not know is a callback on FSL.Host: what to bootstrap, how to read a config block, what to do with each received event, how to categorize one, which clause every wait must carry, what to release at the end. A machine that names no host gets FSL.Host.Default and runs with no protocol at all. FSL.Test.Host (test/support/) is the worked example, and what this package's own suite runs against.

The test of every seam is whether a second binding — XMPP, Matrix, a chatbot framework — could be written without touching FSL. SIP is the first, in Elixip: SIP.FSL.Host is ~380 lines and SIP.Scenario is the 100-line facade a SIP scenario writes.

Documentation

  • the API referenceFSL.Machine is the entry point, FSL.Host is what an embedding writes;
  • samples/README.md — the fishing trip, annotated;
  • docs/design.md — the as-built design: why the engine is a flat call stack, what stay rewrites, how a block's cleanup/1 is guaranteed;
  • spec/fsl-js-ts.md — the cross-language contract, reconciled clause by clause with the TypeScript implementation, including the divergences kept on purpose.

Licence

Apache-2.0. The code was extracted from Elixip, which stays BUSL-1.1, and relicensed so that both implementations of the language ship under one licence — see NOTICE.