The pinned OMG pilot evaluates expressions but executes neither actions nor state machines
(pilot execution referee), so every behavioral row in
spec compliance is refereed by this repository's own conformance models,
trace goldens and robustness cases — all of which were recorded from the executor they check.
This record adds a referee that is not the executor: for each rule below, a minimal model whose
expected outcome and ordering are derived by hand from the bundled library text (Kernel
Semantic Library Occurrences.kerml, Performances.kerml, ControlPerformances.kerml,
StatePerformances.kerml, TransitionPerformances.kerml; Systems Library Actions.sysml), with
the sentence that justifies each ordering constraint cited, and the orderings the library leaves
open named as open.
The oracle cases live beside the other conformance cases under
internal/exec/runtime/testdata/conformance/ and run through the same harness
(go test -run 'TestExecutionConformance|TestExecutionTrace' ./internal/exec/runtime). Where the
executor meets the derived expectation, the case also carries a .trace.golden that
regression-locks the executor's linearization. Where it does not, the derived expectation is kept
in the .expected.json, the case is listed in known_failures.txt so the harness reports rather
than runs it, and the gap is recorded in What the executor gets wrong.
No executor code was changed to build this corpus; that is the point of it. The one gap the
corpus found — a merge closed after its first traversal — was fixed afterwards against the
derivation, not the other way round.
The library states a partial order between performances. A succession is a connector typed
by HappensBefore, which "asserts that the earlierOccurrence is completely separated in time …
with the earlierOccurrence happening completely before the laterOccurrence"
(Occurrences.kerml, assoc all HappensBefore). The steps of a behavior are its
enclosedPerformances, happening during it (Performances.kerml, Performance::enclosedPerformances;
StatePerformances.kerml, "all steps are implicitly considered to be enclosedPerformances, and
hence happening during the state performance"). The default 1..1 multiplicity for a feature
(KerML 1.0 §7.4.5) is the rule for feature values, not an inherited count for an action node:
Actions.sysml declares Action.subactions as Action[0..*], and each action-node usage's own
declared multiplicity determines how many performances it names, whether the usage is reached by a
succession or starts concurrently as an unordered subaction. An omitted multiplicity remains one
performance; a fixed finite [n] or [n..n] names n performances, including none for [0]. A
non-fixed or unevaluable count cannot be run by this fixed-count executor. Nothing in the library
orders two steps no chain of HappensBefore links connects.
A .trace.golden records one linearization of that partial order plus tool-defined
scheduling detail the library says nothing about:
- Tokens are stepped in descending index order within a step; a fork appends its branch tokens in succession-declaration order, so the branch declared last is stepped first in every step.
- A node's body runs in the step that moves its token on, so a body's statements appear in the
golden between the
step N:line that shows the token at the node and thestep N+1:line. - A node several successions reach (a join, or a plain node) performs with a fresh token appended
to the token list once every succession has delivered, and the step that fired it may go on to
step that token (and tokens the removal shifted) again; a
step N:line is therefore the executor's step boundary, not a unit the library defines. - Under the
explorepolicy a step is one token advancing one node, so achoiceline is one pick among the tokens able to act at that moment and the next step picks again among those left and any the move enabled; a linearization is the sequence of those picks, and a branch of several nodes may be overtaken by a concurrent one between any two of them. The fixed policies move every steppable token once per step, so theirstep N:lines group moves thatexplorespreads over consecutive steps; a body's statements run without interruption under both. - A state machine records a transition's guard evaluation, exit, effect and entry as they run, and evaluates a guard once to select the transition and once again to fire it; the second evaluation is a tool detail with no observable effect, since a guard is an expression.
- A
choiceline marks a point where the executor picked among alternatives the library leaves unordered — several steppable tokens in one step, several holding decision guards, several enabled transitions out of one state for one event, or two tokens writing one feature in one step — and names the alternative it took. Everything after achoiceline is one linearization among the ones that line admits.
When a golden is reviewed against this record, the question is whether the golden's linearization is one of the linearizations the derivation admits and whether every outcome the derivation fixes is met — not whether the golden is the only correct trace.
| Where | Text | Used for |
|---|---|---|
Occurrences.kerml HappensBefore |
"the earlierOccurrence happening completely before the laterOccurrence … no snapshot of the earlierOccurrence happens at the same time as any snapshot of the laterOccurrence" | Every succession orders the whole source performance (its body included) before the whole target performance |
Performances.kerml Performance::enclosedPerformances, subperformances |
step enclosedPerformances: Performance[0..*] subsets performances, timeEnclosedOccurrences — "timeEnclosedOccurrences of this Performance that are also Performances"; composite step subperformances: Performance[0..*] subsets enclosedPerformances, suboccurrences — "enclosedPerformances that are composite" |
A composite step's performances start no earlier and end no later than the performance owning them, whether or not a succession orders them |
Occurrences.kerml Occurrence::timeEnclosedOccurrences |
"Occurrences that start no earlier than and end no later than this occurrence" | The owner's performance ends only after every subperformance has; its own successors follow them all |
Actions.sysml Action::subactions |
action subactions: Action[0..*] :> actions, subperformances — "The subperformances of this Action that are Actions" |
Every composite action usage of an action (a send, accept, assign, if, while or for among them) is one of its subperformances; a ref action usage is not composite and is not one |
Actions.sysml ControlAction |
bind start = done — "A ControlAction is instantaneous" |
A control node adds no duration; its successor may start as soon as its predecessors end |
Actions.sysml ForkAction |
"Fork behavior results from requiring that the target multiplicity of all outgoing succession connectors be 1..1" | Each fork performance is followed by exactly one performance of every target |
Actions.sysml JoinAction |
"Join behavior results from requiring that the source multiplicity of all incoming succession connectors be 1..1" | Each join performance follows exactly one performance of every source, one per incoming succession |
Actions.sysml MergeAction, ControlPerformances.kerml MergePerformance |
"Incoming succession connectors to a MergeAction must have source multiplicity 0..1"; "For each instance of MergePerformance, the incomingHBLink is an instance of exactly one of the Successions, ordering the MergePerformance as happening after an instance of the source of that Succession" | A merge performance follows one source performance; a source a given merge performance was not reached from need not exist |
Actions.sysml DecisionAction, ControlPerformances.kerml DecisionPerformance |
"For each instance of DecisionPerformance, the outgoingHBLink is an instance of exactly one of the Successions, ordering the DecisionPerformance as happening before an instance of the target of that Succession" | Each decision performance is followed by a performance of the one target whose guard held |
Stochastic.sysml Probability |
"A seeded run draws a branch by these weights, an unseeded run takes the most probable, and a weighted branch whose guard does not hold is left out of the draw, the others' weights renormalized." | These weights belong to model draws; they do not assign probabilities to unresolved scheduling choices |
| KerML 1.0 §7.4.5 | A feature with no declared multiplicity holds exactly one value | This constrains feature values, not action-node usage counts; a plain step with no multiplicity is one performance per performance of its owner, however many successions reach it |
Performances.kerml Performance::enclosedPerformances; Actions.sysml Action.subactions |
subactions : Action[0..*]; subperformances |
A node usage's declared multiplicity counts its enclosed performances, including unordered concurrent starts; each repeated performance owns a fresh node frame while writing the shared owner features |
| OMG issue KERML-29 | Deferred; the multiplicity of succession ends is unresolved | The execution checker approximates an unwritten end as either unconstrained [0..*] or exact-one [1..1] and accepts only when both readings force and admit the endpoint counts |
StatePerformances.kerml StatePerformance |
succession [1] entry then [*] middle; succession [*] middle then [1] exit |
Entry first, exit last, within a state performance |
StatePerformances.kerml StateTransitionPerformance |
succession all [*] acceptable then [*] guard; succession [*] guard then [1] transitionLinkSource.exit |
The guard is evaluated after the trigger and before the source state's exit |
TransitionPerformances.kerml TransitionPerformance |
binding transitionLink.earlierOccurrence = transitionLinkSource; succession [1] transitionLinkSource then [*] effect; succession [*] effect then [1] transitionLink.laterOccurrence; succession all [*] guard then [*] effect |
The effect runs after the source state performance has ended (its exit included) and before the target state performance starts (its entry included) |
Transfers.kerml SendPerformance |
feature sentTransfer: MessageTransfer [1] subsets sender.outgoingTransfersFromSelf; succession self then sentTransfer |
A send is followed by exactly one transfer carrying its payload |
Transfers.kerml AcceptPerformance |
feature acceptedTransfer: MessageTransfer[1] subsets receiver.incomingTransfersToSelf; succession acceptedTransfer then self.endShot; binding payload = acceptedTransfer.payload |
An accept ends after exactly one transfer to its receiver and yields that transfer's payload; nothing pairs a particular accept with a particular transfer |
Actions.sysml DecisionTransitionAction, TransitionPerformances.kerml NonStateTransitionPerformance, TPCGuardConstraint |
"the base type of TransitionUsages used as conditional successions in action models"; in feature transitionLinkSource: Performance[1], feature transitionLink: HappensBefore[0..1], succession [1] transitionLinkSource then [1] Performance::self, connector all guardConstraint: TPCGuardConstraint[*] from [0..1] transitionLink to [*] guard (constrainedHBLink / constrainedGuard, inv { allTrue(constrainedGuard()) }) |
A guarded succession in an action is a transition performance that happens after its complete source performance — a merge's body included — and whose guard constrains the HappensBefore link to the successor: a false guard leaves the link out, not the source performance |
The SysML v2 specification's own control-node example (ChargeBattery, §7.17.3, reproduced in
the training corpus as 17. Control/Decision Example.sysml) and the pilot validation corpus
(3a-Function-based Behavior-1.sysml: "A merge node is necessary to prevent a loop of
successions from being unsatisfiable"; "The performance of the actions on the left cannot
continue once there is a performance of 'engineStopped'") are used only to confirm that the
readings above are the ones the specification's authors rely on, not as library text.
Each case names its fixture (the .sysml and .expected.json, plus a .trace.golden where the
executor meets the derivation), the constraints derived, the orderings left open, and the
outcome the derivation fixes. An openness that is observable is encoded in the fixture: the
.expected.json lists the admissible set as outcomes and cites the section below as
admissible, and a .trace.order file states the partial order the library does fix as
earlier < later constraints on the trace. The harness checks a run against exactly one member,
then explores the case (exploreConformanceCase, budget exploreBudget, default 1024 runs and 64
choice points) and fails on a listed outcome no linearization reaches, on a reached outcome the
list omits, and on a budget hit. An openness that is not observable is pinned by a fixture with one
expected outcome; such a fixture has no outcomes and the harness does not explore it, so the
explore figures quoted for it below come from running the fixture under the explore policy,
not from the suite.
Fixture: action_explore_mixed_scheduler_weighted_probability (conformance case).
start → fork ─┬─ a: x := 1 ─┐
└─ b: x := 2 ─┴─ join → decide
x == 1 → weighted draw: y := 1 (0.3) | y := 2 (0.7)
x == 2 → y := 0
Derived admissible outcomes:
ForkActionrequires one performance of each outgoing succession target, but does not order the branches. The writes race, so the scheduler may leavex = 1orx = 2at the join.DecisionAction/DecisionPerformancerequires exactly one outgoing succession for each decision performance. Thex == 1route reaches the weighted decision; its two weights sum to one, so the model givesy = 1probability0.3andy = 2probability0.7. Thex == 2route instead completes withy = 0.- Scheduler choices have no probability. Over schedulers,
{x=1,y=1}therefore has range[0, 0.3],{x=1,y=2}has[0, 0.7], and{x=2,y=0}has[0, 1]: each minimum is zero because a scheduler can choose the other write last, while each maximum is the model's weighted probability when thex = 1route is selected or certainty whenx = 2is selected. This is the min/max at scheduling nodes and weighted sum at weighted nodes, not a uniform distribution over linearizations.
The conformance schema has no error member in outcomes; runtime-error outcomes are not
expressible by this expectation format and are rejected as unexpected.
Fixture: action_join_waits_for_slowest_branch (golden).
start → split ⇉ a ──────────────┐
⇉ b1 → b2 ────────┤→ sync → after → done
⇉ c1 → c2 → c3 ───┘
Derived constraints:
splitis followed by exactly one performance each ofa,b1,c1(ForkAction, target multiplicity 1..1).syncfollows exactly one performance each ofa,b2,c3(JoinAction, source multiplicity 1..1), andb2followsb1,c3followsc2followsc1(HappensBefore).afterfollowssync(HappensBefore), so it runs after all threearrived := arrived + 1writes have ended.
Open: the relative order of a, b2 and c3 — and of every node on one branch against every
node on another. The golden's order (the c branch stepped first within each step, a finishing
first because its branch is shortest) is one admissible linearization. Nothing observable depends
on it: every interleaving increments arrived three times before after reads it, so the fixture
pins the one outcome and has no outcomes. Under explore the interleavings of one, two and
three nodes on three branches are 6! / (1! 2! 3!) = 60 linearizations reaching that one outcome
(60 runs, 1 outcome, complete), each a sequence of token choices (step 3: 2@a first of 2@a, 3@b1, 4@c1; step 4: 3@b1 first of 3@b1, 4@c1; …).
Fixed outcome: arrived = 3, seen = 3. The executor agrees; the golden shows after reading
arrived -> 3 and every branch's write preceding it.
Fixture: action_join_one_token_per_incoming_succession (golden).
start → split ⇉ l1 ──┐
⇉ l2 ──┤→ left ────────────────┐
⇉ r1 → r2 → r3 → right ────────┤→ sync → after → done
Derived constraints:
leftis one performance (KerML §7.4.5), and it follows bothl1andl2(HappensBefore over each succession into it).syncfollows exactly one performance ofleftand exactly one ofright(JoinAction, source multiplicity 1..1 on each of its two incoming successions).rightwriteslog := log * 10 + 1;afterfollowssyncand writeslog := log * 10 + 2.
Open: the order of left against any node of the r branch. Nothing observable depends on it,
so the fixture pins the one outcome without outcomes; under explore the seventy
interleavings — l1 and l2 in either order then left, three events woven into the four of
the r branch, 2 × C(7, 3) — all reach it (70 runs, 1 outcome, complete).
Fixed outcome: log = 12 — right writes before after, whatever the interleaving, because
after cannot start before sync, and sync cannot start before right has ended.
Executor: log = 12, no deadlock. Every token records the succession it travelled
(Token.Via, the lowered ActionEdge), and ActionExecutor.synchronize holds a token at a
node until one token has arrived over each succession into it; the golden shows the two
left arrivals collapse into one token at sync (step 4), which waits there for right
(step 6) before after runs (step 7). The case is observable only because a node upstream is reached
over two successions, so it shares a mechanism with the next case; it stays a separate case
because it pins a distinct rule — a join fires on which successions delivered, not on how
many tokens arrived. action_join_same_succession_twice pins the converse: two tokens over
one succession into a join satisfy that succession once, and the second waits for the join's
next firing (log = 1212).
Fixture: action_node_with_two_incoming_successions_runs_once (golden).
start → split ⇉ l1 ──┐
⇉ l2 ──┤→ both → done
Derived constraints:
bothdeclares no multiplicity, so it is one performance ofconverge; this example says nothing about a step usage that explicitly declares a count.- Each of
first l1 then bothandfirst l2 then bothis aHappensBeforelink whoselaterOccurrenceis that one performance, so it starts after bothl1andl2have ended. This is the reading the pilot corpus states in prose forengineStopped, which five successions reach.
Open: the order of l1 against l2. It is not observable — both performs once either way —
so the fixture pins the one outcome without outcomes; under explore the two orders both reach
it (2 runs, 1 outcome, complete).
Fixed outcome: hits = 1.
Executor: hits = 1. The same synchronize gate holds a token at a plain node until each
succession into it has delivered, so both is performed once by the one token the two
arrivals collapse into (golden: both arrivals held at both after step 3, its one
performance in step 4). A join differs only in what it awaits: every incoming
succession (source multiplicity 1..1), so a join one of whose sources no token can reach
deadlocks (robustness_test.go:deadlock_join_starvation, :deadlock_join_same_succession_twice);
a plain node awaits a succession only once it has delivered or while some token of the
activation, other than one held at the node, can still reach its source without passing through
the node — the branch a decision did not take, or a loop back over the node itself, imposes no
HappensBefore on this performance. action_node_converges_after_decision (golden) pins the
first half: converge behind a decision's two branches performs once for the branch taken and
does not deadlock on the other; action_node_loop_back_reperforms (golden) the second: bump,
reached from start and from the decision after it, performs once per pass.
action_nested_node_two_successions_per_performance pins that the count is per performance of
the owning action: two performances of an action holding such a node each perform it once; and
action_node_concurrent_performances and action_node_concurrent_nested_bindings (goldens)
that a flow-owning node reached from both branches of a fork is likewise one performance,
holding at each pin the one delivery the flow into that pin carried.
Fixtures: action_step_multiplicity_exact, _reverse, _explore, _range, _named_bound,
_zero, _local_frames, _nested, _nested_state_entry, _perform, _ordering and
_order_target_only, _unordered_start, _unordered_start_reverse,
_unordered_beside_ordered, _unordered_zero,
_unordered_nested, _unordered_unbounded, _unordered_unaddressable, _unordered_outgoing,
_unordered_loop_body and state_step_multiplicity_unordered_do_body;
action_step_multiplicity_shared_writers states the open outcome set.
Derived constraints:
- A missing multiplicity keeps the historical one performance. An action-node usage's own
[n],[n..n], or named exact bounds that evaluate to an exact count performntimes, including unordered subactions that start concurrently without an incoming succession;[0]performs no body, trace event, flow or data transfer. Other ranges and bounds the model cannot evaluate do not identify a fixed number and are refused. - An action usage in a loop or conditional block flow is performed once per pass. Repetition in
those statement-engine flows is out of scope: exact counts other than
[1], including[0], are refused withaction-step-multiplicity-unsupportedrather than being expanded. Occurrences.kermlHappensBeforeorders whole source performances before whole target performances. A repeated node therefore needs every incident edge to admit and force its complete count. The accepted fixtures use explicitly written end multiplicities:[1] pto[*] a[3], a written target[3] a[3],[*] a[3]to[1] q, and[2] a[2]to[3] b[3]. A start source and done target constrain no repeated endpoint; guards, control-node adjacency, pins of repeated nodes and external reads of their features exceed the supported subset.- KerML leaves succession-end defaults unresolved (OMG KERML-29,
deferred). The checker evaluates unwritten ends both as unconstrained
[0..*]and as[1..1], accepting only an edge whose counts are forced and admitted under each reading. If a reading forces an end that excludes its endpoint count, the result isaction-step-order-unsatisfiable; otherwise an edge not established by both readings isaction-step-order-open. This is deliberately approximate by refusal, not a new claim about KerML defaults. - A plain
then(no written end multiplicities) between a repeated step and any step other than the action'sstart/doneis refused withaction-step-order-unsatisfiable: under the[1..1]reading the end excludes the step's count, and under the unconstrained reading the order is left open. Write the ends explicitly (succession first [1] p then [*] a;,then [3] a;,succession first [*] a then [1] q;) to state the library's fan-out/fan-in pattern. - In
action_step_multiplicity_shared_writers, each of threeaframes has its ownl, which snapshots the sharedcwhen that frame begins; itswwritesl + 1back to that samec. If all three snapshots happen before any write, all writes store1; a snapshot after one or two completed writes can lead to a final value of2or3. There are at most three writes, so the hand-derived final set is exactly{c = 1, c = 2, c = 3}.
Open: the order among sibling repeated performances is not established by their count. The runtime represents them as sibling tokens, each with an independent performance frame; their owner-frame writes share the same feature space. The last completion is a barrier: only after every performance at that node finishes can the token carry its succession and data flows on. Exploration therefore finds each admitted shared-write result without merging states that differ in the live repetition set.
Fixed outcome: action_step_multiplicity_exact has c = 3 under declared, reverse and explore
schedules; explore reaches one distinct outcome. In action_step_multiplicity_zero, [0] leaves
the initial c unchanged and its q successor still runs, setting c = 7. The local-frame
fixture reaches c = 3: each fresh l starts at zero, becomes one, and contributes one to the
shared c.
Fixture: action_fork_branches_write_one_feature (golden).
start → split ⇉ left { x := 1; leftRan := true } ─┐
⇉ right { x := 2; rightRan := true } ─┤→ sync → done
Derived constraints:
leftandrightare each performed exactly once (ForkAction), soleftRanandrightRanboth endtrue.syncfollows both (JoinAction), so both writes ofxhave ended before the action ends andxis1or2, never0.
Open: the order of left against right, and so which write of x stands. The library gives
no HappensBefore link between them and no conflict rule; either final value is admissible.
Pinned outcome: the admissible set {x = 1, x = 2}, with leftRan = true and rightRan = true
in both, which the case states as outcomes citing this section and the harness checks the run
against — a run must match exactly one member. The partial order the library does fix is stated
as .trace.order constraints (split < left, split < right, left < sync, right < sync) the
trace must satisfy. Exploration reaches both outcomes and no other, one linearization each
(2 runs, 2 outcomes, complete). The exact trace golden stays: it records the executor's scheduling (right
is the branch declared last, so its token is stepped first and left writes last, giving
x = 1) and exists only so a change in that scheduling is noticed. The executor reports the
conflict as a choice point (choice step 3: writes x := 1 by token 2, x := 2 by token 3 (unordered; x := 1 by token 2 stood)), so the trace and the diagnostics name the value that
stood as a tool decision rather than passing it off as the model's answer. Companion fixtures:
action_choice_same_step_write_conflict (golden), the same shape over a String feature;
action_choice_chained_write_conflict (golden), the same shape writing one object's feature
through a feature chain (s.reading) from both branches; and
action_choice_performer_write_conflict (golden), a performed action's branches both writing
the part performing it. A destination is the object written and the feature written, whatever
name reached it, so two chains reaching one object are one conflict
(TestWriteConflictOnOneObjectThroughTwoChains), a feature and one redefining it are one
destination under either name (TestAliasWritesAreOneDestination), and two writes of equal
value are still one: the library orders the writes no more when they agree. The choice is
recorded once the step is complete, one per destination, listing the last write of every token
that wrote it and the one that stood: three branches are one choice of three
(TestThreeWritersAreOneChoice), and a token writing twice contributes only its last write, the
one that would stand had it gone last (TestRepeatedWritesByOneTokenListItsLast).
Fixture: action_choice_decision_overlapping_guards (golden).
start → select ─ if level > 50 → warn { handler := 1 } → done
─ if level > 70 → alarm { handler := 2 } → done
Derived constraints:
selectis followed by a performance of exactly one target (DecisionPerformance: "the outgoingHBLink is an instance of exactly one of the Successions"), sohandlerends1or2, never0.- Each guard is a
TPCGuardConstrainton its own link; a false guard leaves that link out. Withlevel = 75both guards are true, so neither link is excluded by its guard.
Open: which of the two admissible links the decision performance takes. The library says only
that it is exactly one of them; nothing ranks warn against alarm.
Pinned outcome: the admissible set {handler = 1, handler = 2}, stated as outcomes citing this
section; exploration reaches each once (2 runs, 2 outcomes, complete). The executor evaluates
every guard, takes the first declared, and records the choice
(choice step 2: decision select branches 1->warn, 2->alarm hold (unordered; took 1->warn));
the golden pins that linearization. Reporting never changes the run: the guards after the first
holding one are read in a preview that is undone — what evaluating them costs, writes or starts
is restored, and nothing they do is traced — so the run spends and does what first-match did
(TestLaterGuardIsProbedWithoutCost). A guard read that way that cannot be evaluated is not an
alternative and not an error: the library's TPCGuardConstraint is inv { allTrue(constrainedGuard()) },
an expression with no result is not true, so the link it guards is simply not selected, and the
library defines no failure for it. The executor records the guard as an informational
guard-unevaluable note (unevaluable guard step 2: decision select branch 2->alarm: division by zero (not selected)) so the tool's reading is visible without changing the run
(TestLaterGuardErrorIsNotAChoiceNorAFailure; fixture action_choice_unevaluable_guard, golden).
The first guard read is the run's own, not a preview, and its failure fails the run as it always
has. A decision whose guards are all false remains an execution error
(TestRuntimeRobustness/decision_all_guards_false), as the library then admits no outgoing link.
Fixture: action_explore_three_writers (golden, explored).
start → split ⇉ a { x := 1; aRan := true } ─┐
⇉ b { x := 2; bRan := true } ─┤→ sync → done
⇉ c { x := 3; cRan := true } ─┘
Derived constraints:
a,bandcare each performed exactly once (ForkAction), soaRan,bRanandcRanall endtrue.syncfollows all three (JoinAction), so every write ofxhas ended before the action ends andxis1,2or3, never0.
Open: the order of the three branches. The library gives no HappensBefore link among them, so
every one of the 3! = 6 orders is a valid linearization. The value of x is the last write, and
each branch is last in exactly two of the six orders, so the six linearizations reach exactly
three outcomes, {x = 1, x = 2, x = 3}, two linearizations each.
Pinned outcome: that admissible set, with the three …Ran flags true in every member, stated
as outcomes citing this section; .trace.order states the partial order the library does fix
(split < a, split < b, split < c, a < sync, b < sync, c < sync). The exact golden
records the default schedule (c is declared last, so its token is stepped first and a writes
last, giving x = 1). Exploration is what makes the set checkable: explore replays the run
along every choice sequence and must reach each of the three outcomes and no other, in six runs.
The first pick among three tokens and the next among the two left are two choice points in
consecutive steps, so a linearization is a sequence of two choices, not one choice among six.
Fixtures: action_unordered_subactions_write_conflict (golden, explored), with
action_unordered_subactions, action_unordered_beside_first, action_unordered_nested_subactions,
action_unordered_statements, action_unordered_send_accept, action_unordered_reference_not_performed,
action_unordered_accept_holds_owner, action_unordered_send_to_receiver,
action_body_flow_unordered_statement and state_do_body_unordered_statement (each one outcome).
race { a { c := 1 } b { c := 2 } } -- no succession, no `first`
Derived constraints:
aandbare composite action usages ofrace, so each is one of itssubactions(Actions.sysml), hence asubperformanceand anenclosedPerformanceof it (Performances.kerml): each is performed exactly once per performance ofrace(KerML 1.0 §7.4.5), starting no earlier and ending no later than it (timeEnclosedOccurrences).racetherefore ends only after both have; a succession out ofrace, itsdoneand the reading of its outputs come after both writes. Afirst-rooted flow beside them (action_unordered_beside_first) is one more part of the same performance, and the owner ends after it and after them.- A statement written among the action's members (
send,accept,assign,if,while,for) is an action usage likeaand is performed the same way; an accept among them holds the owner open until its transfer arrives (AcceptPerformance), and with none to arrive the owner never ends (action_unordered_accept_holds_owner, an accept deadlock). - A
refaction usage is referential, not composite, so it is nosubperformanceand is not performed (action_unordered_reference_not_performed); so is aperform, an event occurrence usage, which is referential (validateEventOccurrenceUsageIsReference), and an abstract usage. - The same holds for the flow a nested action node or a state's entry, do or exit body states
(
action_unordered_nested_subactions,state_do_body_unordered_statement).
Open: the order of a against b. No HappensBefore links them, so both linearizations are
valid; with c := c + 1 and c := c + 10 they agree on c = 11 (action_unordered_subactions),
with c := 1 and c := 2 the write that stands is open.
Pinned outcome: the admissible set {c = 1, c = 2}, stated as outcomes citing this section;
exploration reaches both and no other, one linearization each. The executor performs each such
subaction as a token started with the owner's performance (ActionGraph.Concurrent, lowered by
StartFlow), so its interleavings are the same choice points fork branches are. The exact golden
records the default schedule.
Not covered: a nested action node whose members are only statements, and a loop, branch or
behavior body stating no flow, run their statements and nodes in declaration order, as they did
before; the library orders them no more than it orders a and b, so that order is the
executor's choice, recorded in spec compliance.
Fixture: action_explore_write_between_branch_nodes (golden, explored).
start → split ⇉ left1 { x := 1 } → left2 { y := x } ─┐
⇉ right { x := 2 } ────────────────────┤→ sync → done
Derived constraints:
left1,left2andrightare each performed exactly once (ForkAction, andleft2followsleft1by HappensBefore), andsyncfollowsleft2andright(JoinAction), so every write has ended before the action ends.left2readsxafterleft1's write has ended, soyis1or2, never0.
Open: the order of right against left1 and against left2. The library gives no HappensBefore
link from either to right, so right may end before left1 starts, start after left1 ends and
end before left2 starts, or start after left2 ends: three linearizations, no more, since
left2 cannot precede left1. Each leaves a different pair of values: right, left1, left2
gives x = 1, y = 1; left1, right, left2 gives x = 2, y = 2; left1, left2, right gives
x = 2, y = 1.
Pinned outcome: that admissible set of three, stated as outcomes citing this section;
.trace.order states the partial order the library does fix (split < left1, left1 < left2,
split < right, left2 < sync, right < sync). The exact golden records the default schedule
(right is declared last, so its token is stepped first: right, left1, left2, giving x = 1, y = 1). Exploration must reach each of the three outcomes and no other, in three runs (3 runs,
3 outcomes, complete). The case is what distinguishes exploring one move at a time from exploring
the order of one lockstep step: had every steppable token moved once per step, left1 and right
would both have moved in the step after the fork whichever went first, left2 could never have
run before right, and the exploration would have reported two outcomes complete, missing
x = 2, y = 1. No fixed policy takes it: each moves left1 and right in the step after the fork,
in one order or the other, before left2 can run, so declared (and seed:1) give x = 2, y = 2
and reverse gives x = 1, y = 1.
Fixture: action_explore_early_race_long_tail (explored, checked).
start → split ⇉ a { x := 1 } → p1 { p := 1 } → p2 { p := 2 } ─┐
⇉ b { x := 2 } → q1 { q := 1 } → q2 { q := 2 } ─┤→ sync → done
Derived constraints:
aandbare each performed exactly once (ForkAction) andsyncfollows both branches (JoinAction), soxis1or2, never0, at the end.p1HappensBeforep2andq1HappensBeforeq2, each branch writing its own feature, sopandqboth end2whatever the interleaving.
Open: the order of a against b, and the interleaving of the two branches — the library links
neither. The last write of x stands, so the two orders of the writes are two outcomes; the
C(6, 3) = 20 interleavings of the branches split ten and ten by which write comes last, so the
twenty linearizations reach exactly two outcomes, {x = 1, x = 2}, ten each.
Pinned outcome: that admissible set, stated as outcomes citing this section, and a check
divergent over x alone. The case is what distinguishes an exploration's plan order: a run meets
the one open choice first and closed ones after it, so a walk taking the deepest untried
alternative first spends the ten orders under one write order before it varies the write order,
and a budget under eleven runs tables one value; a walk varying every choice of the first run
once before any twice tables both by the second run. TestExploreVariesEveryChoiceOfTheFirstRunFirst
pins that order; the harness explores the case to its complete table of two.
Fixture: action_explore_performed_and_accept_due_together (golden, explored).
action def Sleeper { start → nap accept after 2 [s] → rested }
start → split ⇉ performed : Sleeper → writeOne { x := 1 } ─┐
⇉ direct accept after 2 [s] → writeTwo { x := 2 } ┤→ sync → done
Derived constraints:
performedperformsSleeperas a suboccurrence, soSleeper'slocalClockiswake's (Occurrences.kerml, "The localClock of a suboccurrence defaults to the localClock of its containing occurrence"):napanddirectareTriggerAfter(2, …)against one clock, each fixed at0 + 2when reached (Triggers.kerml), and each ends when that clock reads 2 (TimeSignal::signalCondition).writeOnefollowsperformed, which ends afternap(HappensBefore, and a performed action ends after its last step);writeTwofollowsdirect;syncfollows both writes (JoinAction). Each write is performed exactly once, soxis1or2, never0.
Open: the order of performed's resumption against direct's, and so of writeOne against
writeTwo. Both accepts end at the same reading of the one clock, no HappensBefore chain
connects a step of one branch to a step of the other, and timeOrderingConstraint
(Clocks.kerml, TimeOf) orders only occurrences already ordered by HappensBefore. Two chains of
two moves each — resume performed then writeOne, take direct then writeTwo — interleave six
ways; the three that end with writeOne give x = 1, the three that end with writeTwo give
x = 2.
Pinned outcome: the admissible set {x = 1, x = 2}, stated as outcomes citing this section;
.trace.order states the partial order the library does fix (split < performed,
performed < writeOne, split < direct, direct < writeTwo, both writes and sync before
done). Exploration must reach both outcomes and no other, in six runs (6 runs, 2 outcomes,
complete). The case is what distinguishes a paused body from a parked token in the explored
move set: performed's token is paused inside Sleeper when the clock reaches 2, and
direct's is parked at its accept. Had the executor resumed paused bodies only after every
ordinary token of a step had acted, as the fixed policies sweep, then under explore — where a
step ends with the first move — direct would always have run first, writeOne would always have
been last, and the exploration would have reported x = 1 alone, complete. The fixed policies
sweep ordinary tokens before paused bodies, so each takes direct first, then steps the two
writes in its own order in the next step: reverse (and seed:1) writes x := 2 then x := 1,
giving x = 1; declared writes them the other way round, giving x = 2.
Fixture: state_run_to_completion_false_self_signal,
state_run_to_completion_scope_sibling_region, and
state_run_to_completion_scope_parent_transition, and
state_run_to_completion_scope_qualified_source, and
state_run_to_completion_terminate_during_held_entry (goldens, explored).
An entry that does not run to completion and a dispatch due at the same instant may proceed in either order. Dispatching first exits the entered composite before its unfinished entry reaches the nested state; completing the entry first visits that nested state before the dispatch exits the composite. Both traces are valid linearizations of the same instant.
Fixture: action_explore_decision_in_loop (golden, explored).
start → again → pick ─ if passes < 2 → left { lefts++; passes++ } → again
─ if passes < 2 → right { rights++; passes++ } → again
─ if passes >= 2 → done
Derived constraints:
pickis followed by a performance of exactly one target on each pass (DecisionPerformance), so each pass adds one topassesand one to exactly one ofleftsandrights.- On the first two passes
passes < 2holds andpasses >= 2does not, sodoneis not selectable and one ofleft,rightis; on the thirdpasses = 2, onlydoneis selectable, and the loop ends. Every run ends withpasses = 2andlefts + rights = 2.
Open: which of left and right follows pick on each of the two passes. Each pass is its own
decision performance, constrained by nothing the earlier pass did, so the four sequences
left,left, left,right, right,left, right,right are all valid linearizations. Two of them
agree on the tallies, so they reach three outcomes: {lefts = 2, lefts = 1 ∧ rights = 1, rights = 2}.
Pinned outcome: that admissible set, stated as outcomes citing this section. The default takes
the first declared branch on every pass, so the golden records left twice. Exploration must
reach all three outcomes and no other in four runs, the third pass never being a choice point:
a decision whose guards leave one link selectable is not a choice, however many links it has.
Fixture: action_choice_shared_message_accept (golden).
start → split ⇉ sendOne { send 1 } → sendTwo { send 2 } ─┐
⇉ left accept x : Integer ─┤→ sync → recorder { a := x; b := y } → done
⇉ right accept y : Integer ─┘
Derived constraints:
sendOne,sendTwo,leftandrightare each performed exactly once (ForkAction; a plain step is one performance), andsendOneends beforesendTwostarts (HappensBefore).- Each send is followed by exactly one
MessageTransfercarrying its payload (SendPerformance::sentTransfer,succession self then sentTransfer), so two transfers exist, one carrying1and one carrying2, and the first is sent before the second. - Each accept ends after exactly one transfer to
thisand yields that transfer's payload (AcceptPerformance::acceptedTransfer,succession acceptedTransfer then self.endShot,binding payload = acceptedTransfer.payload), soxandyeach end as1or2, never0. syncfollowssendTwo,leftandright(JoinAction), so both accepts have ended, and both sends, beforerecorderreadsxandy.
Open: which transfer each accept takes. The transfers' HappensBefore links order each send
before the accept that takes its transfer and nothing else: no link orders left against right,
and acceptedTransfer names a transfer to the receiver, not the one a particular send made. A
transfer is one link object with one payload; the reading this record relies on is that a
transfer is accepted once — the two accepts take the two transfers, one each — which is the
reading under which a second accept parked at a receiver waits for a second message rather than
re-reading the first. Under it either pairing is admissible: left takes 1 and right 2, or
the reverse.
Pinned outcome: the admissible set {a = 2 ∧ b = 1, a = 1 ∧ b = 2}, stated as outcomes citing
this section; exploration reaches each twice (4 runs, 2 outcomes, complete): once the first
message is in flight, sendTwo and both accepts are able to act, and an accept picked first takes
the message while the other must wait for sendTwo, whereas sendTwo picked first leaves both
messages to the two accepts and the accept picked next takes the older. The partial order the
library fixes among the nodes is stated as .trace.order
constraints (split < sendOne, split < left, split < right, sendOne < sendTwo,
sync < recorder, recorder < done); the join's predecessors are not stated as constraints on
sync because a token parks at a join before the join performs, so the entry first mentioning
sync may precede the last branch's arrival. The exact trace golden records the default
scheduling: the accepts are stepped before the sender, both park, and when the first message is
in flight the accept declared last is stepped first and takes it (choice step 4: tokens 2@sendTwo, 3@left, 4@right (unordered; took 4@right first)), leaving 2 to left — a = 2,
b = 1. Under declared and seed:1 (.declared.trace.golden, .seed-1.trace.golden) the
sender is stepped first and the accept declared first takes the message it just sent, in two
successive steps (choice step 3: tokens 2@sendOne, 3@left (unordered; took 2@sendOne first),
then 2@sendTwo, 4@right), so left takes 1 and right takes 2 — a = 1, b = 2. That
linearization reaches two choice points where the default reaches one: the reporting rule (every
choice point a run reaches is reported) applied to a different run, not a difference in what the
model admits.
Two accepts addressed by two sends completing in either order: each payload is fixed, the one that stands is open
Fixture: w7d_send_via_port_to_receiver (golden).
start → sender { send 42 via senderPort to receiver; send 7 via senderPort to sibling }
→ split ⇉ receiver accept value : Integer via receiverPort { receiverGot := value } ─┐
⇉ sibling accept value : Integer via receiverPort { siblingGot := value } ─┤→ sync → done
Derived constraints:
sender,receiverandsiblingare each performed exactly once (a plain step is one performance; ForkAction), and both sends end beforesplitstarts (HappensBefore).- Each send is followed by exactly one
MessageTransfercarrying its payload to the receiver it names (SendPerformance::sentTransfer,succession self then sentTransfer), soreceiver's transfer carries42andsibling's carries7; neither accept can take the other's, soreceiverGotends42andsiblingGotends7in every run. - Each accept ends after its transfer and yields that transfer's payload
(
AcceptPerformance::acceptedTransfer,binding payload = acceptedTransfer.payload). Both declareaccept value : Integer, and an accept's payload is bound in the enclosing action body (the visibility theaccept_payload_*cases pin), so the two accepts write one feature,route'svalue. syncfollows both accepts (JoinAction), so both writes ofvaluehave ended before the action ends andvalueis42or7, never unset.
Open: the order of receiver against sibling. Each transfer's HappensBefore link orders its
own send before the accept that takes it and nothing else; no link orders the two accepts, and
the library has no conflict rule for two performances writing one feature, so the write that
stands is the one whose accept completes last — value = 42 when sibling completes first,
value = 7 when receiver does.
Pinned outcome: the admissible set {value = 42, value = 7}, with receiverGot = 42 and
siblingGot = 7 in both, stated as outcomes citing this section; .trace.order states the
partial order the library does fix (sender < split, split < receiver, split < sibling,
sync < done). The exact golden records the default schedule: sibling is declared last, so its
token is stepped first and receiver's write stands (choice step 4: writes value := 42 by token 2, value := 7 by token 3 (unordered; value := 42 by token 2 stood)), giving value = 42.
Exploration reaches both outcomes in two runs. That the payload of a nested accept is reported
as a feature of the enclosing action at all is the tool's reporting, not the library's; this
record derives only that, given that reporting, both values are admissible.
Fixture: state_choice_transition_conflict (golden).
idle ─ accept Go if level > 5 → low { route := 1 }
─ accept Go if level > 7 → high { route := 2 }
Derived constraints:
- A
StateTransitionPerformancefollows its trigger and its guard, and itstransitionLinkSource.exitfollows the guard (StatePerformances.kerml); the source performanceidleends once, so at most one transition out of it fires for one Go. - Both guards hold for
level = 8, so both transitions are enabled by the one event; the machine ends inloworhigh, never still inidle.
Open: which enabled transition fires. UML orders a transition on a descendant state before one
on its ancestor (the case state_choice_ancestor_priority_not_reported pins that rule, and the
executor does not report it as a choice); between two transitions on the same state nothing
in the library or the specification ranks them. The choice is the firing transition's: the
regions of a parallel state that select the same transition out of it make one choice, reported
once (state_choice_shared_ancestor_regions), and a composite state's transitions that lose to a
nested one were never chosen among, so nothing about them is reported
(state_choice_ancestor_outranked_not_reported).
Pinned outcome: the admissible set {route = 1 in low, route = 2 in high}, stated as outcomes
citing this section; exploration reaches each once (2 runs, 2 outcomes, complete), as it does for
the companion fixtures state_choice_shared_ancestor_regions and
state_choice_change_transition_conflict (2 runs, 2 outcomes each); state_explore_transition_conflict
states the same set for two transitions converging on one target, told apart by side and the
visit list. The executor examines every transition out of the state for the event,
fires the first declared, and records the choice (choice state idle on accept Go: transitions 1->low, 2->high (unordered; took 1->low)); the golden pins that linearization. As for a decision,
the transitions after the first enabled one are read in a preview that is undone, and one whose
guard cannot be evaluated is not an alternative and does not fail the dispatch: it is recorded as
an informational guard-unevaluable note naming the state, the event and the transition
(TestLaterGuardErrorIsNotAChoiceNorAFailure; fixture state_choice_unevaluable_transition,
golden). The first transition read is the run's own, and its failure fails the dispatch as it
always has (TestFirstTransitionFailureStillFailsTheRun). A state's completion is one occurrence
too: several unguarded completion transitions out of one state are the same choice, drawn when
the completion is dispatched, and the completion fires exactly one of them
(state_explore_completion_choice, 2 runs, 2 outcomes, complete).
The choice is recorded when the transition fires, not when it is selected: a transition selected
on the event reads its guard once more as it fires, and one another region's reaction has
meanwhile disabled does not fire and reports nothing
(TestNotesOfATransitionBlockedBeforeFiringAreDropped), while one whose effect then fails was
the run's choice all the same (TestNotesOfATransitionFailingInItsEffectAreKept). A change
occurrence is an event like a signal: two accept when transitions out of one state whose
conditions rise on one write are enabled by one occurrence, and the same rule applies — the
first declared fires, the rise is consumed for the others, and the choice is recorded
(choice state watching on change: transitions 1->cool, 2->hot (unordered; took 1->cool);
fixture state_choice_change_transition_conflict, golden; TestChangeTransitionChoice,
TestChangeTransitionChoiceUnderHierarchyAndRegions for the nested-wins and parallel-region
shapes).
Two branches of a choice enabled by the data the incoming effect wrote: exactly one is taken, which one is open
Fixture: state_choice_dynamic_conflict (golden, explored).
idle ─ accept Go { level := 8 } → pick ─ if level > 5 → low { route := 1 }
─ if level > 7 → high { route := 2 }
Derived constraints:
- A choice is a
DecisionPerformancereached through the segment into it: the segment's effect happens before what the segment leads to (TransitionPerformances.kerml,succession [*] effect then [1] transitionLink.laterOccurrence), so the guards the decision reads are read againstlevel = 8, not against thelevel = 0the machine held when Go arrived. UML says the same in words: a choice vertex's guards are evaluated dynamically, after the incoming transition's behavior has run, where a junction's are evaluated statically with the compound transition's enabledness (UML 2.5.1 §14.2.3.7,choiceandjunction). DecisionPerformance::outgoingHBLink: HappensBefore[1](ControlPerformances.kerml): exactly one branch follows, so the machine ends inloworhigh, never atpickand never in both.- Both guards hold for
level = 8, so both branches are enabled once the effect has run; had the guards been read before it, neither would hold and the transition would have no branch.
Open: which enabled branch is taken. Nothing in the library or the specification ranks two branches of one choice whose guards both hold.
Pinned outcome: the admissible set {route = 1 in low, route = 2 in high}, stated as outcomes
citing this section; exploration reaches each once (2 runs, 2 outcomes, complete). The executor
exits idle, runs the incoming effect, reads the branches in declaration order, takes the first
enabled one and records the choice at the choice vertex (choice choice pick: transitions 1->low, 2->high (unordered; took 1->low)); the golden pins that linearization, seed:1 the other one. As
for a transition conflict, branches after the first enabled one are read in a preview that is
undone. A choice with no enabled branch and no unguarded one fails the run at that instant with a
typed error naming the choice (TestRuntimeRobustness/state_choice_without_an_enabled_branch).
Two branches of a junction enabled when the transition is selected: exactly one is taken, which one is open
Fixture: state_junction_several_enabled_branches (golden, explored).
idle ─ accept Go → split ─ { route := 1 } → left
─ { route := 2 } → right
Derived constraints:
- A junction is a
DecisionPerformancelike a choice; what differs is when its guards are read. UML reads a junction's guards statically, with the enabledness of the compound transition, before any of its effects run (UML 2.5.1 §14.2.3.7,junction; §14.2.3.8.1, "compound transition"), so the two branches here, both unguarded, are both enabled when Go is dispatched fromidle. DecisionPerformance::outgoingHBLink: HappensBefore[1](ControlPerformances.kerml): exactly one branch follows, so the machine ends inleftorright, never atsplitand never in both, and exactly one of the two branch effects runs (TransitionPerformances.kerml,succession [1] transitionLinkSource then [*] effect, per segment taken).
Open: which enabled branch is taken. Nothing in the library ranks two branches of one junction
whose guards both hold, and UML says the same: when several outgoing transitions of a junction are
enabled, "one is chosen; the algorithm for making this selection is not defined" (PSSM 1.0
Junction003, restating UML 2.5.1 §14.2.3.9.1 on conflicting transitions).
Pinned outcome: the admissible set {route = 1 in left, route = 2 in right}, stated as outcomes
citing this section; exploration reaches each once (2 runs, 2 outcomes, complete). The executor
reads the branches when it selects the transition out of idle, takes the first enabled one and
records the choice at the junction as the transition fires, before idle is exited (choice junction split: transitions 1->left, 2->right (unordered; took 1->left)); the golden pins that
linearization, seed:1 the other one.
As at a choice, branches after the first enabled one are read in a preview that is undone. If no
branch has a way through, the compound transition is unenabled and the occurrence is handled as
unmatched; a junction reached only past a choice remains a run error. The distinction and its
fixtures are pinned in the no-way-through section above.
A junction with no way through: the transition is not enabled, and the occurrence is handled as unmatched
Fixture: state_junction_no_way_through_unmatched (trace golden),
state_junction_no_way_through_other_transition_fires,
state_junction_no_way_through_deferred, state_junction_dead_branch_not_drawn,
state_completion_no_way_through_dropped, state_history_default_no_way_through,
state_history_self_transition_default_no_way_through_restores, and
state_join_no_way_out_disables_last_segment (explored outcomes).
Derived constraints:
- UML 2.5.1 §14.2.3.7 (junction) and §14.2.3.8.1 (compound transition) are the extension's
reference for static route availability. The library declares
feature outgoingHBLink: HappensBefore[1](ControlPerformances.kerml), but does not define a state-machine junction's enablement or unmatched-event behavior. - The runtime checks a route before selecting its transition, only as far as the first choice.
A junction with no way through, or a join whose completing occurrence has no route out, leaves
the compound transition unenabled;
errNoWayThroughis the only route error that disables it. The occurrence can then select another enabled transition, remain deferred, or be discarded as unmatched. A completion with no way through is dropped. - A branch that reaches a later junction with no way through is removed from the current
junction's drawable branches; if one branch remains, it is followed without a draw. Cycles,
unevaluable guards and binding failures remain run errors. A route stays open at the first
choice: choices resolve dynamically on arrival and retain
ErrChoiceWithoutBranch; a dead junction reached beyond a choice remains a run error. - A history default with no recorded history is statically checked only when the source is outside the history owner. A transition from the owner or one of its descendants can exit the owner, record history, then restore it instead of being disabled based on a record that does not exist yet.
Open: when more than one ordinary transition can take an occurrence after a dead route is disabled, the existing transition-selection policy decides among them; this rule adds no new choice point for the unavailable route.
Pinned outcome: state_junction_no_way_through_unmatched leaves the machine in s2, logs
T3, and reports the original Start as unmatched. In
state_junction_no_way_through_other_transition_fires, the other Start transition fires; in
state_junction_no_way_through_deferred, the occurrence remains deferred. The dead branch in
state_junction_dead_branch_not_drawn is not drawn when the other route remains available.
state_completion_no_way_through_dropped keeps the source state active until a later signal;
the outside-owner history default is disabled, while the owner self-transition fixture restores
its recorded history. The join fixture ends in S3 with either T1.2 T5 or T1.4 T5.
A junction with two branches enabled in a region another region's reaction may disarm: drawn only as its transition fires
Fixture: state_junction_drawn_as_its_transition_fires (golden, explored).
work ─┬─ a: a1 ─ accept Go { armed := false } → a2
└─ b: b1 ─ accept Go [armed] → split ─ { route := 1 } → left
─ { route := 2 } → right
Derived constraints:
- One Go selects a transition in each region of
work; the two fire in an open order (the section before). Region b's guardarmedand the junction's branches are read when the transitions are selected, before either fires, and both branches hold. - Region a's effect disarms
armed. Fired first, it leaves b's transition unenabled when its turn comes, so b stays inb1,routestays 0 and nothing follows the junction: a compound transition whose guard no longer holds does not fire (UML 2.5.1 §14.2.3.9.1). Fired second, b's transition takes the junction and exactly one branch (DecisionPerformance::outgoingHBLink: HappensBefore[1]), so the machine ends inleftorright.
Open: the region order, and, when b fires, which enabled branch it takes.
Pinned outcome: the admissible set {a2+b1 with route = 0, a2+left with route = 1, a2+right with route = 2}, stated as outcomes citing this section; exploration reaches each once (3 runs, 3
outcomes, complete). The branch is drawn only as b's transition fires, after the region order is
drawn: a witness reads on accept Go: b1 first of a1, b1; junction split -> 2->right, in that
order, and the run in which a fires first draws nothing at the junction, so replaying its
witness meets no draw it does not list. The golden pins the a-first linearization, and so does the
seed:1 golden, whose draw falls the same way; the b-first runs are exploration's.
A junction's guards are read once, as its incoming transition is selected: a branch enabled then is taken though another region's effect since made its guard unevaluable
Fixture: state_junction_guards_read_once (golden, explored).
work ─┬─ a: a1 ─ accept Go { d := 0 } → a2
└─ b: b1 ─ accept Go → split ─ [v > 0] { route := 1 } → left
─ [v / d > 1] { route := 2 } → right
Derived constraints:
- A junction's guards are static: they are read when the compound transition through it is
selected, before any effect of the step runs (UML 2.5.1 §14.2.3.7,
junction; §14.2.3.8.1, "compound transition"). Withv = 4andd = 2both branches hold when Go is dispatched, so b's transition is enabled through either. - Region a's effect zeroes
d. Fired first, it leavesv / dunevaluable, but the guard is not read again: the enabled set b's transition was selected with stands, and the branch drawn from it is taken. Exactly one branch follows (DecisionPerformance::outgoingHBLink: HappensBefore[1]), so the machine ends inleftorright, never fails the run.
Open: the region order, and which enabled branch b takes.
Pinned outcome: the admissible set {a2+left with route = 1, a2+right with route = 2}, stated as
outcomes citing this section; exploration reaches each once per region order (4 runs, 2
outcomes, complete), the a-first run through the second branch among them. The golden pins the
a-first linearization through the first branch, and the seed:1 golden, its draws falling the same
way, the same one; the b-first runs are exploration's.
Every junction guard on a route is read once, as its transition is selected: a junction beyond a draw takes the branch enabled then though another region's effect since changed what its guards read
Fixture: state_junction_beyond_a_draw_read_once (golden, explored).
work ─┬─ a: a1 ─ accept Go { d := 0 } → a2
└─ b: b1 ─ accept Go → split ─ { route := 1 } → again ─ [4 / d > 1] { route += 10 } → left
─ { route := 2 } → again ─ [d == 0] { route += 20 } → right
Derived constraints:
- A compound transition's junction guards are static, on every junction of the route: they are
read when the transition is selected, before any effect of the step runs (UML 2.5.1 §14.2.3.7,
junction; §14.2.3.8.1, "compound transition"). Withd = 2,splithas both branches enabled andagain, beyond either, has its first branch enabled and its second not; b's transition is enabled throughsplit's two branches, each on toleft. - Region a's effect zeroes
d. Fired first, it would make4 / dunevaluable andd == 0hold, butagain's guards are not read again oncesplitis drawn: the route beyond each ofsplit's branches was settled with its transition, soagaintakes the branch enabled then, toleft, androuteis 11 or 12 — never 21 or 22, and the run never fails.
Open: the region order, and which of split's enabled branches b takes.
Pinned outcome: the admissible set {a2+left with route = 11, a2+left with route = 12}, stated as
outcomes citing this section; exploration reaches each once per region order (4 runs, 2
outcomes, complete), the a-first runs among them. The golden pins the a-first linearization
through the first branch, and the seed:1 golden, its draws falling the same way, the same one;
the b-first runs are exploration's.
A history without a record takes its default transition through a junction with two branches enabled: exactly one is taken, which one is open
Fixture: state_history_default_through_junction (golden, explored).
idle ─ accept Go → work.resume ─ (default) → split ─ { route := 1 } → w1
─ { route := 2 } → w2
Derived constraints:
workhas never been left when Go arrives, so its history holds no record and the default transition out of it is taken (UML 2.5.1 §14.2.3.7,shallowHistory), afterworkis entered.- The default transition ends at a junction; both branches hold, exactly one follows
(
DecisionPerformance::outgoingHBLink: HappensBefore[1]), so the machine ends inw1orw2with the one branch effect run.
Open: which enabled branch is taken, as at any junction.
Pinned outcome: the admissible set {route = 1 in w1, route = 2 in w2}, stated as outcomes
citing this section; exploration reaches each once (2 runs, 2 outcomes, complete). The draw is
recorded as a ChoiceTransition at junction split when the default route is taken, so it
appears among the run's notes and choices and in the trace like a junction reached from a
transition, and a seed replays it. The golden pins the first branch, seed:1 the other one.
Fixtures: state_explore_region_order (golden, explored), state_firing_units_interleaved
(golden, explored).
work parallel { a: a1 ─ accept Go { last := 1 } → a2
b: b1 ─ accept Go { last := 2 } → b2 }
Derived constraints:
- The regions of a parallel state are concurrent substate performances of it; the one Go is offered to both, and each region's transition has its own source, so neither outranks the other (the nested-wins rule of the previous section ranks a substate's transition against its enclosing state's, never one region's against a sibling's) and both fire.
- Each
StateTransitionPerformanceis ordered only against its own trigger, guard,transitionLinkSource.exitand target entry (StatePerformances.kerml,TransitionPerformances.kerml); no link joins one region's transition to the other's. UML says the same of the set of transitions selected for one event: the order in which they fire is not defined (UML 2.5.1 §14.2.3.9.4). - Both effects write
last, so the value that stands is the last write:last = 2whena's transition fires first,last = 1whenb's does; the machine ends ina2+b2either way.
Open: which region's transition fires first. The two orders reach two outcomes, told apart by
last and by the order a2 and b2 are visited in.
Pinned outcome: the admissible set {last = 2 visiting a2 then b2, last = 1 visiting b2 then a2},
stated as outcomes citing this section. The order is a choice point under every policy, drawn one
unit at a time — a firing's source exit, its effect and its target entry are its units, and the
draw is among the firings with a unit left — and reported as choice on accept Go: next a1(exit), b1(exit) (unordered; took a1(exit) first): declared and reverse take the firings whole in region
declaration order — a tool-defined order — and the default golden pins that linearization (a
first, last = 2); seed:<n> draws each unit, the seed:1 golden's draws falling on the same
order; explore varies every draw (took b1(exit) first in the witness of the second
outcome) and must reach both outcomes and no other. Here the finer grain reaches no third outcome,
since each region logs one write. state_firing_units_interleaved logs each source's exit and each
effect, so the grain shows: the units of one firing keep their order (transitionLinkSource then effect, TransitionPerformances.kerml), no succession joins them to the other firing's, and the
six linearizations of two chains of two — a source's exit falling between the other firing's exit
and effect among them — are the admissible set, each followed by the join's segment. The fixtures
state_call_trigger_regions, state_composite_region_depth_order,
state_composite_region_deeper_first and state_parallel_broadcast are this same shape and list
both orders as outcomes citing this section, the default golden of each pinning the
declaration-order linearization. state_change_region_order is the shape with a change
occurrence in place of the signal — one write of temp raises temp > 20 in both regions at once
— and lists the same two outcomes: a change occurrence is an event like any other, so the poll
that dispatches it draws the region order the same way (choice on change: next a1(exit), b1(exit) (unordered; took a1(exit) first)), and no order between the two raised conditions is derivable from the
library either.
Fixtures: state_region_entry_order (golden, explored), state_region_entry_order_uneven (golden,
explored), state_fork_branch_order (golden, explored), state_region_entry_nested_front (golden,
explored), state_history_restore_order (golden, explored).
idle ─ accept Go → work parallel { left: { entry { log += "left(entry) " } ; entry; then l { entry { log += "l(entry) " } } }
right: { entry { log += "right(entry) " } ; entry; then r { entry { log += "r(entry) " } } } }
Derived constraints:
- Entering a parallel state starts one substate performance per region, and those are concurrent
(SysML v2 §7.18.1: parallel substates are "performed concurrently"). Within a region the library
fixes the order the trace logs: the region's own entry precedes the entry of the state its
initial transition reaches (
StatePerformances.kermlStatePerformance:succession [1] entry then [*] middle, the substates beingmiddlesteps), soleft(entry) < l(entry)andright(entry) < r(entry). - No succession joins a step of one region's chain to a step of the other's, and the owner's entry
precedes both chains (the regions are its
middle), so the library leaves the two chains unordered against each other. - Every entry appends to
log, sologrecords the interleaving.
Open: the interleaving of the two chains. Two chains of two have six linearizations.
Pinned outcome: the admissible set of those six values of log, stated as outcomes citing this
section. The order is a choice point under every policy, drawn one unit at a time among the regions
with an entry left and reported as choice entering work: next left(entry), right(entry) (unordered; took left(entry) first); declared and reverse take the regions whole in declaration order — a
tool-defined order — and the default golden pins that linearization; seed:<n> draws each unit;
explore varies every draw and must reach all six and no other. state_region_entry_order_uneven
is the shape with one chain of one (left logs its entry, l nothing) and one of two: three
linearizations. state_fork_branch_order reaches the two regions through a fork instead of the
owner's initial transitions: each branch is a chain of its segment's effect then its target's entry,
the owner's entry is one performance (Actions.sysml ForkAction, one performance of every target,
of one work) performed by whichever branch is drawn to it first and preceding both targets, so the
four linearizations of {T1.1(effect), T1.2(effect)} around work(entry) are the set.
state_region_entry_nested_front makes one region's start state itself parallel: its two regions'
entries join the front the sibling region is drawn from, three chains of one, six linearizations.
state_history_restore_order restores two regions through a deep history: the restore enters the
recorded states as a front drawn the same way, and the fixture's eight outcomes are its two entry
orders on the first occurrence, two exit orders on leaving (the next section) and two restore
orders.
Fixtures: state_region_exit_order (golden, explored), state_history_restore_order (golden,
explored).
work parallel { left: l { exit { log += "l(exit) " } }
right: outer { exit { log += "outer(exit) " } ; r { exit { log += "r(exit) " } } } }
exit { log += "work(exit) " }
work ─ accept Go → rest
Derived constraints:
- A transition leaving
workends every active substate performance beforework's own exit (StatePerformances.kermlStatePerformance:succession [*] middle then [1] exit), and a nested state's exit precedes its parent's by the same succession one level down:r(exit) < outer(exit), and both ofl(exit)andouter(exit)beforework(exit). - The two regions are concurrent substate performances; no succession joins
l's exit tor's orouter's, so the chain of one and the chain of two are unordered against each other.
Open: the interleaving of the two chains. A chain of one and a chain of two have three linearizations.
Pinned outcome: the admissible set of those three values of log, each ending in work(exit),
stated as outcomes citing this section. The order is a choice point under every policy, drawn one
unit at a time among the regions with an exit left and reported as choice exiting work: next l(exit), r(exit) (unordered; took l(exit) first); declared and reverse leave the regions whole
in declaration order — a tool-defined order — and the default golden pins that linearization;
seed:<n> draws each unit; explore varies every draw and must reach all three and no other.
Fixture: state_explore_time_trigger_tie (golden, explored).
work parallel { a: a1 ─ accept after 2 [s] { last := 1 } → a2
b: b1 ─ accept after 2 [s] { last := 2 } → b2 }
Derived constraints:
- The regions of
workare concurrent substate performances of it, sharing itslocalClock(Occurrences.kerml, "The localClock of a suboccurrence defaults to the localClock of its containing occurrence"); each region armsTriggerAfter(2, …)on entering its first state att=0(Triggers.kerml), and each ends when that clock reads 2 (TimeSignal::signalCondition). The two are two acceptable events, one per transition, each firing its own region's transition. timeOrderingConstraint(Clocks.kerml,TimeOf) orders only occurrences already ordered byHappensBefore, and noHappensBeforechain joins one region's transition to the other's; the pool order (earlierFirstIncomingTransferSort,Occurrences.kerml) ranks transfers, which a time signal is not. Neither dispatch precedes the other.- Both effects write
last, so the value that stands is the last write:last = 2whena's timer dispatches first,last = 1whenb's does; the machine ends ina2+b2either way.
Open: which time event dispatches first. The two orders reach two outcomes, told apart by last
and by the order a2 and b2 are visited in.
Pinned outcome: the admissible set {last = 2 visiting a2 then b2, last = 1 visiting b2 then a2},
stated as outcomes citing this section. The order is a dispatch-order choice point under every
policy, reported as choice events at t=2.0: time a1 1->a2, time b1 1->b2 (unordered; dispatched time a1 1->a2 first): declared and reverse dispatch the earlier armed first — a tool-defined
order — and the default golden pins that linearization (a first, last = 2); seed:<n> draws
the order; explore varies it and must reach both outcomes and no other, in two runs. A time event
due together with a signal already in the pool at the same instant is the same choice; two pool
events are not, their arrival order being library-derived (earlierFirstIncomingTransferSort).
A completion event precedes both, never drawn.
Fixtures: state_concurrent_do (golden, explored), state_anonymous_do_atomic (golden, explored),
state_concurrent_inline_do_bodies (golden, explored), state_concurrent_do_action_bodies_timed
(golden, explored).
Interleave parallel { left: lwork { do { seq := seq*10+1; seq := seq*10+2; seq := seq*10+3 } }
right: rwork { do { seq := seq*10+4; seq := seq*10+5; seq := seq*10+6 } } }
Derived constraints:
- A state's do behavior is a performance nested in the state performance, after its entry and
before its exit (
StatePerformances.kermlStatePerformance:succession [1] entry then [*] middle; succession [*] middle then [1] exit), so each region's do behavior runs while its state is active and the statements of one body keep their declared order (1 < 2 < 3,4 < 5 < 6). - The regions of a parallel state are concurrent substate performances; no succession joins a step of one region's do behavior to a step of the other's, so the library orders nothing between them. The executor shares the machine one action at a time — each behavior with an action due performs one before any performs its next — and which of the due behaviors acts first in a round is a tool-defined order.
- Every statement writes
seq, so the digits record the interleaving: instate_concurrent_do, each region's start state completes as it is entered, and the pool dispatches the two completions in the order they were generated (PSSM §8.5.9) — the order the entry draw entered the two start states — so the region entered first enters its working state one step before the other (its first digit is alone in its round), the next two rounds each have both due, and the other's last statement is alone again (its last digit last).
Open: which region's start state is entered first (the entry draw, whose two orders the pool
follows), and which region's do behavior acts first in each round both are due in. Two entry
orders of two rounds of two orders reach eight values of seq.
Pinned outcome: the admissible set {124356, 142356, 124536, 142536, 415263, 451263, 415623, 451623}, stated as outcomes citing this section. The entry order is the choice point of
regions entered on one occurrence,
reported as choice entering Interleave: next lstart(entry), rstart(entry) (unordered; took lstart(entry) first) — an entry that performs nothing but generates a completion event is drawn,
its place in the pool being observable — and the round's order is a choice point under every
policy, reported as choice do round at t=0.0: states lwork, rwork react (unordered; took lwork first): declared and reverse take region declaration order at both and the default golden
pins that linearization (124356); seed:<n> draws both; explore varies both and must reach
all eight values and no other. state_anonymous_do_atomic is the same machine with each body
written as do action { … } rather than the braced do { … }; the two spellings are one
anonymous inline action of three statements, and an inline body yields after each statement,
so both interleave and reach the same eight values, not 123456. state_concurrent_inline_do_bodies writes the left body as a for
loop over 1..2 followed by a statement and the right one as a statement followed by an if block
of two: an iteration and a statement of a nested block are each one step, so the same eight values
and no other are reached.
state_concurrent_do_action_bodies_timed is the shape with action bodies
that wait on the clock: both behaviors pause at an accept after 2 [s] and are due again in the
round at t=2.0, where the order of the two counts is open (1324 entering order, 3124 the
other), while the counts at t=4.0 and t=5.0 are alone in their rounds.
Transitions into a join: each exits its source and runs its effect before the owner is left, in which order is open
Fixture: state_join_runs_every_incoming_effect (golden, explored),
state_join_segment_fires_on_own_signal (trace golden), and
state_join_segments_arrive_together_on_one_occurrence and
state_join_segment_not_chosen_does_not_arrive (golden, explored).
Outer { exit { log += "outer(exit) " }
Work parallel { left: l1 ─ accept X: exit + effect → sync
right: r1 ─ accept Y: exit + effect → sync }
sync join ─ effect → Rest { entry { log += "rest(entry)" } } }
Derived constraints:
- The library has no join among states (
forkandjoinare action nodes,Actions.sysmlForkAction/JoinAction); the state-body form follows UML 2.5.1 §14.2.3.8.1 and PSSM §8.5.7JoinPseudostateActivation. Each incoming segment is its ownStateTransitionPerformance, with its ownfeature trigger: MessageTransfer[*];andprivate succession [1] transitionLinkSource then [*] effect;(TransitionPerformances.kerml): the source exits before that segment's effect, and a later trigger can fire another segment independently. A completion segment fires on its own completion; it is not combined with another completion event. - The library does not state that one segment's occurrence waits for another, or provide a succession connecting the segments across regions. The independent-occurrence join rule is therefore UML's extension reference, not a claim supplied by the KerML library: arrived segments plus those enabled by the current occurrence and chosen by their regions in that dispatch complete the join only when all incoming segments are covered. A not-yet-arrived segment fires only when its source is active, its trigger takes the occurrence, its guard holds, and its region's dispatch chose that transition; a competing or nested transition chosen by the region is not displaced by a sibling's join firing.
- At selection, probes count only the candidate segment plus prior arrivals, and check the way
out only if they complete the join. In signal/change dispatches, same-occurrence peers count
only when their regions chose them; firing checks completion against those peers, and a dead
route fires none. Timer expiries are separate occurrences even when due at the same instant;
a dead completing segment is not enabled, leaving its timer group's alternatives available.
state_join_peer_not_chosen_does_not_block_arrival,state_join_time_segments_expire_togetherandstate_join_dead_timer_join_keeps_group_alternativepin these distinctions. - A substate's steps are enclosed in its owner's state performance and are "and hence happening
during the state performance" (
StatePerformances.kerml); the library orders the owner's middle steps before its exit withprivate succession [*] middle then [1] exit;. Thus the runtime leavesWorkand thenOuterafter the last incoming segment effect, before the outgoing segment's effect andRest's entry. PSSM instead leaves the owner before the final segment effect; the runtime's owner-exit ordering is the extension's reading recorded under SM34.
Open: which segment fires first when one occurrence enables several; the library orders neither segment against its sibling.
Pinned outcome: state_join_runs_every_incoming_effect pins both incoming-effect orders, each
ending with outer(exit) sync(effect) rest(entry); exploration reaches exactly both. In
state_join_segment_fires_on_own_signal, signal X logs its source exit and effect, then waits
without running Y's segment; signal Y logs its source exit and effect, followed by the owner exit
and outgoing effect. state_join_segments_arrive_together_on_one_occurrence shows one Go
occurrence firing both enabled segments in either join sync order, recording both arrivals;
Finish then fires the last segment, leaves the owner and runs the outgoing effect. Each segment's
source exit and effect are one ordered unit, while arrivals are retained until the incoming set
is complete.
state_join_segment_not_chosen_does_not_arrive shows that Go fires A's segment while the middle
region takes its nested b1 transition; Finish records C, and the later Go fires B's segment and
completes the join. state_join_peer_not_chosen_does_not_block_arrival shows that an unchosen
peer cannot make a dead way out disable A when A would arrive alone. The region's first dispatch
does not also fire the unchosen B segment.
Each queued timer expiry is its own occurrence, even when several timers are due at the same
instant. Selection checks a segment with only the arrivals already recorded: an incomplete
segment arrives on its own expiry, while a later expiry completes the join. If that completion
has no way out, its segment is disabled and the other transitions in its source's timer group
remain available. If B's expiry is dispatched before A's, B's group can choose either its
incomplete join segment or its alternative. state_join_time_segments_expire_together pins the
separate arrivals; state_join_dead_timer_join_keeps_group_alternative pins the dead-exit
alternative and its admissible outcomes.
Fixture: action_merge_loop_reenters (golden), after the specification's ChargeBattery.
start → continueCharging(merge) → monitor → decide ─ level < 100 ─→ addCharge ─┐
↑ └ level >= 100 → endCharging → done
└────────────────────────────────────────────────────────────────────┘
Derived constraints:
- Each
decideperformance is followed by a performance of the one target whose guard holds (DecisionPerformance:outgoingHBLink"is an instance of exactly one of the Successions, ordering the DecisionPerformance as happening before an instance of the target"). then continueChargingafteraddChargeis aHappensBeforelink from thataddChargeperformance to acontinueChargingperformance, so eachaddChargeis followed by a merge performance, which is followed by amonitorperformance and adecideperformance.- Each merge performance follows exactly one source performance —
startfor the first, the latestaddChargefor the others — and a merge's incoming successions have source multiplicity 0..1, so the merge reached fromstartneeds noaddChargebefore it (MergeAction, MergePerformance). monitorincrementspasses;addChargeadds50tolevel, which starts at0.
Open: nothing that is observable; the loop is a chain. The fixture pins the one outcome without
outcomes; under explore the run reaches no choice point (1 run, 1 outcome, complete).
Fixed outcome: monitor runs with level at 0, 50 and 100; the third decide selects
endCharging; level = 100, passes = 3. This is the reading the specification's example and
the pilot corpus's comment ("a merge node is necessary to prevent a loop of successions from
being unsatisfiable") take for granted: the merge exists so the loop can be re-entered.
Executor: level = 100, passes = 3. stepMergeNode keeps no record of earlier traversals:
each arriving token is one MergePerformance, its body runs, then the guard on the merge's one
outgoing succession is read and the token is forwarded or retired, so the token carrying the loop
re-enters continueCharging as often as decide sends it back. The golden shows monitor reading level -> 0, -> 50
and -> 100, the third decide evaluating level >= 100 -> true, then endCharging and
done. A merge is the one node several successions reach that does not synchronize (the
previous two cases): ActionExecutor.synchronizes exempts MergeNode, since
MergePerformance follows one source performance while a plain node or join follows one per
succession. Loop termination is the guard's and the step budget's job, not the merge's:
robustness_test.go:unguarded_loop_through_a_merge runs start → m(merge) → a → m with no
exit to ErrActionStepLimitExceeded, both under RunToCompletion and after stepping it.
Fixture: action_merge_loop_three_passes (golden).
start → again(merge) → work → decide ─ count < 3 ─┐ again { merged := merged + 1 }
↑ └ count >= 3 → done work { count := count + 1 }
└───────────────────────────────────────┘
Derived constraints:
- The same chain as above: each
decideis followed by the one target whose guard holds (DecisionPerformance); eachdecide → againsuccession is aHappensBeforelink to a merge performance, followed by aworkperformance and adecideperformance. - A merge performance is one performance with its own steps (
MergeActionis anAction;Actions.sysmlAction::merges : MergeAction[0..*]), so its body runs once per merge performance, i.e. once per arrival.
Open: nothing observable. The fixture pins the one outcome without outcomes; under explore
the run reaches no choice point (1 run, 1 outcome, complete).
Fixed outcome: count = 3, merged = 3 — three passes of work, three merge performances.
The executor agrees; the golden shows again's body and work's body alternating three times
before count >= 3 -> true.
Fixture: action_merge_fork_branch_and_loop (golden).
start → split ⇉ gate(merge) → work → more(decide) ─ passes < 3 ─┐ gate { merged := merged + 1 }
⇉ prep → ↑ └ passes >= 3 → done work { worked := worked + 1 }
└────────────────────────────────────────┘ more { passes := passes + 1 }
Derived constraints:
splitis followed by exactly one performance ofgateand one ofprep(ForkAction, target multiplicity 1..1);prepis followed by agateperformance (HappensBefore). Each is its own merge performance following its own one source (MergePerformance, source multiplicity 0..1), and neither waits for the other: two tokens are downstream ofgate.- Each
gateperformance is followed by aworkperformance and amoreperformance; eachmoreis followed by the one target whose guard holds (DecisionPerformance), andmore → gateis aHappensBeforelink to a further merge performance. more's body is a step of the decision performance, so it ends before the guard on the outgoing succession is read (HappensBefore orders the whole source performance before its target). Eachmoreperformance therefore reads apassesit has itself just incremented.
Open: the interleaving of the two tokens at every node; which token takes which exit. The outcome
does not depend on it, so the fixture pins the one outcome without outcomes; under explore
every interleaving — a choice between the two tokens at each step until one of them is done, the
two threads dividing the four passes as 3 + 1, 2 + 2 or 1 + 3 — reaches it. There are
8526 of them, more than the default budget of 1024 runs, so explore alone reports
incomplete: runs budget 1024 hit after 1024 runs with the one outcome tabled, and
explore:runs=10000 completes (8526 runs, 1 outcome, complete).
Fixed outcome: passes = 4, merged = 4, worked = 4. Whatever the interleaving, passes
takes the values 1, 2, 3, 4 one more performance at a time, the two that read 1 and 2 select
gate and the two that read 3 and 4 select done, so the merge is reached twice from the fork
(once directly, once through prep) and twice from the loop, and work follows each of the
four. The executor agrees; the golden shows the direct token at gate in step 2 while the
other is still at prep, the two loop re-entries at steps 5 and 6, and done reached twice.
Had passes been counted in work instead, the outcome would depend on the interleaving
(one token's more may read the other's write), which is why the count is where it is.
Fixtures: f63_merge_body_runs_on_traversal (golden) and action_merge_body_flips_own_guard
(golden).
start → split ⇉ gate(merge) ─ if ready ─→ tail → done gate { mergeRuns := mergeRuns + 1 }
⇉ slow → slower → ↑ slower { ready := true }
tail { passed := mergeRuns }
start → count(merge) ─ if arrivals < 3 ─→ work ─┐ count { arrivals := arrivals + 1 }
↑ │ work { continued := continued + 1 }
└─────────────────────────────────────┘
Derived constraints:
- Each arrival at
gateis its own merge performance (MergePerformance,incomingHBLink[1]), and a merge performance is anActionwith its own steps (Action::merges : MergeAction[0..*]), sogate's body runs once per arrival:mergeRunsreaches 2 in the first model,arrivalscounts every entry tocountin the second. succession first gate if ready then tailis aDecisionTransitionAction— "the base type of TransitionUsages used as conditional successions in action models" (Actions.sysml) — hence aNonStateTransitionPerformancewhosetransitionLinkSource: Performance[1]is thegateperformance (binding transitionLink.earlierOccurrence = transitionLinkSource) and which happens after it:succession [1] transitionLinkSource then [1] Performance::self(TransitionPerformances.kerml). The guard is a step of that later performance, so it reads the state the complete merge performance left, the merge body's write included.- The guard constrains the link, not the source:
TPCGuardConstrainttiesconstrainedHBLink(transitionLink: HappensBefore[0..1]) toconstrainedGuardwithallTrue(constrainedGuard()), so a false guard means theHappensBeforelink totail(orwork) does not exist. The merge performance it would have left exists regardless — nothing in the library makes a performance conditional on its outgoing links.
Open: in the first model, the interleaving of the direct arrival with slow → slower; the
outcome does not depend on it, since ready is written before the second arrival either way.
Both fixtures pin one outcome without outcomes; under explore the first reaches it by every
interleaving of the direct arrival with the slow → slower branch (22 runs, 1 outcome, complete) —
when slower runs before the direct arrival reaches gate, that arrival reads ready = true and
goes on to tail too, and the two tokens' moves through gate, tail and done interleave — and
the second, a chain, reaches no choice point (1 run, 1 outcome, complete).
Fixed outcome, first model: ready = true, mergeRuns = 2, passed = 2 — the direct arrival
performs gate and reads ready = false, so no link to tail follows it; the second arrival
performs gate again, reads ready = true, and tail reads the two merge performances.
Second model: arrivals = 3, continued = 2 — the third count performance's own increment is
what its guard reads, so work does not follow it and the action ends with no token. Had the
guard been read before the body, work would have followed the third arrival (continued = 3),
which is the observable the second fixture pins.
Executor: both agree. stepMergeNode runs the body, then evaluates the outgoing succession's
guard and retires the token when it is false; the goldens show assign mergeRuns before each
eval feature ready, tail reading mergeRuns -> 2, and in the second model arrivals < 3 -> false read straight after the increment that made it so, followed by no active tokens.
Fixture: state_transition_guard_exit_effect_entry_order (golden).
active { exit { x := 0 } } ── accept Go if x > 0 do y := x ──→ finished { entry { z := y + 1 } }
x = 5 initially
Derived constraints:
- The guard is evaluated after the trigger and before the source's exit
(
StateTransitionPerformance:acceptable then guard,guard then transitionLinkSource.exit), so it readsx = 5and holds. - The exit ends the source state performance (
StatePerformance:middle then exit), and the effect follows the source performance (TransitionPerformance:transitionLinkSource then effect), so the effect reads the exit'sx = 0. - The target state performance follows the effect (
effect then transitionLink.laterOccurrence), and its entry is its first step (entry then middle), so the entry reads the effect'sy = 0.
Open: nothing observable; the transition is a chain. The fixture pins the one outcome without
outcomes; under explore the run reaches no choice point (1 run, 1 outcome, complete).
Fixed outcome: x = 0, y = 0, z = 1, final state finished. The executor agrees; the golden
shows the guard reading x -> 5, then exit: active, then assign y reading x -> 0, then
enter: finished with assign z reading y -> 0. The guard appears twice in the golden; that is
the tool detail noted above, not a second reading the library asks for.
Fixture: clock_action_state_due_together (golden).
part beacon : Beacon exhibit state blinking { dark ─ accept after 5 [s] → shining { lit := true } }
action watcher start → arm { armed := beacon.lit == false } → wait accept after 5 [s]
→ look { sawLit := beacon.lit } → done
Derived constraints:
- Every occurrence's
localClockdefaults to theuniversalClock, and a suboccurrence's to its container's (Occurrences.kerml,feature localClock : Clock[1] default universalClock; "The localClock of a suboccurrence defaults to the localClock of its containing occurrence"), so the action and the machine time their accepts against one clock, whosecurrentTime"advances monotonically" (Clocks.kerml,Clock). accept after disTriggerAfter(d, receiver, clock), which "returns … TriggerAt(clock.currentTime- delay, …)" (
Triggers.kerml): the instant is fixed when the accept is reached,0 + 5for both here, sincearmreadsbeacon.litat instant 0, materializing the beacon and starting its machine before the action's own wait is set.
- delay, …)" (
- Each accept ends after its
TimeSignal, whose condition is "the currentTime of the signalClock being equal to the signalTime" (Triggers.kerml,TimeSignal::signalCondition;AcceptPerformance,succession acceptedTransfer then self.endShot), so neitherlooknor the entry ofshininghappens before the clock reads 5, andarmreadslitstillfalse.
Open: the order of look and the entry of shining. Each follows its own accept, and the two
accepts end at the same reading of the one clock; no HappensBefore chain connects a step of the
action to a step of the machine, and timeOrderingConstraint (Clocks.kerml, TimeOf) orders
only occurrences already ordered by HappensBefore. look therefore reads lit either before or
after shining's entry writes it.
Pinned outcome: the admissible set {armed ∧ sawLit, armed ∧ ¬sawLit}, stated as outcomes
citing this section. The executor draws the order of executors due at one instant through the
scheduling policy and records it as a due order choice naming the executors in the order they
were created: under the default policy the last created runs first, as the token order is
reversed — the beacon's machine, created when arm materialized the beacon, runs before the
action, and look reads lit = true (choice at t=5.0: due action watcher, state machine blinking of object #1 (unordered; ran state machine blinking of object #1 first)); under
declared the action, created first, runs first and reads lit = false; seed:1 draws one of
the two (.declared.trace.golden, .seed-1.trace.golden). One executor alone due at an instant
is not a choice and is not reported.
Fixtures: state_do_step_or_dispatch (golden, explored), state_do_step_among_completions
(golden, explored), state_do_step_or_tied_dispatch (golden, explored),
state_do_step_cuts_typed_do (golden, explored), state_do_step_cuts_nested_perform (golden,
explored), state_do_step_cuts_control_node_body (golden, explored, checked),
state_do_action_loop_timed_exit (explored, checked).
state Machine { attribute log : String = "";
entry; then top;
state top { do action work { first start; then action mark assign log := log + "did "; then done; } }
transition first top accept Stop do assign log := log + "stop " then idle;
state idle; }
Derived constraints:
- A state's do behavior starts before the state's other middle steps start and is otherwise
concurrent with them (
StatePerformances.kermlStatePerformance,succession do.startShot then nonDoMiddle.startShot); the succession is on the do performance's start, not on its first action, so a do behavior that has begun and not yet performed its first action is a state the library admits while the machine dispatches. - A transition's accept precedes its source's exit (
TransitionPerformances.kermlStateTransitionPerformance,accept then transitionLinkSource.exit), and the exit ends the do behavior with the state (succession [*] middle then [1] exit): a dispatch that leaves the state cuts the do behavior off wherever it stands. - No
HappensBeforechain connects an action of the do behavior to the dispatch of an occurrence in the machine's pool, so the library orders nothing between the two. - An occurrence no performance accepts is not a step of any performance: dropping it, or holding
it deferred, moves nothing in the
StatePerformance, so there is nothing to order against the do behavior's action — and the do behavior's next action may be theacceptthat takes it. The draw is between the do step and a dispatch that takes its occurrence: fires a transition, or lets a do behavior already parked at anacceptgo on.
Open: whether the do behavior's next action or the dispatch goes first, at every instant both are
due. In the fixture Stop is in the pool as top is entered, so log ends did stop or
stop .
Pinned outcome: the admissible set {did stop , stop }, stated as outcomes citing this section.
Under check, replay and explore the order is a choice point reported as choice at t=0.0: next do top, dispatch accept Stop (unordered; took do top first): one move is one token move of
a state's do behavior — a statement of an inline body, a step of a do behavior given as an
action, a token inside a nested perform — drawn against the dispatch the machine would make now
(do <state> naming the due states, then dispatch <event>), and the draw is made again after
every move while a do behavior is due, so the dispatch may cut the flow anywhere or wait for it
to rest. A dispatch that would drop its occurrence is not drawn ahead
of a due do step; it waits until no do move is due, as under the fixed policies, so an occurrence a
do behavior is about to accept — Tick in state_join_completion_segment_waits_for_do_behavior,
b1's timer in state_join_completion_is_not_a_timers_expiry — is not lost to the draw, and
those fixtures keep their admissible sets. declared, reverse and seed:<n> run the whole do
round — every due do behavior, each steppable token once — and dispatch after it, so their traces
record no such choice and end did stop ; explore must reach both outcomes and no other, and
the fixed policies' run is always among the runs check tables. state_do_step_cuts_typed_do
makes the do behavior a typed action of two steps whose inout writes back as it ends: the
dispatch cuts it at either step (count = 100) or takes it after it ended (111).
state_do_step_cuts_nested_perform performs that action from an inline do body between two
assignments: 1000 (cut before the first), 1001 (after it, or inside the perform, whose
write-back is lost), 1012 (after the perform), 1112 (after the body ended).
state_do_step_cuts_control_node_body forks the do flow through a fork with a body of its own
(fork split { assign count := count + 1; }): a control node's body is performed by the token
passing through it, so it is a move the dispatch may fall before (1000) or after (1001, the
fixed policies' run, whose sweep moves each token once and so ends at the fork), then after
either branch (1011, 1101) or both (1111) — five outcomes, exact under check. A control
node with no body only routes control, and where between two moves it falls no other move
observes, so it is not drawn.
state_do_action_loop_timed_exit loops a forked do flow through timed waits against a timed
exit due at the same instant: the exit may cut the flow before either branch writes, after one,
or after both — the fixed policies' left = right = 1 — four outcomes, exact under check.
state_do_step_among_completions is the shape with two regions'
completion effects for the dispatch: a region's do step and the other region's completion are
each drawn at every instant both are due, and the order among the completions themselves is the
entry draw's, which the pool follows (§8.5.9) — the do step falls before, between or after the
two effects in either of their orders, six outcomes. state_do_step_or_tied_dispatch ties two time triggers at the instant the
do step is due, one guarded on what the step writes: each tied event is previewed on its own, so
the unguarded trigger alone is drawn against the step (choice at t=2.0: next do top, dispatch time top 2->idle) and the guarded one, which the dispatch would drop before the step, waits for
the round to close, where the two are a dispatch order; log ends did one , did two or
two , and explore reaches the three and no other. Were the tied events judged together, the
dropped one would hide the acting one behind the step and two would be lost.
Fixtures: state_do_step_before_sibling_entry (golden, explored, checked),
state_do_step_before_sibling_entries (golden, explored, checked),
state_do_step_before_nested_entries (golden, explored, checked),
state_do_step_nested_before_outer_entry (golden, explored, checked),
state_do_step_before_fork_branch (golden, explored, checked),
state_do_step_before_history_restore (golden, explored, checked),
state_do_step_typed_before_sibling_entry (golden, explored, checked),
state_do_step_cut_by_sibling_completion (golden, explored, checked),
state_do_step_cut_by_sibling_terminate (golden, explored, checked).
idle ─ accept Go → work parallel { left: { entry; then l1 { do { log += "did " } } }
right: { entry; then r1 { entry { log += "r1(entry) " } } } }
Derived constraints:
- Entering
workstarts one performance per region, concurrent with each other (SysML v2 §7.18.1); withinleft,l1's entry precedes its do behavior's start (StatePerformances.kermlStatePerformance,succession [1] entry then [*] middle, the do behavior amiddlestep whose start precedes the other middle steps' starts), sol1(entry) < did. - No succession joins a step of
left's chain to a step ofright's (the previous section's derivation for the entries), and the do behavior's actions are steps ofleft's chain: the library ordersdidafterl1's entry and against nothing inright. Thatr1's entry is another unit of the same entry occurrence orders nothing — the do behavior has started and its next action is due as any other due action is. - Every write appends to
log, sologrecords the interleaving.
Open: whether the do behavior's next action or the sibling's remaining entry goes first, at every
draw of the entry front where both are left. The fixture's log ends did r1(entry) or
r1(entry) did .
Pinned outcome: the admissible set {did r1(entry) , r1(entry) did }, stated as outcomes citing
this section. The step is a unit of its region's queue on the entry front: once the queue has
performed its entries — the entry unit that started the do behavior and, below a composite, its
substates' — each due token move of that behavior is drawn against
the sibling regions' remaining entry units under the front's own draw — the entering <owner>
choice, its alternative labeled do <state> beside the entries — for as long as a sibling has a
unit left; when none has, the remaining moves fall to the do-step site of the previous section,
drawn against the dispatch after the entry move settles. declared, reverse and seed:<n>
never take the alternative: they run the entries whole, as before, and the do round after the
move settles, so their traces record no such draw and end r1(entry) did ; check, replay
and explore draw it at every unit and reach both outcomes and no other.
state_do_step_before_sibling_entries leaves two sibling regions' entries, m1 and r1: the
step falls before, between or after them in either of their orders, six outcomes.
state_do_step_before_nested_entries makes the sibling's start state parallel: its regions'
entries a1, b1 are units of the same front and the step is drawn against each while one is
left, six outcomes; state_do_step_nested_before_outer_entry puts the do behavior in that nested
state instead, drawn against the outer sibling's l1 as against its own sibling's b1, six
outcomes. state_do_step_before_fork_branch reaches the regions through a fork: the target one
branch enters starts its do behavior, and the step is drawn against the other branch's effect and
its target's entry, three outcomes. state_do_step_before_history_restore restores two regions
through a deep history, one of them into a state with a do behavior: the restore is a front drawn
the same way, so the step falls before or after the other region's restored entry, on top of the
first occurrence's firing — where the step falls before the other region's entry, after it, or
not at all, the Pause already in the pool cutting it (the previous section's draw) — and the
exit's two orders: twelve log values.
state_do_step_typed_before_sibling_entry gives the do behavior as a typed action def of two
steps with an inout written back as it ends: each step is one move, drawn against the sibling's
entry while it is left, and the sibling's write lands before the write-back, which overwrites
it, or after both steps: two count values. state_do_step_cut_by_sibling_completion completes the
sibling's state into a transition that leaves the parallel state: the do behavior's two steps
are drawn against the sibling's entry and then, as the previous section has it, against the
completion's dispatch that cuts them off, six outcomes; state_do_step_cut_by_sibling_terminate
is PSSM Terminate 002's shape, the sibling completing into a terminate that ends the machine,
and the do activity's first segment falls before the sibling's entry, after it, or never, its
second — beyond an accept the terminate leaves unfed — never; with the two entry orders, five
outcomes, PSSM's five admitted traces.
Fixtures: state_do_step_before_own_substate_entries (golden, explored, checked),
state_do_step_before_own_body_entry (golden, explored, checked),
state_do_step_way_down_before_fork_branch (golden, explored, checked),
state_do_step_machine_before_top_entries (golden, explored, checked).
idle ─ accept Go → work parallel { do { log += "did " }
left: { entry; then l1 { entry { log += "l1(entry) " } } }
right: { entry; then r1 { entry { log += "r1(entry) " } } } }
Derived constraints:
work's entry precedes its do behavior's start and its substates' entries alike (StatePerformances.kermlStatePerformance,succession [1] entry then [*] middle: the do behavior and the nestedStatePerformances are bothmiddlesteps), and PSSM §8.5.5 has the do activity start after the entry behavior and run concurrently with what follows it — sowork(entry) < didandwork(entry) < l1(entry),work(entry) < r1(entry).- No succession orders the do behavior's actions against the nested performances' entries: they
are concurrent
middlesteps of oneStatePerformance, as the previous section has the regions' chains concurrent with each other. - Every write appends to
log, sologrecords the interleaving.
Open: whether the do behavior's next action or a remaining substate entry goes first, at every
draw where both are left. The fixture's log is did before, between or after l1(entry) and
r1(entry) in either of their orders.
Pinned outcome: the six interleavings, stated as outcomes citing this section. The composite's
do behavior begins as its own entry unit ends, before its regions are entered, and its due token
move is drawn on the front entering them beside the regions' queues — the same entering work
choice, the alternative labeled do work — for as long as a region has a unit left.
declared, reverse and seed:<n> never take the alternative and end l1(entry) r1(entry) did
(reverse: r1(entry) l1(entry) did ); check, replay and explore reach the six and no other.
state_do_step_before_own_body_entry gives the composite a serial body two states deep, whose
entries no front orders: each entry on the way down is drawn against the step at its own
entering <owner> choice, did falling before w1(entry) , between it and w2(entry) , or
after both, three outcomes. state_do_step_way_down_before_fork_branch reaches the substates
through a fork: the first branch's way down enters the composite and starts its do behavior,
which is drawn against the branches' remaining target entries, six outcomes.
state_do_step_machine_before_top_entries is the same shape at the machine, whose do behavior
begins before its top regions are entered: six outcomes.
Nothing, at present: every derivation above is met and carries a golden. The table this section
held is empty and so omitted; internal/exec/runtime/testdata/conformance/known_failures.txt is
kept with only its header comments, because the harness reads it and because it is where the
next unmet derivation goes (see Adding a case).
The one entry it held, action_merge_loop_reenters, was met by removing the executor's
first-traversal record from stepMergeNode so that a merge passes every arriving token; the
case's expected outcome was not touched. The same change moved the merge's body ahead of its
outgoing guard, as every other node kind already had it, which is the one existing expectation
that moved: f63_merge_body_runs_on_traversal went from mergeRuns = 1, passed = 1 (an
arrival whose guard was false skipped the body) to mergeRuns = 2, passed = 2, per the
derivation above. When a new gap is found, list the case there, record it
in a table here (case, derived, executor, root), and when the fix lands remove the entry, run
-update-traces for the case, and review the new golden against its derivation before
committing it.
- Write the smallest model that makes the rule observable through feature values, not only
through trace order — a value the harness can compare is what the
.expected.jsonholds, and what survives a change in tool-defined scheduling. - Derive the constraints and the outcome from the library text before running the executor, and record them here with the sentence relied on. Say which orderings are open.
- Put the derived outcome in the
.expected.jsonwith"trace": true. Run the conformance test. If it passes, run-update-tracesfor the case and check that the golden is one of the admissible linearizations. If it fails, add the case toknown_failures.txtwith a one-line reason and add it to the table above; do not adjust the expectation to the executor. - Cite the case from the compliance row it refers to, leaving the row's status as the executor earns it — a golden that pins an approximation does not make the approximation faithful.