Actions and state machines are executed, not just parsed. A debugger steps through
them, and the non-interactive -action and -state flags run them to completion and report the
values they produce. A behavior can be performed by an object, in which case the messages it sends
are routed over that object's connections.
Action execution (step-by-step):
sysml> action SimpleWorkflow {
...> attribute result = 0;
...> first start;
...> then action compute { assign result := 42; }
...> then done;
...> }
✓ action SimpleWorkflow
sysml> %action SimpleWorkflow
✓ Started action executor for "SimpleWorkflow"
State: Running
Tokens: 1
Use %step to advance, %tokens to inspect, %continue to run to completion
sysml> %step
✓ Step complete
State: Running
Tokens: 1
sysml> %tokens
Active tokens (1):
Token 1 @ compute
Values:
result = 0
sysml> %continue
✓ Action completed
Final state: Completed
Results:
result = 42
State machine execution:
A machine completes when a transition reaches done, the terminal state the standard
library provides for every state machine. Entering it runs the exit actions, and then the
machine reports itself completed. With orthogonal regions, each region has its own done,
and the machine completes only once every region has reached it.
sysml> state TrafficLight {
...> entry; then start;
...> state start;
...> state green { accept after 25 [SI::s] then yellow; }
...> state yellow { accept after 5 [SI::s] then red; }
...> state red { accept after 30 [SI::s] then done; }
...> succession first start then green;
...> }
✓ state TrafficLight
sysml> %state TrafficLight
✓ Started state machine executor for "TrafficLight"
Current state: start
Time: 0.0
Events: 1
Use %events to see queue, %current for state, %advance <time> to step
sysml> %advance 25
✓ Advanced to 25.0 (2 event(s) processed)
Current state: yellow
Last event at: 25.0
Remaining events: 1
sysml> %current
Current state: yellow
Time: 25.0
Last event at: 25.0
Execution state: Running
sysml> %advance 5
✓ Advanced to 30.0 (1 event(s) processed)
Current state: red
Last event at: 30.0
Remaining events: 1
sysml> %advance 30
✓ Advanced to 60.0 (1 event(s) processed)
Current state: done
Last event at: 60.0
Remaining events: 0
✓ State machine completed (a transition reached `done`)
Sending a signal. A transition that waits on an accept is driven from the prompt with
%send, which puts the signal on the runtime's message bus exactly as a send from an action
would, so nothing has to be written in the model just to fire it:
sysml> package Lamps {
...> private import ScalarValues::*;
...> attribute def go;
...> attribute def Dim { attribute level : Integer; }
...> state def Lamp {
...> attribute brightness : Integer = 0;
...> entry; then off;
...> state off;
...> transition off_on first off accept go then on;
...> state on;
...> transition on_dim first on accept d : Dim do assign brightness := d.level then dimmed;
...> state dimmed;
...> }
...> part def Bulb { exhibit state lamp : Lamp; }
...> part bulb : Bulb;
...> }
✓ package Lamps
sysml> %instantiate bulb
✓ Created instance of Lamps::bulb
ID: 1
Use %features bulb to inspect
sysml> %state bulb
✓ Debugging state machine "lamp" exhibited by object #1 of "Lamps::bulb"
Current state: off
Time: 0.0
Events: 0
sysml> %send go
✓ Sent go to object #1 of "Lamps::bulb"
Accepted by state machine "Lamp" in state off
Use %step or %advance <time> to dispatch it
sysml> %events
Signals in flight: 1
go
Use %advance <time> to process next event
sysml> %advance 1
✓ Advanced to 1.0 (1 event(s) processed)
Current state: on
Last event at: 0.0
Remaining events: 0
sysml> %send Dim(level=3+4)
✓ Sent Dim(level=7) to object #1 of "Lamps::bulb"
Accepted by state machine "Lamp" in state on
sysml> %step
✓ Event dispatched
Current state: dimmed
Time: 0.0
Events: 0
sysml> %send go
error: object #1 of "Lamps::bulb" accepts no signal go now: state machine "Lamp" in state dimmed
Without to <object>, the signal goes to the object whose machine the %state session is
debugging (%send go to bulb names it explicitly, and is the form to use when no session is
active; the object is any object reference, to #1 or to rack.lamp included). Payload features
are written <parameter>=<expression> as for %invoke, and are checked against the signal's
declaration: %send Dim(lvl=1) is refused because Dim carries no lvl. A
signal nothing in the machine's current state accepts is refused up front, with the state named,
rather than queued to be silently dropped — and so is one whose every triggered transition is
held back by its guard, decided as the dispatch would decide it, with the payload bound: with
transition on_dim first on accept d : Dim if d.level > 0 ..., %send Dim(level=0) is refused
while %send Dim(level=3) is in flight. A guard that cannot be evaluated is a %send error. If
the state or the data a guard reads changes between the send and the dispatch, the %step or
%advance that drops the signal says so.
Action debugging commands:
%action <name> [<object>]— Start an action debugging session, optionally performed by an instantiated object%step— Advance all tokens one step%continue— Run to completion, or to the first breakpoint hit%tokens— Show active tokens with data%break <node>— Set breakpoint on a named node, one anifbranch or a loop body declares included;%continuestops when a token reaches it, or before a body performs it%stop— Stop debugging
State machine debugging commands:
%state <name> [<object>]— Start a state machine debugging session; naming an instantiated object runs the machine on behalf of that object, so what it sends routes over that object's connections. Naming the machine the object exhibits attaches to its running machine instead (see below)%send <signal>[(<p>=<expr>, ...)] [to <object>]— Send a signal to an object's machine over the runtime's message bus; by default to the object being debugged%events— Show event queue and signals in flight%current— Show current state, stack, data%advance <time>— Advance simulation time by<time>units, processing every event due%stop— Stop debugging
Choice points. The library orders some things and leaves others open: a succession says
which step comes first, but nothing says which of two fork branches steps first, which of two
waiting accepts takes the one message both answer to, which of two holding guards a decision
follows, which of two transitions out of one state fires on the same event, or whose write
stands when several branches assign one feature in one step. Where the
executor has to pick, it follows one fixed rule — reverse token order, first holding guard, first
declared transition, so a run replays exactly — and records a choice point rather than passing
the pick off as the only outcome. %step, %continue and %advance end with a count of the
choices they made (2 choice points; %trace on to see them), %trace on shows each as a
choice line naming the alternatives and the one taken (choice step 3: tokens 2@left, 3@right (unordered; took 3@right first)), and the gRPC responses carry each as an informational
diagnostic. A run with no choice points has the one outcome the model states; one with choice
points has the outcome this executor's rule produces, and the lines say where another rule would
diverge. The innermost-transition-wins rule between a substate and the state enclosing it is
spec-defined order, not a choice, and is not reported. Reporting never changes the run: once a
guard or transition holds, the ones after it are read in a preview that is undone, and one that
cannot be evaluated there — a division by zero, say — is not an alternative and not an error (a
guard with no result is not true, so its branch is not taken); it is counted beside the choices
(1 guard not evaluable) and shown in the trace as an unevaluable guard line. The first guard
read is the run's own, and its failure fails the run as it always has.
Scheduling policies. The fixed rule is one scheduling policy, named reverse, and the
executor can be told to resolve every choice point under another: declared takes tokens in the
order they were spawned and guards and transitions in declaration order, and seed:<n> draws each
pick from a pseudo-random sequence the non-negative integer n fixes, so seed:1 replays the same
run every time and on every platform while seed:2 may take another linearization. The policy is
spelled the same everywhere — sysml -schedule declared for -action, -state and -analysis
(a calc's body performs nothing, so -calc has no choice to make), %schedule seed:7 in the
REPL for the runs started after it (a debugging session under way keeps its own), a schedule
field on the gRPC execution requests, and a schedule pin on a conformance case — and changes
only which alternative each choice takes: every choice point the run reaches is reported, and
each took … is what the named policy took, so running a model under two policies and comparing
the outcomes is how a scheduling artefact is told from a bug. A guard the policy picks past the
first was only previewed, so the run reads it once more for real before taking its branch (the
trace shows that reading), as a transition's guard is always read again as it fires. Another
linearization can reach
other choice points — which tokens are steppable in a step depends on the order the earlier ones
moved — so the count is not fixed across policies, only the reporting is. An unknown spelling —
random, seed without a number, seed:-1 — is refused before anything runs rather than falling
back to the default. Where the library orders the alternatives
— the innermost transition over its enclosing state's — there is no choice, and every policy
follows that order.
For complete workflows, see examples/action-executor-demo.sysml, examples/orthogonal-regions-demo.sysml and examples/pseudostates-demo.sysml.
A type that exhibits a state machine or performs an action binds that behavior to every object of the type: instantiating an object gives it an execution of its own, tied to its identity. Two objects of the same type run two independent machines, each with its own current state, event queue and feature values, and an assignment in a behavior body writes the feature value of the object performing it.
sysml> part def Monitor {
...> attribute count = 0;
...> exhibit state modes {
...> entry; then idle;
...> state idle {
...> entry action bump { assign count := count + 1; }
...> accept after 10 [SI::s] then awake;
...> }
...> state awake { entry action mark { assign count := count + 10; } }
...> }
...> action bumpBy { in n; action apply { assign count := count + n; } first apply; then done; }
...> }
✓ part def Monitor
sysml> %instantiate Monitor
✓ Created instance of Monitor
ID: 1
Use %features Monitor to inspect
sysml> %state Monitor
✓ Debugging state machine "modes" exhibited by object #1 of "Monitor"
Current state: idle
Time: 0.0
Events: 1
sysml> %step
✓ Event dispatched
Current state: awake
Time: 10.0
Events: 0
sysml> %features Monitor
Instance: Monitor (ID: 1)
Features:
count = 11
Behaviors:
modes: exhibited state machine, current state awake
bumpBy: action, not running
%instantiate started the machine, and %state Monitor attached the debugger to that object's
machine rather than to a detached run of the usage. %step, %advance, %current and %events
therefore drive that machine, and %features shows the values its entry actions wrote: 1 from
idle, then 10 more from awake once the timer fired. The machine and the operation are not
values the object holds, so they are listed under Behaviors: with what the object is doing with
each: the exhibited machine's current state is the one %current reports, and bumpBy, which the
type declares but does not perform, is not running.
The two-argument form does the same when the machine it names is the one the object exhibits:
%state Monitor::modes Monitor attaches to the running machine and says so in a note: line,
rather than performing modes a second time against the same feature values (which would run
its entry actions again, leaving count at 2 instead of 1). Only a machine the object does
not exhibit — one it merely performs — is started as a detached performance by that form. When
the object exhibits one definition as several usages (exhibit state front : Blink; exhibit state rear : Blink;), naming the definition names no one machine, so %state Blink lamp refuses
and names Lamp::front and Lamp::rear to name instead.
Naming the machine alone attaches the same way when one held object exhibits it: %state modes
(or %state Monitor::modes) after %instantiate Monitor drives that object's running machine,
so what its do and entry actions write shows up in %features Monitor. It never performs
the machine detached from its object: with no object exhibiting it (%state modes before any
%instantiate), or several (a second Monitor held as a part of another object), %state
refuses and names the objects — or, before any exists, the type exhibiting the machine — and you
name one with %state <object> or %state <machine> <object>. Only a state def no type
exhibits is started as a detached performance by that form.
The object can also be a part reached through composition, or an id. With part def Driver { part r : Monitor; }, part driver : Driver; and %instantiate driver, %state driver.r debugs the nested part's own
machine, and %state #2 the same by the id %features driver prints for it (r = Instance(ID: 2)). A path that stops short of an object says which segment failed, in the words
every command uses for an object reference:
sysml> %state driver.x
error: driver has no feature "x" (its features are r, and 13 more the library declares)
Naming a usage whose definition alone was instantiated is reported as such, with what to
instantiate instead. With part monitor : Monitor; declared:
sysml> %instantiate Monitor
sysml> %state Monitor::modes monitor
error: no instance of the usage "monitor": object #1 of "Monitor" is of its definition "Monitor", not of the usage — use %instantiate monitor to create the usage's object, or name Monitor to address it
When a machine starts, and how far it runs. The object's feature values are built and its
constant defaults evaluated first, so an entry action sees the declared initial values. The
machine is then initialized and run until it is quiescent: no event is due at the current
time, no do action can run, and no message is in flight. A machine waiting on a timer or an
accept is quiescent, and advancing time is what lets it proceed. Objects that signal one
another are run together until they all settle, within the event and do-step budgets described in
reference/environment.md; an exchange that never settles reports a
budget error rather than hanging. Instantiating the same name twice creates a second object with
its own identity and its own machines: %instantiate reports the new object, and the name then
refers to it, while the first object keeps running and is still addressed by its id
(%state #1, %invoke #1 bumpBy n=4, %features #1; see
addressing an object). A %state or %action session
started on the first object stays with it — it now knows the object as #1 — and it ends only if
that object is later dropped. A machine a nested part exhibits is
debugged by naming that part through its owner, %state Monitor.sensor or %state #1.sensor.
An exhibited machine with no initial state is reported as such. A performed action
that declares no flow has nothing to step, but the object is still created. A performed action
waiting at an accept is also quiescent, and a message sent later by a sibling object
wakes it up.
Editing the model while an object runs. Submitting an unrelated declaration keeps the object
(its identity survives the rebuilt analysis) but not the execution it was running. An execution
belongs to the graph, names and message bus of the analysis it started in, so the object's
behaviors are restarted from their initial states in the rebuilt analysis, and any values the
discarded run wrote are dropped with it. The restart is reported (note: the exhibited state machine modes of object #1 was restarted from its initial state because the model was rebuilt), and a
%state session follows the object onto its restarted machine, so a restarted behavior exchanges
messages with objects instantiated after the edit in the usual way. Redeclaring what the object
runs (its type's features, or the body of a machine or action it runs) produces a different
object, so the original is dropped with a stated reason and %instantiate creates a new one.
Invoking an operation. %invoke <object> <op> [<p>=<expr>] runs an action owned by the
object's type, performed by that object — named as %state names one, so %invoke driver.r bumpBy n=4 and %invoke #2 bumpBy n=4 reach a nested part:
sysml> %invoke Monitor bumpBy n=4
✓ Invoked bumpBy on object #1 of "Monitor"
sysml> %features Monitor
Instance: Monitor (ID: 1)
Features:
count = 15
Behaviors:
modes: exhibited state machine, current state awake
bumpBy: action, not running
Each argument is written as <parameter>=<expression>. An unbound parameter, an argument that
names no parameter, and an operation the type does not own are each reported as errors. A calc
or constraint cannot yet be invoked this way.
An analysis case is a case, a case is a calculation, and a calculation is an action (SysML v2
§7.22, §7.21, §7.19), so running one is performing an action whose result is its out and
return parameters. %analysis and -analysis run a case the way %calc invokes a calc:
the case's subject is an in parameter the usage binds (subject s = ship;) or the object
named after the case supplies, the other in parameters take arguments in parentheses,
positionally or by name, or their declared defaults, and the report lists what the case computed
with its units and, after that, the verdict of its objective.
package test {
private import ScalarValues::*;
part def Ship { attribute cost : Real = 5.0; }
part ship : Ship;
analysis steps {
subject s = ship;
action a {
out p : Real;
assign p := s.cost + 1.0;
}
then action b {
in q : Real = a.p;
out w : Real;
assign w := q * 10.0;
}
out total : Real = b.w + a.p;
return : Real = b.w;
}
}
$ sysml -analysis test::steps steps.sysml
✓ package test
✓ test::steps
total = 66.0
result = 60.0The body's action steps run through the same executor %action debugs: then sequences them,
each later step reading an earlier one's output by step.pin, and a body that states no
succession performs its steps in declaration order, as a calc body does. A nested analysis
usage is a step too; one that binds no subject of its own runs on the enclosing case's subject,
and the enclosing case reads its outputs as features of it (out total : Real = inner.m * 2.0;).
The out parameters and return are evaluated in the case's frame after the steps complete, so
they may read the subject, the in parameters, the steps' outputs and call calc defs.
%trace on shows the order: the case is entered, its subject bound, each step performed as an
action node, then return and every out evaluated.
The objective is a requirement the case frames; it is evaluated, not executed. After the body
runs, its require and assume constraints are checked against the case's results by the same
engine %requirement uses, and the verdict is printed beside them: satisfied, not satisfied
with the condition that failed, or undecided with the reason a condition could not be evaluated
(a feature the condition reads has no value). An assert constraint { ... } inside the body is
checked the same way, against the values the run bound once its steps have completed. %optimize
still asks a solver which
values would make the objectives best; %analysis reports what they are for the values the model
has.
An objective typed by a requirement definition binds that definition's subject as a requirement
usage does, in every spelling — objective : MassLimit { subject = ship; }, subject s = ship;
or subject :>> s = ship; — and the binding may read the case's subject, its in parameters and
locals, and its steps' outputs — a nested case's (subject = inner.picked;) or an action's
(subject = weigh.m;). The case's own result, named or not,
is readable by its qualified name as the OMG examples write it — subject = MassCase::result; in
the objective, MassCase::result < limit in an assert constraint, inner.result from the case
performing inner as a step. The qualifier names whose result it is: MassCase::result (or
Cases::Case::result) is the running case's, while a sibling usage's light::result is the
sibling's own run, never the running case's value. An objective that binds no subject takes the
library's default for it: the case's result (Cases::Case::obj declares subject subj default Case::result, SysML v2 §7.22). So an objective typed by MassLimit in a case that returns a Ship checks the ship
returned, while in a case that returns a Real it is undecided, saying so: subject s defaults to the case's result (Cases::Case::obj): type mismatch: 1000.0 (a Real) is not a Ship. The result
must also fit the subject's multiplicity: one Ship for a subject pair : Ship[2] is undecided
as a multiplicity violation (an objective redeclaring the subject without one, subject :>> pair;,
keeps the [2]). A case that returns nothing leaves such a subject unbound, and the
verdict says to bind it or return a result. Bound either way, an object is held as a value of the
subject, so one declared a Ship and bound to a subject t : Tanker is a Tanker for the
conditions, its cargo answering t.cargo, exactly as a requirement usage's subject = ship;
holds it; a value the subject cannot hold at all (a Buoy for a Tanker) is refused as a
type mismatch before any condition is read, and an expression yielding more or fewer values
than the subject declares (one Ship for a subject pair : Ship[2], or none) as a
multiplicity violation, just as the default is. The object a satisfaction assertion supplies
with by is held to the subject's declaration the same way: classified by its type, refused as a
type mismatch where it cannot be, and as a multiplicity violation where one object is too few.
requirement def MassLimit {
subject s : Ship;
attribute limit : Real = 2000.0;
require constraint { s.hullMass < limit }
}
analysis def MassCheck {
subject ship : Ship;
objective : MassLimit { subject = ship; }
return r : Real = ship.hullMass;
}
analysis light : MassCheck { subject ship = O::ship; }
$ sysml -analysis O::light mass.sysml
✓ package O
✓ O::light
r = 1000.0
objective obj: satisfiedA case usage owned by a part definition (part def Holder { analysis inner : CostAnalysis { subject s = h; } part h : Ship; }) is a feature of every object of that type, as a calc usage
owned by a part is: holder.inner.total runs the case on first read and keeps the result until a
value it depends on changes, and an attribute redefined from an analysis output (attribute :>> fuelEconomy = cityAnalysis.fuelEconomyResult;) evaluates through the same path.
What stops a case is reported as an error naming it, never as a silent empty result: a
subject nothing binds (analysis An::CostAnalysis: s subject is unbound: bind it (subject s = ) or run it on an object), an in parameter with neither argument nor
default, a step that fails, a body that deadlocks or exhausts the step budget, and a case whose
body runs itself. A case that recurses without bound — through a nested analysis step that
performs its own definition, or a calc def through a calc usage member typed by itself — hits
the calc depth limit, and the error collapses the repeated frames to one line as -calc's does:
analysis An::rec: node again: analysis An::Rec::again: … 9999 frames: node again: calc recursion limit exceeded: calc An::Rec::again nested 10000 deep (unbounded recursion?; raise OPENSYSML_MAX_CALC_DEPTH to allow more).
A verification case body uses the same grammar and runs the same way: %analysis and -analysis
accept a verification def or verification usage, bind its subject and in parameters as they
bind an analysis case's, run its body over the same action graph, and check its objective and
assert constraints afterwards. Beside those verdicts they report the VerdictKind the body
produced, which is what running the body answered:
verification def SpeedCheck {
subject lander : Lander;
objective { verify touchdown; require constraint { lander.verticalSpeed <= 1.5 } }
VerificationCases::PassIf(lander.verticalSpeed <= 1.5)
}
verification checkSlow : SpeedCheck { subject lander = L::slowLander; }
$ sysml -analysis L::checkSlow landing.sysml
✓ package L
✓ L::checkSlow
result = VerdictKind::pass
objective obj: satisfied
✓ Verification L::checkSlow verdict: passThe two verdicts are independent: an objective stating no condition of its own —
objective { verify touchdown; } alone — stays undecided and leaves the case unresolved, while
the body verdict beside it still reports what the body answered.
A body whose result is a VerificationCases::PassIf(...) call is pass or fail as that library
calculation computes it; one binding verdict to a VerdictKind literal reports that literal; one
producing no verdict value is inconclusive; and one whose run could not be carried out is error
carrying the same message the run failed with. Each nested verification step is reported on its
own line, marked (subcase), since the library states no roll-up of a subcase's verdict into its
parent's.
%requirement, %satisfy, -requirement and -satisfy report those verdicts beside their own,
and their own verdict is unchanged: the requirement engine still decides whether the requirement is
satisfied, and a failing verification body does not make a satisfied requirement violated.
%sweep runs the case once per value of a range rather than once, and %samples <n> <seed>
draws that many values from it instead — the same tables -sweep and -samples print. Each row
is an ordinary run of the case with the swept parameter bound to that row's value, so it reports
what that run computed and how long it took:
%instantiate An::ship
%sweep An::CostAnalysis An::ship limit=10.0..30.0:10.0
sweep An::CostAnalysis — 3 run(s)
limit | total | verdict | time
------+-------+---------------------------+--------
10.0 | 12.0 | affordable: not satisfied | 0.510ms
20.0 | 12.0 | affordable: satisfied | 0.022ms
30.0 | 12.0 | affordable: satisfied | 0.012ms
%samples 3 42 An::CostAnalysis An::ship limit=10.0..30.0
samples An::CostAnalysis — 3 run(s), seed 42
limit | total | verdict | time
-------------------+-------+---------------------------+--------
26.509450139960897 | 12.0 | affordable: satisfied | 0.016ms
10.856399027228605 | 12.0 | affordable: not satisfied | 0.015ms
25.521460994239078 | 12.0 | affordable: satisfied | 0.013ms
The endpoints are expressions evaluated where the session evaluates one, units included, and a
calc is swept the same way (%sweep An::Sum(2.0) b=0.0..10.0:2.5). Several ranges run their
cartesian product, a failed run is a row of the table rather than the end of it, and
reference/repl-commands.md states each refusal. Sampling is
uniform over the range — the bundled library defines no probability distribution, so a
distribution asked for by name is refused naming what is missing.
Each model below is in
examples/action-executor-demo.sysml, and the output
shown is from
sysml -action ActionExecutorDemo::<name> examples/action-executor-demo.sysml.
The sequential flow below uses standard implicit succession notation:
action sequential {
attribute result : Integer = 0;
first start;
then action compute { assign result := 42 * 2; }
then done;
}
A single token is created at start, moves to compute, which runs its body, and is consumed at
done:
$ sysml -action ActionExecutorDemo::sequential examples/action-executor-demo.sysml
✓ Action completed
Final state: Completed
Results:
result = 84
fork and join are action node literals, so this action is standard notation.
action forkJoin {
attribute task1 : Integer = 0;
attribute task2 : Integer = 0;
attribute task3 : Integer = 0;
first start;
fork split;
action left { assign task1 := 10; }
action middle { assign task2 := 20; }
action right { assign task3 := 30; }
join sync;
succession first start then split;
succession first split then left;
succession first split then middle;
succession first split then right;
succession first left then sync;
succession first middle then sync;
succession first right then sync;
succession first sync then done;
}
fork places a token on each outgoing succession. join is an AND-join: it waits for a token
on every incoming succession before a single token continues. A fork duplicates control, not
values: all three branches are steps of the same performance, so every assignment is visible
when it completes.
$ sysml -action ActionExecutorDemo::forkJoin examples/action-executor-demo.sysml
✓ Action completed
Final state: Completed
Results:
task1 = 10
task2 = 20
task3 = 30If a branch never arrives, that is a deadlock rather than a failure, and the run is reported as undecided.
decide with a guarded branch and an else branch is standard notation. The
OpenSysML spelling decision is the one that produces a warning.
action conditional {
attribute x : Integer = 15;
attribute taken : Integer = 0;
first start;
action pathA { assign taken := 1; }
action pathB { assign taken := 2; }
succession first start then check;
succession first pathA then done;
succession first pathB then done;
decide check;
if x > 10 then pathA;
else pathB;
}
decide evaluates its guards in the order written, with the action's features in scope, and takes
the first guard that holds. The else branch is taken when no guard holds. With x = 15:
$ sysml -action ActionExecutorDemo::conditional examples/action-executor-demo.sysml
✓ Action completed
Final state: Completed
Results:
taken = 1
x = 15Setting x to 5 gives taken = 2. The state-machine counterparts (orthogonal
regions, choice and junction) appear in
examples/orthogonal-regions-demo.sysml and
examples/pseudostates-demo.sysml, and every case the
executors are tested against lives under internal/core/runtime/testdata/conformance/.
A run that stops early, whether through deadlock or by hitting a budget, is reported as an undecided check rather than a failure. The budgets are documented in reference/environment.md.