Chapter 6: State Machines
Learning Objectives
- Model workflow transitions with explicit state structs/enums.
- Keep transition logic deterministic and testable.
- Use bounded history for predictable memory behavior.
- Apply
#[must_use]mindset to state-returning APIs.
Concept Introduction
State machines are one of Rust’s strongest design fits because enums make state
space explicit and match forces transition handling to stay exhaustive. In
Fireside, traversal is not just incrementing an index: it must respect explicit
next overrides, branch choices, rejoin semantics (after), and backtracking.
A state machine expresses these rules without hidden mutable side effects.
The central idea is to represent state and transitions as data, not scattered
conditionals. TraversalEngine stores current and history, then exposes
operations (next, back, goto, choose) that each return a typed
TraversalResult. This keeps call sites simple: they react to either
Moved { from, to } or AtBoundary.
Bounded history is a practical systems concern. Unlimited stacks can grow over
long sessions. By capping history with VecDeque and pruning oldest entries,
Fireside keeps memory predictable while preserving recent navigation context.
This is a good example of policy encoded at the state-machine layer rather than
left to ad hoc cleanup.
Error handling also belongs in transitions. Invalid go-to indices and branch key
mismatches return typed EngineError values instead of silent no-ops inside the
engine. The app may choose to ignore or display them, but correctness starts at
the state machine boundary.
A final pattern: keep transition internals private. push_history in traversal
centralizes retention policy, so all movement methods share the same invariant.
That prevents drift where one transition path forgets pruning or logging.
Fireside Walkthrough
Source anchor: crates/fireside-engine/src/traversal.rs.
#[derive(Debug, Clone, PartialEq, Eq)]pub enum TraversalResult { Moved { from: usize, to: usize }, AtBoundary,}
const MAX_HISTORY: usize = 256;Why this design:
- Results are explicit and pattern-matchable.
- Bounded history avoids unbounded growth.
- Branch and override behavior is centralized in
next/choose.
Exercise
Add a test that repeatedly moves through a long generated graph and asserts
history length never exceeds MAX_HISTORY.
Verification
Run:
cargo test -p fireside-engine traversalWhat would break if…
If transitions directly mutated current from multiple modules without a single
engine API, behavior would diverge quickly: some paths might ignore branch
rejoin rules, others might skip history updates, and backtracking correctness
would become accidental.
Key Takeaways
State machines make behavior legible and robust. In Rust, enums and explicit results are ideal for this pattern because the compiler enforces complete handling. Centralized transition APIs, bounded history, and typed errors produce navigation logic that is easy to test and hard to corrupt.