Finite-state machine builder on top of Yog.
Choreo.FSM lets you define states, transitions, and render classic
state-machine diagrams. It supports normal states, initial states, and
final (accepting) states.
When to use
Use Choreo.FSM when modelling stateful behaviour — protocol handlers,
UI flows, game states, or embedded-system controllers. It verifies
determinism, finds dead states, and checks whether any input sequence
leads to acceptance.
Further reading
Quick Start
fsm =
Choreo.FSM.new()
|> Choreo.FSM.add_initial_state(:idle, label: "Idle")
|> Choreo.FSM.add_state(:running, label: "Running")
|> Choreo.FSM.add_final_state(:done, label: "Done")
|> Choreo.FSM.add_transition(:idle, :running, label: "start")
|> Choreo.FSM.add_transition(:running, :done, label: "finish")
|> Choreo.FSM.add_transition(:running, :idle, label: "pause")
dot = Choreo.FSM.to_dot(fsm)
mermaid = Choreo.FSM.to_mermaid(fsm)Diagram
Themes
Use :default, :dark, or a custom Choreo.Theme struct:
dot = Choreo.FSM.to_dot(fsm, theme: :dark)
mermaid = Choreo.FSM.to_mermaid(fsm, theme: :dark)
Summary
Functions
Adds a final (accepting) state to the FSM.
Adds an initial state to the FSM.
Adds a state to the FSM.
Adds a transition (directed edge) between two states.
Returns the complement FSM: final states become normal, and normal states become final.
Returns all final state IDs.
Returns the initial state ID, or nil if none is defined.
Creates a new empty FSM.
Prunes unreachable and dead states, returning a smaller equivalent FSM.
Removes a state from the set of final states.
Removes a state from the set of initial states.
Fully removes a state and all transitions involving it from the FSM.
Returns all state IDs in the FSM.
Returns a theme for Choreo.FSM.
Renders the FSM to DOT format.
Renders the FSM to Mermaid.js flowchart syntax.
Returns all transitions as {from, to, label} tuples.
Types
@type t() :: %Choreo.FSM{ edge_meta: %{optional(Yog.Multi.Graph.edge_id()) => map()}, graph: Yog.Multi.Graph.t(), meta: %{ initial_state: Yog.node_id() | nil, final_states: MapSet.t(Yog.node_id()), strict: boolean() } }
Functions
@spec add_final_state(t(), Yog.node_id(), keyword()) :: t()
Adds a final (accepting) state to the FSM.
Final states are rendered as double circles.
Options
:label(String.t/0):shape(atom/0):fillcolor(String.t/0):fontcolor(String.t/0):style(String.t/0):penwidth:image(String.t/0)
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_final_state(:done)
iex> :done in Choreo.FSM.final_states(fsm)
trueDiagram
@spec add_initial_state(t(), Yog.node_id(), keyword()) :: t()
Adds an initial state to the FSM.
Initial states are rendered with a filled entry-point dot and an incoming arrow in DOT output.
Options
:label(String.t/0):shape(atom/0):fillcolor(String.t/0):fontcolor(String.t/0):style(String.t/0):penwidth:image(String.t/0)
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_initial_state(:idle)
iex> Choreo.FSM.initial_state(fsm) == :idle
trueDiagram
@spec add_state(t(), Yog.node_id(), keyword()) :: t()
Adds a state to the FSM.
Options
:type:label(String.t/0):shape(atom/0):fillcolor(String.t/0):fontcolor(String.t/0):style(String.t/0):penwidth:image(String.t/0)
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_state(:idle, label: "Idle")
iex> fsm.graph.nodes[:idle].label
"Idle"Diagram
@spec add_transition(t(), Yog.node_id(), Yog.node_id(), keyword()) :: t()
Adds a transition (directed edge) between two states.
Multiple transitions are allowed per (from, to) pair (parallel edges),
as long as they have unique labels from the source state.
Options
:label(String.t/0):guard(String.t/0)
Examples
iex> fsm =
...> Choreo.FSM.new()
...> |> Choreo.FSM.add_state(:a)
...> |> Choreo.FSM.add_state(:b)
...> |> Choreo.FSM.add_transition(:a, :b, label: "go")
iex> Yog.Multi.edges_between(fsm.graph, :a, :b) != []
trueDiagram
Returns the complement FSM: final states become normal, and normal states become final.
Initial states keep their :initial type. Note that if the initial state
was not previously final, it becomes both initial and final in the
complement FSM (which is mathematically correct to accept the empty string).
Examples
iex> fsm =
...> Choreo.FSM.new()
...> |> Choreo.FSM.add_initial_state(:a)
...> |> Choreo.FSM.add_final_state(:b)
iex> comp = Choreo.FSM.complement(fsm)
iex> :a in Choreo.FSM.final_states(comp)
true
iex> :b in Choreo.FSM.final_states(comp)
falseDiagram
@spec final_states(t()) :: [Yog.node_id()]
Returns all final state IDs.
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_final_state(:done)
iex> :done in Choreo.FSM.final_states(fsm)
true
@spec initial_state(t()) :: Yog.node_id() | nil
Returns the initial state ID, or nil if none is defined.
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_initial_state(:idle)
iex> Choreo.FSM.initial_state(fsm)
:idle
@spec initial_states(t()) :: MapSet.t(Yog.node_id())
Creates a new empty FSM.
Options
:directed— whether the underlying graph is directed (default:true):strict— if true, raises on transition definitions with non-existent states (default:false)
Examples
iex> fsm = Choreo.FSM.new()
iex> fsm.graph.kind == :directed
true
Prunes unreachable and dead states, returning a smaller equivalent FSM.
- Unreachable states — no path from any initial state
- Dead states — no path to any final state
Examples
iex> fsm =
...> Choreo.FSM.new()
...> |> Choreo.FSM.add_initial_state(:a)
...> |> Choreo.FSM.add_state(:b)
...> |> Choreo.FSM.add_state(:trap)
...> |> Choreo.FSM.add_final_state(:c)
...> |> Choreo.FSM.add_transition(:a, :c, label: "go")
iex> pruned = Choreo.FSM.prune(fsm)
iex> :a in Choreo.FSM.states(pruned)
true
iex> :b in Choreo.FSM.states(pruned)
false
iex> :trap in Choreo.FSM.states(pruned)
falseDiagram
@spec remove_final_state(t(), Yog.node_id()) :: t()
Removes a state from the set of final states.
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_final_state(:done) |> Choreo.FSM.remove_final_state(:done)
iex> :done in Choreo.FSM.final_states(fsm)
false
@spec remove_initial_state(t(), Yog.node_id()) :: t()
Removes a state from the set of initial states.
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_initial_state(:idle) |> Choreo.FSM.remove_initial_state(:idle)
iex> Choreo.FSM.initial_state(fsm)
nil
@spec remove_state(t(), Yog.node_id()) :: t()
Fully removes a state and all transitions involving it from the FSM.
If the state was the initial state, the initial state is cleared. If the state was a final state, it is removed from the final states set. If the state does not exist, the FSM is returned unchanged.
Examples
iex> fsm =
...> Choreo.FSM.new()
...> |> Choreo.FSM.add_state(:a)
...> |> Choreo.FSM.add_state(:b)
...> |> Choreo.FSM.remove_state(:a)
iex> :a in Choreo.FSM.states(fsm)
false
iex> :b in Choreo.FSM.states(fsm)
true
@spec states(t()) :: [Yog.node_id()]
Returns all state IDs in the FSM.
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_state(:a) |> Choreo.FSM.add_state(:b)
iex> Enum.sort(Choreo.FSM.states(fsm))
[:a, :b]
@spec theme( atom(), keyword() ) :: Choreo.Theme.t()
Returns a theme for Choreo.FSM.
Examples
iex> theme = Choreo.FSM.theme(:default, graph_rankdir: :tb)
iex> theme.graph_rankdir
:tb
Renders the FSM to DOT format.
Options
:theme—:default,:dark, or aChoreo.Themestruct
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_state(:a)
iex> dot = Choreo.FSM.to_dot(fsm)
iex> String.contains?(dot, "digraph")
true
iex> String.contains?(dot, "a")
true
Renders the FSM to Mermaid.js flowchart syntax.
Options
:theme—:default,:dark,:warm,:forest,:ocean, or aChoreo.Themestruct:direction—:lr(default),:td,:rl,:bt
Examples
iex> fsm = Choreo.FSM.new() |> Choreo.FSM.add_state(:a)
iex> mermaid = Choreo.FSM.to_mermaid(fsm)
iex> String.contains?(mermaid, "graph LR")
true
iex> String.contains?(mermaid, "a")
true
@spec transitions(t()) :: [{Yog.node_id(), Yog.node_id(), String.t()}]
Returns all transitions as {from, to, label} tuples.
Examples
iex> fsm =
...> Choreo.FSM.new()
...> |> Choreo.FSM.add_state(:a)
...> |> Choreo.FSM.add_state(:b)
...> |> Choreo.FSM.add_transition(:a, :b, label: "go")
iex> Choreo.FSM.transitions(fsm)
[{:a, :b, "go"}]