Exhaustive state-space model checker for ExTLA.Spec modules. Analogous to TLC
for TLA+.
The search is breadth-first, and that is not an implementation detail: an invariant is
evaluated when a state is first discovered, so the first violation found is at minimal
distance from init and the trace is the shortest counterexample. A depth-first search
would report a counterexample, which is a much less useful thing to hand someone.
check_liveness: true adds temporal checking: the search also builds the liveness graph,
and ExTLA.ModelChecker.Liveness looks for the fair cycle that refutes a temporal
property. It is off by default, because the graph costs memory and time and a spec that
declares no temporal has nothing to gain from it. mix extla.check turns it on for a
spec that declares one.
symmetry: [:processes] collapses states that differ only by a permutation of an
interchangeable constant, and verify_symmetry: true checks the declaration was sound by
running the model both ways (ExTLA.Symmetry). workers: n explores each level across
n processes (ExTLA.ModelChecker.Parallel). Either one together with
check_liveness: true raises, because neither combination gives a sound liveness answer.
run/2 lists every option.
Summary
Functions
Every parameter binding of an action in a state, guard or no guard.
Run only safety checking: run/2 with check_liveness: false, whatever the config says.
Every invariant a run will check: the spec's own, preceded by the implicit
:type_ok of 02-dsl-semantics.md §1.5 when any variable is typed.
Every parameter binding of an action whose guard holds in a state.
Every {action, binding} a state admits — the choices available from it, without
applying any of them.
Run the model checker and return the full state graph, for inspection.
Run the model checker on a spec module.
Every {action, binding} in a state, guard or no guard.
Every successor of a state: one per action, per binding of that action's params, whose guard holds.
Types
@type config() :: %{ optional(:constants) => map(), optional(:max_states) => pos_integer(), optional(:workers) => pos_integer(), optional(:check_deadlock) => boolean(), optional(:check_liveness) => boolean(), optional(:liveness_graph) => boolean(), optional(:symmetry) => [atom()] | nil, optional(:verify_symmetry) => boolean(), optional(:state_constraint) => (ExTLA.State.t() -> boolean()) | nil, optional(:fingerprint_bits) => 64 | 128, optional(:profile) => boolean(), optional(:invariants) => :all | [atom()], optional(:action_properties) => :all | [atom()], optional(:temporals) => :all | [atom()] }
@type result() :: %{ status: :ok | :violation | :max_states_reached | :constrained | :deadlock, states_found: non_neg_integer(), distinct_states: non_neg_integer(), transitions: non_neg_integer(), diameter: non_neg_integer(), deadlock: boolean(), duration_ms: non_neg_integer(), violation: ExTLA.Trace.t() | ExTLA.Trace.lasso() | nil, coverage: ExTLA.Coverage.t(), profile: ExTLA.Profile.t() | nil, warnings: [String.t()], fingerprint_bits: pos_integer(), collision_estimate: float() }
Functions
Every parameter binding of an action in a state, guard or no guard.
Run only safety checking: run/2 with check_liveness: false, whatever the config says.
Every invariant a run will check: the spec's own, preceded by the implicit
:type_ok of 02-dsl-semantics.md §1.5 when any variable is typed.
type_ok goes first deliberately. A state outside its declared type makes
every other invariant's verdict on that state uninteresting — the useful thing
to report is that a variable holds something the spec says it cannot.
Every parameter binding of an action whose guard holds in a state.
Params bind left to right and each generator receives the bindings made before it, so a later param can be constrained by an earlier one. A generator that returns nothing disables the action here, exactly as a false guard would.
Every {action, binding} a state admits — the choices available from it, without
applying any of them.
successors/3 answers the same question and applies the transitions, which is what the
search needs and what a caller choosing ONE of them does not: ExTLA.Simulation.Generator
picks a candidate and asks the refinement for a message, and
ExTLA.PropertyTest.action_sequences/3 picks one and walks. Both use this, so a
simulation, a property test and the checker agree on what may happen next.
Actions are taken in declaration order and bindings in generator order, so the list is the same on every run.
Run the model checker and return the full state graph, for inspection.
The caller owns the returned graph. It is backed by two ETS tables created in the
calling process, and they live until that process exits — which is the point: the graph
is the return value, and releasing it here would hand back a term whose contents are
gone. run/2, which discards the graph, is where the release belongs, and it does it by
searching in a process of its own.
A caller that keeps a graph beyond one short-lived process should say when it is done
with it: ExTLA.StateGraph.delete/1.
Run the model checker on a spec module.
ExTLA.ModelChecker.run(Specs.Counter, %{constants: %{max: 3}})Options
constants:— a map assigning the spec's constants. Default%{}. Each constant'sdefault:fills in what this leaves out, and an explicit value wins.run/2reads no application config:mix extla.checkaddsconfig :extla, Spec, constants: %{…}withExTLA.Spec.Compiled.resolve_constants/1and passes the result here.max_states:— the state budget. Default1_000_000. The search stops once the graph has passed it, so the count reported with:max_states_reachedexceeds the limit.workers:— processes exploring each breadth-first level. Default1;ExTLA.Config.workers/0returns the machine's answer if you want it. Raises together withcheck_liveness: true.check_deadlock:— report a reachable state with no enabled action and noterminaladmitting it. Defaulttrue.check_liveness:— check the spec'stemporalproperties. Defaultfalse. Raises together withsymmetry:orworkers:above one.liveness_graph:— build the liveness graph without checking anything with it, for measuring it. Defaultfalse;check_liveness: trueimplies it.symmetry:— constants whose elements are interchangeable. Defaultnil.verify_symmetry:— run the model with and withoutsymmetry:and raiseExTLA.Symmetry.UnsoundErrorif any verdict differs. Defaultfalse.state_constraint:— a predicate on states; a state that fails it is not expanded, and the run's status is:constrained. Defaultnil.fingerprint_bits:—64or128. Default64.profile:— measure what each declaration costs, in reductions (ExTLA.Profile). Defaultfalse, and free when off.invariants:,action_properties:,temporals:—:all(the default) or a list of declaration names to check. A name the spec does not declare is refused.
Returns the verdict and keeps nothing: the search runs in a process of its own, so the
graph's ETS tables are freed when it ends, and an exception is re-raised in the caller
with its original stack. explore/2 returns the graph instead.
Every {action, binding} in a state, guard or no guard.
The counterpart of bindings/3 to enabled_steps/3's enabled_bindings/3. A simulation
that sends only enabled messages can never find an implementation that accepts one the
spec forbids, which is why the unfiltered form is offered rather than assumed away.
Every successor of a state: one per action, per binding of that action's params, whose guard holds.
Nondeterminism comes from params: and only from params:
(02-dsl-semantics.md §3), so this is the whole of it. Actions are taken in
declaration order and bindings in generator order, which makes the search
deterministic — a checker whose state graph varied between runs could not be
compared to TLC.