Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Basic lifecycle

An FSM puts lifecycle topology in the model. It declares an initial state, allowed transitions, and a reachable final state.

YAML model

quent: alpha
model: finite_state_machine

fsms:
  Job:
    states:
      queued:
        initial: true
        to: [loading_input, restoring_checkpoint]
      loading_input:
        to: [running]
      restoring_checkpoint:
        to: [running]
      running:
        to: [completed]
      completed: {}

The parser rejects missing initial states, unreachable states, invalid targets, and FSMs without a reachable final state. It also derives event cardinality from the topology.

Instrumentation API

Entering a state emits its generated event. The generated API represents the current FSM state in the handle’s type. Each transition consumes that handle and returns a handle for the target state. Only transitions allowed from the current state are available to the compiler or type checker. This pattern is called typestate.

The Rust and C++ examples show the state-specific handle types directly. The generated Python type stubs expose the same transition constraints to type checkers and editors.

When control flow must return an FSM in a state selected at runtime, use a dynamic-state handle (see next lesson).

use instrumentation::{Context, FiniteStateMachine, Job, Noop};

#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<FiniteStateMachine>::try_new(Noop)?;
    let _job = context
        .observer::<Job>()
        .handle()         // FsmHandle<Job>
        .queued()         // FsmHandle<Job, job_state::Queued>
        .loading_input()  // FsmHandle<Job, job_state::LoadingInput>
        .running()        // FsmHandle<Job, job_state::Running>
        .completed();     // FsmHandle<Job, job_state::Completed>

    Ok(())
}
#include "quent-tutorial-finite-state-machine-cpp-bridge/gen/quent.hpp"

int main() {
  auto context = quent::Context::none();

  quent::FsmHandle<quent::Job> job = context.job_observer()->handle();

  // A transition consumes its move-only state handle and returns the handle
  // type for the new state. Named C++ objects require std::move to be consumed.
  quent::FsmHandle<quent::Job, quent::job_state::Queued> queued =
      std::move(job).queued();
  quent::FsmHandle<quent::Job, quent::job_state::LoadingInput> loading =
      std::move(queued).loading_input();
  quent::FsmHandle<quent::Job, quent::job_state::Running> running =
      std::move(loading).running();
  quent::FsmHandle<quent::Job, quent::job_state::Completed> completed =
      std::move(running).completed();
  return 0;
}
import quent_tutorial_finite_state_machine as quent


def main() -> None:
    with quent.Context() as context:
        completed = (
            context.job_observer()
            .handle()
            .queued()
            .loading_input()
            .running()
            .completed()
        )


if __name__ == "__main__":
    main()
Key point

The generated handle exposes only the transitions allowed from its current state.

Check yourself

Which property does the parser validate for this FSM?

Which states can directly precede running?

Full code