Why state machines are first-class
Recoil makes finite state machines
(fsm!) and streaming transducers
(fst!) into real types. Their states, events, guards, and payload
types are checked at compile time and lowered to plain C — no library, no
interpreter, no hand-rolled switch soup.
This is unusual: most languages leave state machines to a library or to error-prone manual encoding. In Recoil a machine is a declaration the compiler understands, so invalid transitions and payload mismatches are caught before you run anything. Combined with PARSE, FST gives you a type-safe pipeline from raw bytes to structured values.
FSM — core model
An fsm! type declares optional fields: (machine-local
storage), events: (named transitions with typed payloads), and one
block per state. The first state is the initial state.
TrafficLight!: make fsm! [
events: [
go ["Switch to green"]
slow ["Switch to yellow"]
stop ["Switch to red"]
]
red [go => green]
green [slow => yellow]
yellow [stop => red]
]
light: make [#mutable TrafficLight!] TrafficLight!/red
send light go []
send light slow []
send light stop []
Fields, events & payloads
fields: adds machine storage shared across transitions.
events: declares each event's doc string and typed payload
parameters; the payload is supplied in the block passed to send.
Counter!: make fsm! [
fields: [count: i32!]
events: [
inc ["Increment" n [i32!]]
reset ["Reset"]
]
zero [
inc => one [count: count + n]
]
one [
inc => one [count: count + n]
reset => zero [count: 0]
]
]
c: make [#mutable Counter!] Counter!/zero
send c inc [5]
Transitions, guards & actions
A transition is event => target, optionally guarded with
when [cond], with a catch-all else => target. An
optional action block can mutate fields, assign from calls, or make bare void
calls.
Gated!: make fsm! [
fields: [ready: i32!]
events: [try [] arm []]
locked [
try when [ready > 0] => unlocked
arm => locked [ready: 1]
else => locked
]
unlocked []
]
send and state?
Dispatch an event with send machine event [payload]. Inspect the
current state with state? machine Type!/state-name, which returns a
logic!.
send light go []
print state? light TrafficLight!/green
FST — streaming transducers
An fst! consumes an input series and emits typed
values as it goes. It declares emits: (the required sum type every
emit produces), optional fields:, and one block per
state whose transitions are [parse-rule] => target [action]. The
first state is initial; a state with no transitions is terminal.
Token!: make sum! [
ident
num [value: i32!]
]
Lexer!: make fst! [
emits: Token!
fields: [count: i32!]
scanning [
["A"] => scanning [count: count + 1 emit Token!/num count]
[" "] => scanning
]
finished []
]
emit
Each transition's parse rule tests the current source head; a match consumes
those bytes and fires the transition. The action can emit values:
- atomic:
emit Token!/num count - 1-to-N (push several at once):
emit [Token!/a Token!/b]
A terminal state (no transitions) short-circuits take to
done.
transduce/lazy and take
Build a lazy iterator with transduce/lazy Type! source and pull
one value at a time with take, which returns an auto-registered
<Type>-take-result! sum you dispatch on with
match:
drive: func [] [
lex: transduce/lazy Lexer! "AAA"
result: take lex
match result [
ok [v] [print 1]
need-more [] [print 8]
done [] [print 9]
fail [err] [print 7]
]
]
The sum has variants ok [value: <emits>],
need-more [], done [], and
fail [err: error!].
Parse + FSM integration
Parse results drive control flow, and parse paren actions can send real FSM
events. state? can then choose the next parse rule — enough to
build a complete protocol loop (read → parse → send).
parse "GET /path" [
"GET" (send conn got-method [])
" "
thru end
]
either state? conn Conn!/reading-body [
parse "payload" [thru end (send conn got-body [])]
] [
print "still waiting for method"
]
See the repository's Parse + FSM integration doc for the full pattern catalogue.