14. Catching mistakes early: static validation¶
A statechart is a program, and like any program it can be wrong in structural ways — a
terminal with no verdict, a state nothing reaches, a selector branch that can never match.
validate finds those before you run the machine, surface-independent and pure.
validate returns findings¶
from harel import definition_from_dsl, validate
BROKEN = """
machine order {
initial Cart
state Cart {}
state Shipped {}
state Orphan {}
from Cart to Shipped on Deliver
from Orphan to Shipped on Rescue
}
"""
defn = definition_from_dsl(BROKEN, "order")
for issue in validate(defn):
print(issue)
[error] unknown_event at Cart: transition references undeclared event 'Deliver'
[error] unknown_event at Orphan: transition references undeclared event 'Rescue'
[warning] unreachable at Orphan: state is not reachable from the root
[error] terminal_missing_outcome at Shipped: terminal of the machine/region must declare an `outcome` (e.g. success / failed) — the verdict the model routes on
Distinct problems, each a structured Issue (severity, code, path, message):
Deliver/Rescueare referenced by transitions but never declared with aneventline — an error. Declaring your events up front turns them into a checked vocabulary, so a typo (Delvier) can’t slip through as a silent event that never fires.Shippedis a leaf sink that ends the execution but declares nooutcome— an error. This is the rule that backs step 3: the engine won’t guess a verdict, so it makes you state one where it’s consumed.Orphancan’t be reached from the initial state — a warning (dead model, probably a leftover or a missing edge).
What it checks¶
validate covers the structural traps, including:
every execution-ending terminal declares an
outcome;composites have a resolvable
initial;no non-deterministic automatic transitions;
selector branch targets resolve, and declared
returns {…}branches are exhaustive (no phantom or missing branch);every referenced event is declared (and, for events with fields, the field references exist and the operators match) — an undeclared event is an error;
timeoutshape, and a warning for an unhandledTimeout;unreachable states.
Fix it, and fail the build on errors¶
The fixed machine declares the verdict and drops the dead state — validate returns no
findings. Pass validate=True to fail the build on any error, so a broken machine never even
compiles:
from harel import definition_from_dsl, validate
FIXED = """
event Deliver {}
machine order {
initial Cart
state Cart {}
final Shipped success {}
from Cart to Shipped on Deliver
}
"""
print("findings:", validate(definition_from_dsl(FIXED, "order")))
defn = definition_from_dsl(FIXED, "order", validate=True) # raises ValidationError on any error
print("built:", defn.id)
findings: []
built: order
validate=True raises ValidationError if any error-severity issue is present; warnings don’t
block. Wiring validate into your machine-loading path (as the bundled worker does) closes the
gap between “it parsed” and “it’s a sound machine”.
That’s the model¶
You now have the whole authoring surface: states and transitions, actions, outcomes, guards,
selectors, hierarchy, timers, orthogonal regions, fragments, imports, submachine invoke,
fan-out, payloads, and validation.
The rest of the documentation is the operations side — how these machines run for real:
Visualization & tooling — diagrams, the language server, the live preview.
Durability — persisting executions across crashes; the store backends.
Distribution — running machines across many workers.
Control plane — cancel, suspend, resume, terminate.
…plus a DSL reference and the public API.