Skip to content

Input event driven state machines

Arsène von Wyss edited this page Dec 22, 2018 · 1 revision

State machines are powerful, but tedious and verbose to completely write by hand - large switch statements or chains of if-then-else, manual assignment of state numbers, keeping track of state machine context data...

A fluent API for defining input/event-driven state machines is part of the Sirius.Common library. After defining all the states, inputs, actions and transitions, a compilable state machine lambda expression can be emitted. The state machine function has the following signature:

bool StateMachineFunc<in TInput>(TInput input, ref int state, ref object context)

The initial state is always 0 but will change as the state machine transitions between states based on the input. The context is the (optional) context data to be used by the state function. Initialize the context data before the first state function call if you use it! Note that the state function may change the context data depending on the state actions being visited, which is why this is a ref parameter.

A simple state machine for counting newline characters

// Define the state machine
var root = new StateSwitchBuilder<char, char, int>();
root.On('\n').Do(i => i+1).Yield(root);
root.Default.Yield(root);

// Compile the state machine
var emitter = new StateMachineEmitter<char, char>(root, EquatableConditionEmitter<char>.Default);
var stateExpr = emitter.Emit();
var stateFn = stateExpr.Compile();

// Run the state machine for some input
var state = 0;
object context = 0;
foreach (var ch in input) {
	stateFn(ch, ref state, ref context);
}
// The context is now an integer with the number of newlines

Any number of On can be added to switch-states (a switch-state is the part of the state which decides what to do based on the input). The Default on each switch-state is performed whenever no On-match was hit. If the default is not redefined, the state machine will go to the break state (-1) by default.

After the On (or Default), any number of synchronously performed operations can be added with Do. These operations can use the context data and optionally the current input, and they can change the context (by returning a value from the lambda expression). Note that not only the value of the context can change - the type can change as well. Finally the state machine goes to another state and waits for the next input (Yield), it can immediately resume with another state (Goto), or it can break (e.g. error or end of input, using Break or nothing).

Note that no state identifiers are required; these will automatically be assigned upon emitting the state machine to an expression. Emitting will build one expression which incorporates the body of the expressions defined for the Do operations, and the comparison of input against the comparand will be emitted by a matching IConditionEmitter<,>. This allows to use complex comparands such as Range<> and RangeSet<> instead of using plain equality only.

Clone this wiki locally