Exploring state machine reachability
For a model with behavior over time – a state machine writing to attributes –
evaluate answers reachability questions: which values can an attribute take as the
machine runs? Each reachable value comes with a trace: the step-by-step run that
reaches it, so you can see how the machine got there.
Create thermostat.sysml. A two-state machine starts in Idle and flips a flag
whenever it enters Heating:
package Thermostat {
private import ScalarValues::Integer;
part thermostat [1] {
attribute heaterOn : Integer := 0;
exhibit state behavior {
entry; then Idle;
state Idle;
state Heating {
entry assign heaterOn := 1;
}
transition Idle then Heating;
transition Heating then Idle;
}
}
assert constraint heaterCanRun { Thermostat::thermostat::heaterOn == 1 }
}
Can heaterOn become 1? Ask for its reachable values. Two extra options come into
play:
--depthbounds how many steps of the machine’s runevaluateexplores. The machine here can loop forever, so the listing of reachable values needs a horizon. (checkdoes not need one – see below.)--alt-semantics freereads the model closed-world: values change only when the machine writes them, and nothing outside the model interferes. Without it, the specification’s default reading lets an unwritten snapshot hold any value, which buries the answer in irrelevant possibilities. See Adding assumptions.
syside solver evaluate -i thermostat.sysml \
--expr "Thermostat::thermostat::heaterOn" \
--depth 3 --alt-semantics free
{
"result": {
"subject": "Thermostat",
"expression": "Thermostat::thermostat::heaterOn",
"determination": "undetermined",
"outcome": {
"shape": "approximated",
"proven": [
{
"value": "0",
"trace": [
{ "index": 0,
"values": [["Thermostat::thermostat::heaterOn", "0"]] },
{ "index": 1,
"values": [["Thermostat::thermostat::heaterOn", "1"]] },
{ "index": 2,
"values": [["Thermostat::thermostat::heaterOn", "1"]] },
{ "index": 3,
"values": [["Thermostat::thermostat::heaterOn", "1"]] }
]
},
{
"value": "1",
"trace": [
{ "index": 0,
"values": [["Thermostat::thermostat::heaterOn", "0"]] },
{ "index": 1,
"values": [["Thermostat::thermostat::heaterOn", "1"]] },
{ "index": 2,
"values": [["Thermostat::thermostat::heaterOn", "1"]] },
{ "index": 3,
"values": [["Thermostat::thermostat::heaterOn", "1"]] }
]
}
],
"exhaustive": "undetermined",
"integral": true
},
"causes": []
},
"alt_semantics": [ "..." ],
"warnings": [],
"exit_code": 0
}
(The alt_semantics section, elided here, spells out every override that free
switched on.)
How to read this:
The
shapeis"approximated": for a machine that can run forever, the Solver reports what it proved reachable within the explored depth, rather than claiming a complete set.provenlists the values:0(the initial value, before the machine first entersHeating) and1(after it does). Both are genuinely reachable.Each value’s
traceis a run of the machine, snapshot by snapshot:heaterOnstarts at 0 at step 0 and is 1 from step 1 on – the machine took theIdle then Heatingtransition on its first step."exhaustive": "undetermined"is honest bookkeeping: within depth 3 nothing beyond 0 and 1 was reached, but the Solver does not claim that a longer run could never produce more.
Checking a reachability assertion
The same model carries an assertion, heaterCanRun. Remember from Checking that requirements hold
that check asks a universal question – “does this hold in every allowed run?” –
which is the wrong shape for “can the heater run?”. A machine that never leaves
Idle is an allowed run, so the universal claim fails while the reachability answer
is yes.
For a “can it happen?” question, use evaluate as above and look for the value among
proven. Use check for the mirror-image question, “is this always true?” – for
example an invariant like “the flag is never 2”:
syside solver check -i thermostat.sysml \
--predicate "not (Thermostat::thermostat::heaterOn == 2)" \
--alt-semantics free
{
"result": {
"subject": "Thermostat",
"expression": "not Thermostat::thermostat::heaterOn == 2",
"determination": "determined",
"outcome": { "shape": "predicate", "status": "satisfied" },
"causes": []
},
"warnings": [],
"exit_code": 0
}
(The alt_semantics section is elided.)
check verdicts hold at any depth
Note what just happened: no --depth, on a machine that can run forever – and the
verdict is determined, not an approximation. A check verdict on a state machine
is not “satisfied as far as we looked”:
satisfied means the Solver proved the condition for runs of any length, including runs longer than anything it explicitly explored. This works even when the values themselves grow without bound: for a machine that increments a counter forever,
check --predicate "... ticks + 1 > 0"still comes backsatisfied.violated comes with a finite
trace– a concrete run reaching the violation – which is a complete proof by itself, however deep it lies. Asking the incrementing counter whetherticks < 5always holds returnsviolatedwith the run that reaches 5, with no depth setting.unknown is the honest third verdict: the Solver could neither prove the condition for all runs nor find a violating run. The exit code stays 0;
determinationisunknown. Treat it like an unanalyzable assertion (see Handling unsupported syntax): the tool is telling you it does not know, not that the model is fine.
evaluate is different: a reachable-value listing for a machine that can run forever
is always an approximation ("exhaustive": "undetermined"), and --depth sets how
far it explores. A too-small depth can miss values that need more steps to reach (they
simply will not appear in proven); a larger depth explores more and runs longer.
Start small, and increase the depth if a value you expect to be reachable is missing.