Statecharts · Lesson 05
Context, Guards, and Run-to-Completion
What does not belong in a box, how the chart asks questions about it, and the exact order in which an engine does everything one event sets off — which is where the subtle bugs live.
Not everything is a box
An elevator with a queue of pending floors has no finite state space. Ten floors, any subset pending, in any order — that is millions of configurations and none of them is a useful thing to draw. So the queue is not states. It is a list, hanging off the machine, and the chart only ever asks it yes/no questions.
The working rule, and it settles most arguments:
The failure mode in both directions is worth naming. Making context into states gives
you retry_1, retry_2, retry_3 and a fourth one next sprint.
Making states into context gives you the boolean soup from
lesson 01
— isLoading, isError, hasData — where the illegal
combinations are representable and reachable.
Guards, and the order they are tried
Several transitions can share an event. They are tried in declaration order and the first whose guard passes fires:
tick = (
cooking.to(idle, cond="last_second") # tried first
| cooking.to.itself(internal=True, on="decrement")
)
This is a deliberate priority list, not a switch whose arms happen to be
exclusive. Write the specific case above the general one, and treat two simultaneously-true guards on
the same event as a bug even though the engine will quietly pick the first — the reader of the
diagram has no way to see which.
A transition can also have no event at all. An eventless transition fires the moment its guard becomes true, with nobody sending anything:
class motion(State.Compound):
idle = State(initial=True)
moving_up = State()
idle.to(moving_up, cond="safe_to_move and target_is_above") # no name, no event
That is the elevator's dispatcher, and no caller has to remember to start the car. Eventless transitions are how a statechart expresses “this is a standing condition” rather than “this is a thing that happens”.
One event at a time, to completion
Everything above depends on the engine's processing model, which is worth knowing exactly because it is where the subtle bugs are.
send(..., internal=True), SCXML's raise) is drained inside the current
macrostep; the external queue (send()) waits until the machine is stable.This is run-to-completion: an event is fully processed, including every consequence it sets off, before the next one is looked at. It is what makes a statechart's behaviour predictable without locks, and it is why an action that sends an event does not re-enter the machine.
The fix is general: if two regions must agree about something at the instant it becomes true, that something has to be visible to both at that instant — a value in context that the guard reads, not an event still sitting in a queue.
When not to reach for this
Three cases, offered as synthesis rather than as anything Harel wrote:
Exercise 1 — state, context, or guard
Exercise 2 — run the macrostep
Exercise 3 — recall
The rest is practice
That is the whole vocabulary. What remains is judgement, and judgement comes from problems: P3 for regions and context under the run-to-completion rule, P4 for living with someone else's flat machine and then refactoring it, and P5 for all of it at once.
Sources: SCXML Appendix D · python-statemachine: processing model · Harel 1987 §5 · RESOURCES.md