Recoil logo

Recoil

State machines & streaming transducers

  • Home
  • Getting Started
  • Guide
  • Reference
  • PARSE
  • State Machines
  • FFI & Systems
  • Ports
  • Packages
  • f00 & Embedded
  • Examples
  • Internals

On this page

Loading…

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.

↑

Recoil — an ownership-first, statically-typed language with a static borrow checker.

codeberg.org/rebolek/recoil · boleslav@brezovsky.eu · Apache-2.0