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()
The generated handle exposes only the transitions allowed from its current state.