diff --git a/changes/unreleased/composite-state-connection-points.added.md b/changes/unreleased/composite-state-connection-points.added.md new file mode 100644 index 000000000..b7b7ef67e --- /dev/null +++ b/changes/unreleased/composite-state-connection-points.added.md @@ -0,0 +1 @@ +- **The SysML v1 migrator writes a composite state's entry and exit points.** A UML `State.connectionPoint` pseudostate, on a nested composite state or on one with orthogonal regions, is a `junction` of the state, reached by path (`then Work::start;`, `first Work::leave then Idle;`), so the runtime runs the state's entry behavior before the entry point's outgoing transition and the transition into the exit point before the state's exit behavior, the order UML and PSSM give connection points. An entry point whose transitions each start a region of an orthogonal state is a `fork`, an exit point its regions reach from each side a `join`, and an entry point no transition leaves is the state's default entry. An entry point leading straight to an exit point of the same state, a route from a connection point on into a history pseudostate, and the other shapes with no faithful form are refused with the shape named in the report; before, every connection point on a state and every transition through it was unmapped. The OMG PSSM test suite's connection points, which the report refused wholesale, migrate under this rule, and the migrated suite validates with no syntax errors. diff --git a/changes/unreleased/compound-transition-exits-by-segment.fixed.md b/changes/unreleased/compound-transition-exits-by-segment.fixed.md new file mode 100644 index 000000000..a71eaa7ef --- /dev/null +++ b/changes/unreleased/compound-transition-exits-by-segment.fixed.md @@ -0,0 +1 @@ +- **A compound transition through a pseudostate inside a composite state exits and runs its effects segment by segment.** A transition into a junction of a composite state, continued by the junction's outgoing transition out of the state, now exits the source, runs the first effect, exits the composite state, then runs the second effect and enters the target, as UML 2.5.1 §14.2.3.8.4 orders the segments; before, every exit ran before any effect. A join of the state its orthogonal regions leave through runs each region's exit and effect, then the state's exit, then the outgoing transition; a route ending at a terminate action or a history pseudostate keeps its effects in that order. diff --git a/changes/unreleased/empty-transition-effect-braces.fixed.md b/changes/unreleased/empty-transition-effect-braces.fixed.md new file mode 100644 index 000000000..dac69ba1a --- /dev/null +++ b/changes/unreleased/empty-transition-effect-braces.fixed.md @@ -0,0 +1 @@ +- **The SysML v1 migrator writes a transition effect with no body as `do action effect { }`.** An effect activity with no nodes was written `do action effect;`, which ended the transition clause before its `then`, a syntax error in the migrated notation; the braces are now kept so the `then` still belongs to the transition. diff --git a/changes/unreleased/transition-guard-by-reference.fixed.md b/changes/unreleased/transition-guard-by-reference.fixed.md new file mode 100644 index 000000000..ae8c43ae5 --- /dev/null +++ b/changes/unreleased/transition-guard-by-reference.fixed.md @@ -0,0 +1 @@ +- **The SysML v1 migrator reads a transition guard serialized as a reference.** A UML `Transition.guard` some exporters write as a `guard="…"` reference to an owned rule of the transition, rather than as a `guard` child, is now found and written as the `if` clause, reported and kept in a comment when it has no v2 form; before, such a transition was written unguarded and its constraint left out of the report. A `LiteralBoolean` guard whose `value` the file omits is read as `false`, the UML default, where a transition guard is concerned; before, it was taken for `true` and dropped. diff --git a/docs/internals/design/precise-semantics-alignment.md b/docs/internals/design/precise-semantics-alignment.md index 8080c57b1..cb0599a3d 100644 --- a/docs/internals/design/precise-semantics-alignment.md +++ b/docs/internals/design/precise-semantics-alignment.md @@ -1516,7 +1516,7 @@ which supersede the hand count this section was first written with — the moves | Deferrable trigger | `defer Sig;` — this project's extension | extension | | Fork, join, junction, choice, shallow and deep history pseudostates | State-body `fork`/`join`/`junction`/`choice`/`history`/`deep history` — this project's extensions | extension | | Terminate pseudostate | A terminate action usage in the region, `action t terminate;`, that a transition ends at with `then t` (§7.18.3; SM38) | standard | -| Entry point, exit point (connection points and connection point references) | none | no spelling | +| Entry point, exit point (connection points and connection point references) | none in standard notation. The migrator's reading, established since this table was drawn: a composite state's entry or exit point is a `junction` of that state (a `fork`/`join` when its transitions each start, or come from, a different orthogonal region), reached by path — `then Work::start;`, `first Work::leave then Idle;` — so the runtime runs the state's entry behavior before the junction's outgoing transition, and the transition into the junction before the state's exit behavior, the order UML §14.2.3.4.5 and PSSM §8.5 give connection points; see [sysml-v1-migration.md](../../reference/sysml-v1-migration.md#behaviors). The referee's classifier still counts the construct here, so the table below is unchanged until the referee is rewired onto the migrator | no spelling (referee); extension (migrator) | | Local transition, internal transition | none (SM36, SM37) | no spelling | | State machine generalization: extended regions, redefined transitions | none | no spelling | | Entry, exit or do behavior with parameters (reading the triggering event's data) | none written: the notation binds event data on the transition (`accept d : Data`, §7.18.2; `TransitionPerformances.kerml`'s `accepter`), never on an `entry`/`exit`/`do` action, so the payload has to be routed through the transition — the reading is recorded under [Behavior parameters](#behavior-parameters-operation-results-tester-traces-and-standalone-machines) below | no translation | diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 2f619b0e0..2749ced59 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -818,6 +818,7 @@ checked after the result is bound is not a form the runtime offers, and none is | Junction pseudostates: the route through a junction is resolved statically, before the incoming transition fires and before any effect runs; every outgoing guard is read against the data as it then stands, several enabled branches are the transition choice point at the junction (`ChoiceTaken` drawn and recorded only as the transition fires, after the region order among several candidates and the transition's own guard read again, enumerated by `explore`, replayed by a seed; a candidate another region's reaction disarms draws nothing; no branch guard is read again — the route beyond each enabled branch, through any further junction, is settled with the transition — so a branch enabled at selection is taken along it though another region's effect since changed what its guards read; the draw is made as the move begins, so a replay refused at a choice beyond the junction undoes the draw, its note and its `ChoiceTaken` with the move), the unguarded branches are the default when no guard holds, and no enabled branch leaves the compound transition unenabled; a history's default transition through such a junction draws and records the same way | `state_route.go` resolveRoute, followOut, enabledBranches (later guards read in a preview that is undone), junctionDraw, travel, travelResolving, settleDraws, pickBranch; `state_executor.go` fireTransition, defaultHistoryRoute | `state_junction_pseudostate.sysml`, `state_completion_through_pseudostate.sysml`, `state_junction_several_enabled_branches.sysml`, `state_junction_drawn_as_its_transition_fires.sysml`, `state_junction_guards_read_once.sysml`, `state_junction_beyond_a_draw_read_once.sysml`, `state_history_default_through_junction.sysml` (+ trace goldens, `.check.expected.json`), `explore_test.go:TestExploreStaticJunctionBranches`, `:TestExploreJunctionDrawnAsTransitionFires`, `:TestExploreHistoryDefaultThroughJunction`, `replay_test.go:TestReplayRefusedChoiceUndoesTheJunctionDrawBeforeIt`, `:TestReplayRefusedChoiceUndoesTheHistoryDefaultsJunctionDraw`, `robustness_test.go:region_pseudostate_without_satisfied_guard` | ✅ Faithful (static, unlike a choice; which of several enabled branches is taken is open, as at a choice) | | Fork pseudostates: the branches enter one composite state's orthogonal regions, one target per region, at least two, none guarded or triggered (UML 2.5.1 §14.2.3.5 Pseudostates: the transitions outgoing from a fork "must not have a guard or a trigger"), bypassing the initial state of each region they name — a target may lie below a region's own substates, the region being the one it lies in transitively, and the branch enters every state on the way down; a region the fork leaves out starts at its own initial state, and a region without one may be entered by a fork's branch alone — a machine with another way into the composite state that would start such a region by default (a transition to the state itself, to another of its regions or its history, the machine's entry naming it, or another fork — nested in one of its regions, or above it — passing through) is refused when lowered. Entering through a fork exits the source configuration down to the ancestor the source and the composite share — every region of the composite, in declaration order, when the source lies inside them, the composite itself staying active — then the first branch in region order runs its effect and enters the states still on the way down, and every region enters in declaration order, each branch's effect before its target — a region the fork leaves out never brings the composite in ahead of a branch, however early it is declared — the effects reading and writing the attributes of the state declaring the fork, as a transition leaving one of its substates does; branches ending at `done` complete the composite, or the machine, as an ordinary entry does. A fork's static footprint covers the regions it leaves to start by default, so the checker's reduction keeps their entry behaviors' reads, writes and completion dependent on what other units do | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`, `checkForkOnlyRegion`, `defaultEntryInto`, `defaultStart`, `forkStartsByDefault`; `lower/state_footprint.go` `stateFootprintBuilder.pseudostate` (fork case), `entersRegion`; `lower/state_graph.go` `ownTransitionEffects`; `state_executor.go` `fireForkTransition`, `leaveForFork`, `exitRegionsOf`, `completeIfDone`; `state_region_entry.go` `forkEntry`, `enterForkBranches`, `enterRegion`, `enterLazily` | `lower/fork_plan_test.go` (`TestToStateGraph_ForkEntersRegionsWithoutInitial`, `:TestToStateGraph_ForkBranchEffectsOwnedByDeclaringState`, `:TestToStateGraph_ForkKeepsRegionInitialApart`, `:TestToStateGraph_ForkBranchTargetsNestedState`, `:TestToStateGraph_ForkBranchesNestedInOneRegionFail`, `:TestToStateGraph_RegionWithoutInitialOrForkFails`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultFails`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryFails`, `:TestToStateGraph_ForkOnlyRegionOmittedByAnotherForkFails`, `:TestToStateGraph_ForkOnlyRegionEnteredByANestedForkFails`, `:TestToStateGraph_ForkOnlyRegionKeepsExplicitEntries`, `:TestToStateGraph_ForkShapeRejected`), `lower/state_footprint_test.go:TestTransitionFootprintsCoverForkOmittedRegions`, `state_fork_join_pseudostate.sysml` + trace golden, `state_fork_enters_regions_without_initial.sysml`, `state_fork_in_composite_enters_parallel_substate.sysml`, `state_fork_through_inactive_ancestors.sysml`, `state_fork_within_active_ancestor.sysml`, `state_fork_within_active_region.sysml`, `state_fork_completes_owner.sysml`, `state_fork_completes_nested_owner.sysml`, `state_fork_enters_nested_region_states.sysml`, `state_fork_enters_nested_parallel_state.sysml`, `state_fork_omitted_region_declared_first.sysml`, `state_fork_from_within_owner_regions.sysml`, `state_fork_from_within_nested_owner_regions.sysml` (+ trace goldens), `state_fork_branch_effect_owns_state_attribute.sysml`, `fork_join_test.go:TestForkBypassesTargetedRegionInitials`, `robustness_test.go:fork_branches_share_region`, `:fork_leaves_a_region_without_a_way_in`, `:fork_only_region_entered_by_default`, `:fork_branch_with_a_trigger`, `:nested_fork_starts_an_outer_region_by_default` | ✅ Faithful | | Join pseudostates | `state_executor.go:782` fireJoinTransition, `:827` joinSources (declaration order) | `pseudostate_test.go:TestJoinWaitsForEveryBranch`, `fork_join_test.go:TestForkJoinVisitOrderIsDeterministic` | ✅ Faithful | +| A compound transition through a pseudostate declared inside a composite state exits and runs its effects segment by segment (UML 2.5.1 §14.2.3.8.4, the segments' behaviors "executed in sequence", each after the exits of the states its segment leaves; PSSM §8.5 on exit points): a transition `first Inner accept Sig do { A } then Owner::x;` into a junction `x` of `Owner`, continued by `first Owner::x do { B } then Out;`, exits `Inner`, runs `A`, exits `Owner`, runs `B`, then enters `Out` — not every exit first. The segments' effects are grouped by segment (`routeEffect.segment`); a segment leaving from a pseudostate of a state exits that state's descendants the move leaves and the states up to that segment's boundary before its effects, and a segment from the machine's body exits what the move has still to leave. Into a join of `Owner` from its orthogonal regions, each region's transition exits its source and runs its effect (the regions in an open order), then `Owner` exits, then the join's outgoing transition runs. A route that ends at a terminate action, or at a history pseudostate, keeps its effects in that order. This is the runtime form the SysML v1 migrator writes a composite state's exit point in (`docs/reference/sysml-v1-migration.md`, *Pseudostate exitPoint on a composite State*); a transition into a junction of a composite state from outside runs the state's entry behavior before the junction's outgoing transition and the target's entries, which the migrator's entry point form relies on | `state_route.go` travelResolving, leaveAlong, leftBySegment, segmentBoundary, vertexState; `state_executor.go` terminateAlong, terminateAt, moveToHistory (effects passed through) | `state_junction_exit_effect_before_owner_exit.sysml` + `.trace.golden`, `state_join_exit_from_regions_before_owner_exit.sysml` + `.trace.golden` (`admissible` orders), `state_junction_entry_skips_default_initial.sysml` + `.trace.golden` (an entry through a junction runs the state's entry behavior, skips the region's `entry; then`, and history restores the substate); `robustness_junction_exit_route_test.go:TestRuntimeRobustnessJunctionExitRoute` (a junction left through with no outgoing transition, one whose outgoing transition ends at a shallow history, one whose every guard is false: typed errors naming the junction); `tests/migrate/states_test.go:TestCompositeStateConnectionPointsKeepTheUMLOrder` (the migrated `station_points.xmi` run end to end) | ✅ Faithful | | History pseudostates (shallow and deep). A region is recorded per region, in the state it was left in, so a region left by a transition that started inside its own composite state's region is restored to that composite state and its inner configuration rather than to the region's initial state; a region left with no active state at all has nothing to restore; a region or body left at `done` completed and leaves no history. A shallow or deep history with nothing recorded takes its own outgoing transition when it has one and otherwise performs the owning state's default entry, through its `entry` transition, as a plain transition into the composite would; an owner with no entry transition either is the typed `ErrHistoryWithoutEntry` naming the history and its owner | `parser/behavior.go` parseStateMember (`history ;`, `shallow history ;`, `deep history ;`); `state_executor.go` fireHistoryTransition, `:historyEntry`, `:historyRecorded` (an empty history, read by `state_route.go` `resolveRoute` too when settling the default transition), `:hasDefaultEntry`, `:deepestRecorded`, `exitState` (records the configuration left), `:recordChildHistory`, `:recordRegionHistory`, `:forgetRegionHistory`, `errors.go` `ErrHistoryWithoutEntry`, `state_region_transition.go` `leaveRegion`, `exitRegionTo`, `lower/state_graph.go` PseudostateOwner | `tests/parser/testdata/parse/state_history.golden`, `parser/state_notation_test.go:TestHistoryPseudostateParsing`, `lower/state_notation_test.go:TestToStateGraph_HistoryNotation`, `state_shallow_history.sysml`, `state_deep_history.sysml`, `state_history_revisit.sysml` + trace golden, `state_deep_history_region_composite.sysml`, `history_test.go:TestShallowHistoryRestoresLastSubstate`, `:TestDeepHistoryRestoresInnermostSubstate`, `:TestHistoryRestoresOrthogonalRegions`, `:TestDeepHistoryRestoresBelowRegion`, `:TestDeepHistoryRestoresARegionLeftFromInsideItsCompositeState`, `:TestHistoryTakesDefaultTransitionWhenUnvisited`, `:TestHistoryOverACompletedConfigurationIsADefaultEntry`, `state_history_empty_default_entry`, `state_deep_history_empty_default_entry`, `state_history_after_completion_default_entry`, `state_history_after_completion_default_transition` (+ trace goldens), `robustness_test.go:history_outside_composite_state`, `:history_without_record_default_or_entry` | ✅ Faithful | | Composite state with regions entered by a plain transition | `state_executor.go` transitionToInto (keeps the region configuration entering it just built) | `history_test.go:TestHistoryRestoresOrthogonalRegions` | ✅ Faithful | | Leaving a composite state exits only its own regions | `state_executor.go` exitState (scoped to `CompositeStates[state]`) | `history_test.go:TestExitingNestedRegionsKeepsSiblingRegions` | ✅ Faithful | diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 436ce68ad..79bbf2052 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -134,7 +134,7 @@ returned over the service yet. | Comment, Documentation | `doc` (first) / `comment`, HTML tags stripped | mapped | | Custom-profile stereotypes and tags | preserved as `/* applied stereotype «Name»: tag = value */` | mapped | | SysML stereotype tags without a v2 form (`Block.isEncapsulated`, `ValueType.unit`, …) | preserved as `/* «Name» tags with no v2 form: tag = value */` | approximated | -| Two members of one namespace with the same name (UML allows it, v2 does not) | the later one renamed `Name 2`, a state, pseudostate or history a machine's region puts beside its attributes included; a connection end named like a member of its connection def renamed `name2` | approximated | +| Two members of one namespace with the same name (UML allows it, v2 does not) | the later one renamed `Name 2`, a state, pseudostate or history a machine's region puts beside its attributes included, as is a written connection point of a state whichever region a tool listed it in, while one written as no member takes no name; a connection end named like a member of its connection def renamed `name2` | approximated | | Anonymous property with no v2 type | a `ref` named after its type, or `unnamed` | approximated | | Multiplicity bounds that are not natural numbers (a tool's `492x21` array dimensions) | omitted | approximated | | `NaN`/infinite real literals | comment | approximated | @@ -177,11 +177,14 @@ returned over the service yet. | Pseudostate fork, join | `fork x;` / `join x;` — the transitions out of a fork enter the states of several regions of a `parallel` state, those into a join leave them | mapped | | Pseudostate shallowHistory, deepHistory | `history x;` / `deep history x;` in the composite state; a transition targeting it re-enters the substate (the innermost substates) active when the state was last left, the history's own outgoing transition being its default | mapped | | Pseudostate entryPoint, exitPoint on a state machine | a `state` of the submachine's `state def`; a transition into an entry point continues by the entry point's own transition, a transition out of an exit point leaves the submachine state | mapped | +| Pseudostate entryPoint on a composite State (`State.connectionPoint`) | `junction x;` of the state, a transition into it written `then Work::x` by path; the runtime runs the state's entry behavior, then the junction's outgoing transition, then the target's entries, in one run-to-completion step. One whose outgoing transitions each start a different orthogonal region is `fork x;`; one no transition leaves is the state's default entry, and the transition is written to the state | mapped | +| Pseudostate exitPoint on a composite State | `junction x;` of the state, a transition out of it written `first Work::x` by path; the runtime runs the transition into it (its source's exits, its effect), the state's exit behavior, then the outgoing transition. One reached from several orthogonal regions is `join x;`, left through when every region's transition has fired. A connection point a tool lists among a region's vertices belongs to the state all the same; a region listing nothing else is skipped, not written as a region of a `parallel` state | mapped | +| Entry point leading straight to an exit point of the same state, back to the state itself, out of the state, into a history pseudostate or to no target, or whose outgoing transition has a trigger, or several of whose outgoing transitions start the same region; an exit point reached from outside its state, one no transition leaves or whose outgoing transition has a trigger, leads back to the state or into it, into a history pseudostate or to no target, or one several regions reach that is also reached twice from one region, from the state's own local transition, or from a pseudostate; a connection point route into a history pseudostate | refused with the shape named | unmapped | | Pseudostate exitPoint on a region, terminate | a transition into it is written to `done` | approximated | | ConnectionPointReference on a submachine state | the transition is written to `s.` / from `s.`, the submachine's state named by its path | mapped | | Transition between regions or nesting levels (source or target not a sibling) | the transition names the far end by its path, `Work::Run`; a local transition into a substate of its source is written external, so the composite state exits and re-enters | mapped (local into own substate: approximated) | | `entry`, `doActivity`, `exit` behaviors | `entry action { … }` / `do action { … }` / `exit action { … }` inline when the behavior is owned by the state, `entry x;` / `do x : Def;` by reference otherwise | mapped | -| Transition | `transition first s accept Sig if do then t;`; several triggers are several transitions; a completion transition is `transition first s then t;` | mapped (several triggers: approximated) | +| Transition | `transition first s accept Sig if do then t;`; several triggers are several transitions; a completion transition is `transition first s then t;`; the guard is the transition's `guard` child or the owned rule its `guard` reference names, and a `LiteralBoolean` guard whose value the file omits is `false`, the UML default | mapped (several triggers: approximated) | | `entry`, `doActivity`, `exit` behavior or transition `effect` that is an Activity with no nodes | an empty action: `entry action x;` in a state, `do action x { }` on a transition, whose target follows on the next line | mapped (the note says the action is empty) | | `entry`, `doActivity`, `exit` behavior or transition `effect` that is an Activity whose every action node is refused | the action, holding the flow and a comment for each refused node; the behavior runs nothing | approximated (each node: **unmapped**) | | `entry`, `doActivity`, `exit` behavior or transition `effect` that is an OpaqueBehavior in a language the mapping cannot write | the action, holding the body as a comment | approximated | @@ -459,8 +462,30 @@ chains, `fork`/`join` to enter and leave the regions of an orthogonal state, `hi history` to re-enter what was active when the state was last left. An entry or exit point of a state machine is a `state` of its `state def` whose own transition continues into the machine, and a submachine state's connection point references address them by path, -`then Cell::warmStart;` / `first Cell::spent then Idle;`. An internal transition is a self -transition, faithful when re-entering the state is not observable (no entry, exit, do or +`then Cell::warmStart;` / `first Cell::spent then Idle;`. An entry or exit point of a composite +state (UML `State.connectionPoint`) is a `junction` of that state — its transient node, so a +transition in from outside, `then Work::start;`, runs the state's entry behavior, then the +junction's own transition and the target's entries in the same step, and a transition out, +`first Work::leave then Idle;`, runs the inner transition's exits and effect, the state's exit +behavior, then the outgoing effect and the target's entry — the order UML 2.5.1 §14.2.3.4.5 and +PSSM give connection points. An entry point whose transitions each start a region of an +orthogonal state is a `fork`, an exit point its regions reach from each side a `join`; an entry +point no transition leaves is the state's default entry, and the transition is written to the +state. An entry point that leads straight to an exit point of the same state, so the state is +crossed without settling in it, is refused: the runtime would run neither its entry nor its exit +behavior; so is an entry point whose transition leads back to the state itself (v1 enters it by +its default entry where the runtime would leave and re-enter it), out of the state, on into a +history pseudostate or to no target, one whose transition has a trigger (a junction's transition +is followed at once, not on an event), and any point whose transitions do not form one of the +shapes above. An exit point's outgoing transition is held to the same: one with a trigger, to +no target, into a history, back to the state or to a vertex within it refuses the point, as does +an exit point no transition leaves, since the runtime would halt at the junction. A guard on that transition is kept: UML evaluates a junction's guards with the +rest of the compound transition's before it fires, not after entering the state, and the +runtime evaluates the junction's guard when it selects the transition; where UML leaves the +compound transition disabled by a false guard, the runtime reports it, as at any junction. +A connection point a tool lists among a region's vertices rather than as the state's +`connectionPoint` is still the state's, and is named through the state, not the region. An +internal transition is a self transition, faithful when re-entering the state is not observable (no entry, exit, do or substates) and reported otherwise; one written with no target stays in its source, one that targets another vertex or leaves a pseudostate is refused, and one without a trigger is a comment, as a self transition would fire again on every re-entry. A transition into an exit point of a diff --git a/internal/exec/runtime/debug_api_test.go b/internal/exec/runtime/debug_api_test.go index fa1287939..456b6baff 100644 --- a/internal/exec/runtime/debug_api_test.go +++ b/internal/exec/runtime/debug_api_test.go @@ -1484,11 +1484,13 @@ func TestFiredTransitionsLogsSegmentsIntoAChoice(t *testing.T) { // A history entered before its owner has run takes its default transition, and // that route is logged after the transition into the history: straight to the -// default state, or through a choice with each segment on the way. +// default state, through a choice with each segment on the way, or into a +// terminate that ends the machine. func TestFiredTransitionsLogsDefaultHistoryRoute(t *testing.T) { for _, tc := range []struct { name, src string want []string + ended bool }{ {"direct", `package test { state Machine { @@ -1503,7 +1505,7 @@ func TestFiredTransitionsLogsDefaultHistoryRoute(t *testing.T) { } transition first init accept go then previous; } - }`, []string{"->init", "init->previous", "previous->idle"}}, + }`, []string{"->init", "init->previous", "previous->idle"}, false}, {"choice", `package test { state Machine { attribute priority : Integer = 2; @@ -1520,7 +1522,21 @@ func TestFiredTransitionsLogsDefaultHistoryRoute(t *testing.T) { } transition first init accept go then previous; } - }`, []string{"->init", "init->previous", "previous->route", "route->idle"}}, + }`, []string{"->init", "init->previous", "previous->route", "route->idle"}, false}, + {"terminate", `package test { + state Machine { + entry; then init; + state init; + state running { + history previous; + state idle; + action stop terminate; + transition first previous then stop; + transition first idle accept work then stop; + } + transition first init accept go then previous; + } + }`, []string{"->init", "init->previous", "previous->stop"}, true}, } { t.Run(tc.name, func(t *testing.T) { ctx, sym := loadState(t, tc.src, "Machine") @@ -1533,7 +1549,11 @@ func TestFiredTransitionsLogsDefaultHistoryRoute(t *testing.T) { if err := exec.ProcessNextEvent(); err != nil { t.Fatalf("ProcessNextEvent: %v", err) } - if got := activeStateNames(exec); !strings.Contains(got, "idle") { + if tc.ended { + if exec.State() != StateTerminated { + t.Fatalf("state = %v, want the machine ended at the terminate", exec.State()) + } + } else if got := activeStateNames(exec); !strings.Contains(got, "idle") { t.Fatalf("active = %s, want idle", got) } if got := firedNames(exec); !slices.Equal(got, tc.want) { diff --git a/internal/exec/runtime/robustness_junction_exit_route_test.go b/internal/exec/runtime/robustness_junction_exit_route_test.go new file mode 100644 index 000000000..bb7751943 --- /dev/null +++ b/internal/exec/runtime/robustness_junction_exit_route_test.go @@ -0,0 +1,118 @@ +package runtime + +import ( + "strings" + "testing" + "time" +) + +// TestRuntimeRobustnessJunctionExitRoute: a compound transition that leaves a +// composite state through a junction of its own fails with a typed error, never +// a hang or a panic, when the route past the junction cannot be taken. +func TestRuntimeRobustnessJunctionExitRoute(t *testing.T) { + t.Run("junction_of_the_left_state_with_no_outgoing_transition", testJunctionOfTheLeftStateWithNoOutgoingTransition) + t.Run("junction_of_the_left_state_leading_into_history", testJunctionOfTheLeftStateLeadingIntoHistory) + t.Run("junction_of_the_left_state_whose_every_branch_is_closed", testJunctionOfTheLeftStateWhoseEveryBranchIsClosed) +} + +// runRouteToError drives one Go through the machine and returns the error the +// run ends with, failing the test if the run hangs or succeeds. +func runRouteToError(t *testing.T, exec *StateExecutor) error { + t.Helper() + if err := exec.initialize(); err != nil { + t.Fatalf("initialize: %v", err) + } + exec.SendSignal("Go", nil) + done := make(chan error, 1) + go func() { done <- exec.RunToCompletion() }() + select { + case err := <-done: + if err == nil { + t.Fatal("expected the run to fail at the junction the composite state is left through") + } + return err + case <-time.After(10 * time.Second): + t.Fatal("RunToCompletion hung on a junction the composite state is left through") + } + return nil +} + +// testJunctionOfTheLeftStateWithNoOutgoingTransition: the nested source's exit +// leads to a junction of its owner that no transition leaves; the error names it. +func testJunctionOfTheLeftStateWithNoOutgoingTransition(t *testing.T) { + exec := stateExecutorForSource(t, "Machine", `package test { + attribute def Go; + state Machine { + entry; then S1; + state S1 { + entry; then A; + state A; + junction XP; + transition first A accept Go then XP; + } + state S2; + } + }`) + err := runRouteToError(t, exec) + if !strings.Contains(err.Error(), "junction XP has no outgoing transitions") { + t.Errorf("expected the error to name the junction, got %v", err) + } +} + +// testJunctionOfTheLeftStateLeadingIntoHistory: a junction the owner is left +// through cannot lead on into a sibling's history; the refusal names both. +func testJunctionOfTheLeftStateLeadingIntoHistory(t *testing.T) { + exec := stateExecutorForSource(t, "Machine", `package test { + attribute def Go; + state Machine { + entry; then S1; + state S1 { + entry; then A; + state A; + junction XP; + transition first A accept Go then XP; + } + state S2 { + entry; then B; + state B; + history H; + } + transition first S1::XP then S2::H; + } + }`) + err := runRouteToError(t, exec) + if !strings.Contains(err.Error(), "junction XP: a transition into shallow history H is not supported") { + t.Errorf("expected the refusal to name the junction and the history, got %v", err) + } +} + +// testJunctionOfTheLeftStateWhoseEveryBranchIsClosed: a junction's guards are +// read before the compound transition fires; when none holds the run fails at +// the junction before any exit or effect runs. +func testJunctionOfTheLeftStateWhoseEveryBranchIsClosed(t *testing.T) { + exec := stateExecutorForSource(t, "Machine", `package test { + private import ScalarValues::*; + attribute def Go; + state Machine { + attribute log : String = ""; + attribute x : Integer = 0; + entry; then S1; + state S1 { + exit action { assign log := log + "S1(exit);"; } + entry; then A; + state A { exit action { assign log := log + "A(exit);"; } } + junction XP; + transition first A accept Go do assign x := 2 then XP; + } + state S2; + transition first S1::XP if x == 1 then S2; + } + }`) + err := runRouteToError(t, exec) + if !strings.Contains(err.Error(), "junction XP: no guard evaluated to true") { + t.Errorf("expected the error to name the junction, got %v", err) + } + if log := FormatValue(exec.StateData()["log"]); log != `""` { + t.Errorf("log is %s, want empty: nothing is left when no branch out of the junction holds", log) + } +} diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index e6cc9491a..e805ee499 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -2424,13 +2424,13 @@ func (e *StateExecutor) moveToHistory(trans *lower.Transition, currentState *ast if err != nil { return err } + e.noteFired(r.segments...) if r.terminate != nil { - return e.terminateAt(trans, fromName, r, e.descendantChain(below, e.graph.TerminateOwner[r.terminate])) + return e.terminateAt(trans, fromName, r, r.effects(e.graph), e.descendantChain(below, e.graph.TerminateOwner[r.terminate])) } if err := e.runEffects(r.effects(e.graph), e.descendantChain(below, r.target)); err != nil { return err } - e.noteFired(r.segments...) return e.enterBelow(trans, fromName, below, r.target, nil) } diff --git a/internal/exec/runtime/state_route.go b/internal/exec/runtime/state_route.go index 66251168f..899ee567e 100644 --- a/internal/exec/runtime/state_route.go +++ b/internal/exec/runtime/state_route.go @@ -711,11 +711,11 @@ func (e *StateExecutor) travelResolving(trans *lower.Transition, from *ast.State if err != nil { return err } - if err := e.exitAhead(ends.certainExits()); err != nil { + certainExits, certainEntries := ends.certainExits(), ends.certainEntries() + if _, err := e.leaveAlong(r, certainExits, certainEntries, nil); err != nil { return err } - certainEntries := ends.certainEntries() - if err := e.runEffects(r.effects(e.graph), certainEntries); err != nil { + if err := e.exitAhead(certainExits); err != nil { return err } if err := e.enterOwnerOf(r.choice, certainEntries); err != nil { @@ -726,29 +726,116 @@ func (e *StateExecutor) travelResolving(trans *lower.Transition, from *ast.State return err } } + var leaving, entering []*ast.StateNode + if r.terminate != nil { + leaving = e.expandExits(e.terminateExits(from, trans, r.terminate)) + entering = e.terminateEntries(from, trans, r.terminate) + } else { + leaving, entering = e.expandExits(exits(r.target)), enters(r.target) + } + effects, err := e.leaveAlong(r, leaving, entering, r.segments[len(r.segments)-1]) + if err != nil { + return err + } e.noteFired(r.segments...) if r.terminate != nil { - return e.terminateAlong(trans, from, r) + return e.terminateAlong(trans, from, r, effects) + } + return move(effects, r.target) +} + +// leaveAlong runs the segments before upto, exiting what each has left before its effects +// (UML 14.2.3.8.4), and returns the effects from upto on, which run after the move's exits. +func (e *StateExecutor) leaveAlong(r route, leaving, entering []*ast.StateNode, upto *lower.Transition) ([]routeEffect, error) { + effects := r.effects(e.graph) + var left []*ast.StateNode + for _, seg := range r.segments { + if seg == upto { + break + } + left = append(left, e.leftBySegment(leaving, seg)...) + n := 0 + for n < len(effects) && effects[n].segment == seg { + n++ + } + if n == 0 { + continue + } + if err := e.exitAhead(left); err != nil { + return nil, err + } + left = nil + if err := e.runEffects(effects[:n], entering); err != nil { + return nil, err + } + effects = effects[n:] + } + return effects, nil +} + +// leftBySegment lists, among the states the move leaves, those seg leaves: the ones from its +// source up to its boundary and their contents; from the machine's body, all still to exit. +func (e *StateExecutor) leftBySegment(leaving []*ast.StateNode, seg *lower.Transition) []*ast.StateNode { + source := e.vertexState(seg.Source) + if source == nil { + return leaving + } + boundary := e.segmentBoundary(seg) + if boundary != nil && e.isBelowOrEqual(boundary, source) { + return nil + } + top := source + for e.graph.ParentState[top] != boundary && e.graph.ParentState[top] != nil { + top = e.graph.ParentState[top] + } + var left []*ast.StateNode + for _, state := range leaving { + if e.isBelowOrEqual(state, top) { + left = append(left, state) + } } - return move(r.effects(e.graph), r.target) + return left +} + +// segmentBoundary is the state seg stays inside of: the LCA of its ends (a pseudostate standing +// in its declaring state), or the parent of a source that encloses the target; nil is the body. +func (e *StateExecutor) segmentBoundary(seg *lower.Transition) *ast.StateNode { + source, target := e.vertexState(seg.Source), e.vertexState(seg.Target) + if declared, isState := seg.Source.(*ast.StateNode); isState && e.encloses(declared, target) { + return e.graph.ParentState[declared] + } + return e.getLCA(source, target) +} + +// vertexState is the state a transition end lies in; nil for the machine's body. +func (e *StateExecutor) vertexState(end ast.Node) *ast.StateNode { + switch v := end.(type) { + case *ast.StateNode: + return v + case *ast.PseudostateNode: + return e.graph.PseudostateOwner[v] + case *ast.Usage: + return e.graph.TerminateOwner[v] + } + return nil } // terminateAlong finishes a transition at the terminate action its route reaches // (SysML v2 §7.18.3): the states the move leaves are exited and the ones down to // the action's owner entered, as for a state beside it, then the machine ends. -func (e *StateExecutor) terminateAlong(trans *lower.Transition, from *ast.StateNode, r route) error { +func (e *StateExecutor) terminateAlong(trans *lower.Transition, from *ast.StateNode, r route, effects []routeEffect) error { if err := e.exitStates(e.terminateExits(from, trans, r.terminate)); err != nil { return err } - return e.terminateAt(trans, StateVertexName(from), r, e.terminateEntries(from, trans, r.terminate)) + return e.terminateAt(trans, StateVertexName(from), r, effects, e.terminateEntries(from, trans, r.terminate)) } // terminateAt ends the machine's performance at the terminate action r reaches, // the move's exits done: the effects run entering the chain down to the action's // owner, then no further state is exited, no exit behavior runs, and the do // behaviors still under way are abandoned where they stand. -func (e *StateExecutor) terminateAt(trans *lower.Transition, fromName string, r route, entering []*ast.StateNode) error { - if err := e.runEffects(r.effects(e.graph), entering); err != nil { +func (e *StateExecutor) terminateAt(trans *lower.Transition, fromName string, r route, effects []routeEffect, entering []*ast.StateNode) error { + if err := e.runEffects(effects, entering); err != nil { return err } if err := e.enterAhead(entering); err != nil { diff --git a/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.check.expected.json b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.check.expected.json new file mode 100644 index 000000000..77cc55ad1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.check.expected.json @@ -0,0 +1,12 @@ +{ + "verdict": "divergent", + "divergent": { + "log": [ + "\"A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);S2(entry);\"", + "\"B1(exit);T22(effect);A1(exit);T12(effect);S1(exit);T3(effect);S2(entry);\"" + ] + }, + "agreed": { + "finalState": "S2" + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.declared.trace.golden new file mode 100644 index 000000000..b8846c8d7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.declared.trace.golden @@ -0,0 +1,38 @@ +choice join XP: states A1, B1 react (unordered; took A1 first) +exit: A1 (exit action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A1(exit);" -> "A1(exit);" + eval operator + -> "A1(exit);" +stmt assign log + eval feature log -> "A1(exit);" + eval literal "T12(effect);" -> "T12(effect);" + eval operator + -> "A1(exit);T12(effect);" +exit: B1 (exit action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);" + eval literal "B1(exit);" -> "B1(exit);" + eval operator + -> "A1(exit);T12(effect);B1(exit);" +stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);" + eval literal "T22(effect);" -> "T22(effect);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);" +exit: S1 (exit action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);" + eval literal "S1(exit);" -> "S1(exit);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);" +stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);" + eval literal "T3(effect);" -> "T3(effect);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);" +enter: S2 (entry action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);" + eval literal "S2(entry);" -> "S2(entry);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);S2(entry);" +transition: S1 -> S2 (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.expected.json b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.expected.json new file mode 100644 index 000000000..1f949cd8e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.expected.json @@ -0,0 +1,20 @@ +{ + "type": "state", + "trace": true, + "events": [{"signal": "Go"}], + "outcomes": [ + { + "finalState": "S2", + "outputs": { + "log": {"type": "String", "value": "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);S2(entry);"} + } + }, + { + "finalState": "S2", + "outputs": { + "log": {"type": "String", "value": "B1(exit);T22(effect);A1(exit);T12(effect);S1(exit);T3(effect);S2(entry);"} + } + } + ], + "admissible": "Transitions into a join: each exits its source and runs its effect before the owner is left, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.seed-1.trace.golden new file mode 100644 index 000000000..b8846c8d7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.seed-1.trace.golden @@ -0,0 +1,38 @@ +choice join XP: states A1, B1 react (unordered; took A1 first) +exit: A1 (exit action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A1(exit);" -> "A1(exit);" + eval operator + -> "A1(exit);" +stmt assign log + eval feature log -> "A1(exit);" + eval literal "T12(effect);" -> "T12(effect);" + eval operator + -> "A1(exit);T12(effect);" +exit: B1 (exit action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);" + eval literal "B1(exit);" -> "B1(exit);" + eval operator + -> "A1(exit);T12(effect);B1(exit);" +stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);" + eval literal "T22(effect);" -> "T22(effect);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);" +exit: S1 (exit action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);" + eval literal "S1(exit);" -> "S1(exit);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);" +stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);" + eval literal "T3(effect);" -> "T3(effect);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);" +enter: S2 (entry action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);" + eval literal "S2(entry);" -> "S2(entry);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);S2(entry);" +transition: S1 -> S2 (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.sysml b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.sysml new file mode 100644 index 000000000..8596309f7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.sysml @@ -0,0 +1,32 @@ +// A join inside a composite state stands for an exit point reached from each +// orthogonal region: each incoming segment exits its region and runs its +// effect, then the owner's exit action runs, then the outgoing segment fires. +package Test { + private import ScalarValues::*; + + attribute def Go; + + state Machine { + attribute log : String = ""; + + entry; then S1; + state S1 parallel { + exit action { assign log := log + "S1(exit);"; } + state R1 { + entry; then A1; + state A1 { exit action { assign log := log + "A1(exit);"; } } + transition first A1 accept Go do assign log := log + "T12(effect);" then XP; + } + state R2 { + entry; then B1; + state B1 { exit action { assign log := log + "B1(exit);"; } } + transition first B1 accept Go do assign log := log + "T22(effect);" then XP; + } + join XP; + } + transition first S1::XP do assign log := log + "T3(effect);" then S2; + state S2 { + entry action { assign log := log + "S2(entry);"; } + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.trace.golden b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.trace.golden new file mode 100644 index 000000000..b8846c8d7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_join_exit_from_regions_before_owner_exit.trace.golden @@ -0,0 +1,38 @@ +choice join XP: states A1, B1 react (unordered; took A1 first) +exit: A1 (exit action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A1(exit);" -> "A1(exit);" + eval operator + -> "A1(exit);" +stmt assign log + eval feature log -> "A1(exit);" + eval literal "T12(effect);" -> "T12(effect);" + eval operator + -> "A1(exit);T12(effect);" +exit: B1 (exit action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);" + eval literal "B1(exit);" -> "B1(exit);" + eval operator + -> "A1(exit);T12(effect);B1(exit);" +stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);" + eval literal "T22(effect);" -> "T22(effect);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);" +exit: S1 (exit action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);" + eval literal "S1(exit);" -> "S1(exit);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);" +stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);" + eval literal "T3(effect);" -> "T3(effect);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);" +enter: S2 (entry action) +stmt action body + stmt assign log + eval feature log -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);" + eval literal "S2(entry);" -> "S2(entry);" + eval operator + -> "A1(exit);T12(effect);B1(exit);T22(effect);S1(exit);T3(effect);S2(entry);" +transition: S1 -> S2 (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.expected.json b/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.expected.json new file mode 100644 index 000000000..bd291ba8a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.expected.json @@ -0,0 +1,9 @@ +{ + "type": "state", + "trace": true, + "events": [{"signal": "Go"}, {"signal": "Back"}, {"signal": "Again"}], + "finalState": "A2", + "outputs": { + "log": {"type": "String", "value": "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);T3(effect);S1(entry);A2(entry);"} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.sysml b/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.sysml new file mode 100644 index 000000000..50a611bd2 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.sysml @@ -0,0 +1,32 @@ +// A junction inside a composite state stands for an entry point: entering +// through it runs the owner's entry action, then the junction's outgoing +// effect, then the target's entry, and the owner's default initial does not +// fire. A later entry through history restores the state the junction reached. +package Test { + private import ScalarValues::*; + + attribute def Go; attribute def Back; attribute def Again; + + state Machine { + attribute log : String = ""; + + entry; then S0; + state S0; + state S1 { + entry action { assign log := log + "S1(entry);"; } + exit action { assign log := log + "S1(exit);"; } + then A1; + junction EP; + history H; + state A1 { entry action { assign log := log + "A1(entry);"; } } + state A2 { + entry action { assign log := log + "A2(entry);"; } + exit action { assign log := log + "A2(exit);"; } + } + transition first EP do assign log := log + "EP(effect);" then A2; + } + transition first S0 accept Go do assign log := log + "T1(effect);" then S1::EP; + transition first S1 accept Back do assign log := log + "T2(effect);" then S0; + transition first S0 accept Again do assign log := log + "T3(effect);" then S1::H; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.trace.golden b/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.trace.golden new file mode 100644 index 000000000..5f95e3187 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_junction_entry_skips_default_initial.trace.golden @@ -0,0 +1,58 @@ +exit: S0 +stmt assign log + eval feature log -> "" + eval literal "T1(effect);" -> "T1(effect);" + eval operator + -> "T1(effect);" +enter: S1 (entry action) +stmt action body + stmt assign log + eval feature log -> "T1(effect);" + eval literal "S1(entry);" -> "S1(entry);" + eval operator + -> "T1(effect);S1(entry);" +stmt assign log + eval feature log -> "T1(effect);S1(entry);" + eval literal "EP(effect);" -> "EP(effect);" + eval operator + -> "T1(effect);S1(entry);EP(effect);" +enter: A2 (entry action) +stmt action body + stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);" + eval literal "A2(entry);" -> "A2(entry);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);" +transition: S0 -> A2 (event: accept Go) +exit: A2 (exit action) +stmt action body + stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);A2(entry);" + eval literal "A2(exit);" -> "A2(exit);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);" +exit: S1 (exit action) +stmt action body + stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);" + eval literal "S1(exit);" -> "S1(exit);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);" +stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);" + eval literal "T2(effect);" -> "T2(effect);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);" +enter: S0 +transition: A2 -> S0 (event: accept Back) +exit: S0 +stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);" + eval literal "T3(effect);" -> "T3(effect);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);T3(effect);" +enter: S1 (entry action) +stmt action body + stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);T3(effect);" + eval literal "S1(entry);" -> "S1(entry);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);T3(effect);S1(entry);" +enter: A2 (entry action) +stmt action body + stmt assign log + eval feature log -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);T3(effect);S1(entry);" + eval literal "A2(entry);" -> "A2(entry);" + eval operator + -> "T1(effect);S1(entry);EP(effect);A2(entry);A2(exit);S1(exit);T2(effect);T3(effect);S1(entry);A2(entry);" +transition: S0 -> A2 (event: accept Again) diff --git a/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.expected.json b/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.expected.json new file mode 100644 index 000000000..272ab7cbb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.expected.json @@ -0,0 +1,9 @@ +{ + "type": "state", + "trace": true, + "events": [{"signal": "Go"}], + "finalState": "S2", + "outputs": { + "log": {"type": "String", "value": "S111(exit);S11(exit);T12(effect);S1(exit);T3(effect);S2(entry);"} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.sysml b/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.sysml new file mode 100644 index 000000000..dc4c1ef76 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.sysml @@ -0,0 +1,31 @@ +// A junction inside a composite state stands for an exit point: the transition +// into it from a nested state runs its effect after the nested exits and +// before the owner's exit action, then the outgoing segment's effect runs. +package Test { + private import ScalarValues::*; + + attribute def Go; + + state Machine { + attribute log : String = ""; + + entry; then S1; + state S1 { + exit action { assign log := log + "S1(exit);"; } + entry; then S11; + state S11 { + exit action { assign log := log + "S11(exit);"; } + entry; then S111; + state S111 { + exit action { assign log := log + "S111(exit);"; } + } + } + junction XP; + transition first S111 accept Go do assign log := log + "T12(effect);" then XP; + } + transition first S1::XP do assign log := log + "T3(effect);" then S2; + state S2 { + entry action { assign log := log + "S2(entry);"; } + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.trace.golden b/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.trace.golden new file mode 100644 index 000000000..6cbaca125 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_junction_exit_effect_before_owner_exit.trace.golden @@ -0,0 +1,33 @@ +exit: S111 (exit action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "S111(exit);" -> "S111(exit);" + eval operator + -> "S111(exit);" +exit: S11 (exit action) +stmt action body + stmt assign log + eval feature log -> "S111(exit);" + eval literal "S11(exit);" -> "S11(exit);" + eval operator + -> "S111(exit);S11(exit);" +stmt assign log + eval feature log -> "S111(exit);S11(exit);" + eval literal "T12(effect);" -> "T12(effect);" + eval operator + -> "S111(exit);S11(exit);T12(effect);" +exit: S1 (exit action) +stmt action body + stmt assign log + eval feature log -> "S111(exit);S11(exit);T12(effect);" + eval literal "S1(exit);" -> "S1(exit);" + eval operator + -> "S111(exit);S11(exit);T12(effect);S1(exit);" +stmt assign log + eval feature log -> "S111(exit);S11(exit);T12(effect);S1(exit);" + eval literal "T3(effect);" -> "T3(effect);" + eval operator + -> "S111(exit);S11(exit);T12(effect);S1(exit);T3(effect);" +enter: S2 (entry action) +stmt action body + stmt assign log + eval feature log -> "S111(exit);S11(exit);T12(effect);S1(exit);T3(effect);" + eval literal "S2(entry);" -> "S2(entry);" + eval operator + -> "S111(exit);S11(exit);T12(effect);S1(exit);T3(effect);S2(entry);" +transition: S111 -> S2 (event: accept Go) diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index c1c814824..5d4e1442e 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -81,6 +81,9 @@ func FromModel(name string, model *sysmlv1.Model) *Result { indexed: map[string]int{}, regionUsed: map[*sysmlv1.Element]map[string]bool{}, vertexNames: map[*sysmlv1.Element]string{}, + points: map[*sysmlv1.Element]pointForm{}, + incoming: map[*sysmlv1.Element][]*sysmlv1.Element{}, + outgoing: map[*sysmlv1.Element][]*sysmlv1.Element{}, instant: map[*sysmlv1.Element]map[*sysmlv1.Element]instantValue{}, self: "this", lanes: map[*sysmlv1.Element]*lanes{}, @@ -245,6 +248,11 @@ type migration struct { regionUsed map[*sysmlv1.Element]map[string]bool // vertexNames gives the v2 name of every vertex a state machine writes. vertexNames map[*sysmlv1.Element]string + // points says how each connection point of a composite state is written. + points map[*sysmlv1.Element]pointForm + // incoming and outgoing list the transitions into and out of each vertex + // of the machines named so far. + incoming, outgoing map[*sysmlv1.Element][]*sysmlv1.Element // instant names, per state machine, the TimeInstantValue attribute each // absolute time event its transitions accept is written as. instant map[*sysmlv1.Element]map[*sysmlv1.Element]instantValue @@ -478,32 +486,43 @@ func (m *migration) distinguish(e *sysmlv1.Element) { seen[c.Name] = true continue } - name := c.Name - for i := 2; seen[name] || m.nameTaken(e, name); i++ { - name = fmt.Sprintf("%s %d", c.Name, i) - } - seen[name] = true - m.names[c] = name + m.names[c] = distinct(seen, func(n string) bool { return m.nameTaken(e, n) }, c.Name) } } +// distinct gives a member named name, which a sibling in seen already bears, the +// first `name 2`, `name 3`, … neither seen nor taken, and marks it seen. +func distinct(seen map[string]bool, taken func(string) bool, name string) string { + fresh := name + for i := 2; seen[fresh] || taken(fresh); i++ { + fresh = fmt.Sprintf("%s %d", name, i) + } + seen[fresh] = true + return fresh +} + // namespaceMembers lists the children of e written as members of its v2 body: its -// own, and the named vertices of its one region, which v2 puts beside them. +// own, and the named vertices of its one written region, which v2 puts beside them. +// A state's connection points are left to namePoints, which knows which are written. func namespaceMembers(e *sysmlv1.Element) []*sysmlv1.Element { var members []*sysmlv1.Element - inline := len(e.Owned("region")) == 1 + var inline *sysmlv1.Element + if written := writtenRegions(e); len(written) == 1 { + inline = written[0] + } for _, c := range e.Children { switch { case c.Role == "region": - if !inline { + if c != inline { continue } for _, v := range c.Owned("subvertex") { - if vertexBase(v) != "" { + if vertexBase(v) != "" && memberOwner(v) != e { members = append(members, v) } } members = append(members, c.Owned("transition")...) + case c.Role == "connectionPoint" && e.Type == "State": case !ownerWritten(c.Role): members = append(members, c) } diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index f0a9162ff..aee23cb2b 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -83,9 +83,10 @@ func lowerFirst(s string) string { // segments returns the v2 qualified-name segments of an element: the names // from the top-level declaration down, the root Model not being written. A // lone region is its owner's body; one of several is a sub-state of a parallel state. +// A connection point is a member of its owner, whichever region a tool listed it in. func (m *migration) segments(e *sysmlv1.Element) []string { var segs []string - for cur := e; cur != nil; cur = cur.Parent { + for cur := e; cur != nil; cur = memberOwner(cur) { if cur.Parent == nil && cur.Type == "Model" { break } diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index db9101de7..7ca464d8b 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -52,6 +52,7 @@ func (m *migration) nameMachine(sm *sysmlv1.Element) map[string]bool { } used := inheritedStateNamesSet() m.regionUsed[sm] = used + m.indexTransitions(sm) for _, cp := range sm.Owned("connectionPoint") { m.nameVertex(cp, sm, used) } @@ -59,6 +60,29 @@ func (m *migration) nameMachine(sm *sysmlv1.Element) map[string]bool { return used } +// indexTransitions lists the transitions into and out of every vertex of a machine, so a +// connection point's shape can be read before it is written; a machine nested in it indexes its own. +func (m *migration) indexTransitions(sm *sysmlv1.Element) { + var walk func(e *sysmlv1.Element) + walk = func(e *sysmlv1.Element) { + for _, c := range e.Children { + if c.Type == "StateMachine" { + continue + } + if c.Role == "transition" { + if src := m.model.Ref(c, "source"); src != nil { + m.outgoing[src] = append(m.outgoing[src], c) + } + if tgt := m.model.Ref(c, "target"); tgt != nil { + m.incoming[tgt] = append(m.incoming[tgt], c) + } + } + walk(c) + } + } + walk(sm) +} + // nameRegions names the vertices of the regions of a state machine or state: // one region shares its owner's body, several become the sub-states of one // parallel state, each with a body of its own; owner is the element whose body @@ -86,24 +110,328 @@ func (m *migration) nameRegions(regions []*sysmlv1.Element, owner *sysmlv1.Eleme func (m *migration) nameRegion(r, owner *sysmlv1.Element, used map[string]bool) { m.regionUsed[r] = used for _, v := range r.Owned("subvertex") { - m.nameVertex(v, owner, used) if v.Type == "State" { - m.nameRegions(m.populatedRegions(v), v, inheritedStateNamesSet()) + m.nameVertex(v, owner, used) + inner := inheritedStateNamesSet() + m.namePoints(v, inner) + m.nameRegions(m.populatedRegions(v), v, inner) + for _, pr := range pointRegions(v) { + m.regionUsed[pr] = inner + } + continue } + if pointOwner(v) != nil && pointOwner(v).Type == "State" { + // A tool that lists a state's connection point among its region's vertices. + continue + } + m.nameVertex(v, owner, used) } } -// populatedRegions returns the regions of a machine or state that hold a -// vertex; an empty one has nothing to enter, so it is skipped rather than -// written as a sub-state no entry starts. -func (m *migration) populatedRegions(owner *sysmlv1.Element) []*sysmlv1.Element { +// pointRegions lists the regions of state v holding nothing but connection points +// of v, which a tool listed there: their transitions are written in v's body. +func pointRegions(v *sysmlv1.Element) []*sysmlv1.Element { var out []*sysmlv1.Element + for _, r := range v.Owned("region") { + if len(r.Owned("subvertex")) > 0 && pointsOnly(r, v) { + out = append(out, r) + } + } + return out +} + +// pointsOnly reports whether every vertex region r lists is a connection point of +// the state owner, written in the state's body rather than the region's. +func pointsOnly(r, owner *sysmlv1.Element) bool { + if owner.Type != "State" { + return false + } + for _, v := range r.Owned("subvertex") { + if pointOwner(v) != owner { + return false + } + } + return true +} + +// namePoints settles how each connection point of composite state v is written +// and names those written as members of its body, whose names used lists. +func (m *migration) namePoints(v *sysmlv1.Element, used map[string]bool) { + // seen holds the names of the body's other members; a written point that bears one + // yields to it, taking a name no member and no synthesized name of the body has. + seen := map[string]bool{} + for _, c := range namespaceMembers(v) { + if name := m.nameOf(c); name != "" { + seen[name] = true + } + } + synthesized := func(n string) bool { return m.taken[v][n] } + for _, cp := range m.connectionPoints(v) { + f := m.statePointForm(cp, v) + m.points[cp] = f + if f.kw == "" { + continue + } + if name := m.nameOf(cp); name != "" && seen[name] { + m.names[cp] = distinct(seen, synthesized, name) + } else if name != "" { + seen[name] = true + } + m.nameVertex(cp, v, used) + } +} + +// connectionPoints lists the entry and exit points of composite state v: those +// it owns, and those a tool listed among the vertices of its regions. +func (m *migration) connectionPoints(v *sysmlv1.Element) []*sysmlv1.Element { + points := v.Owned("connectionPoint") + for _, r := range v.Owned("region") { + for _, sv := range r.Owned("subvertex") { + if k := pseudoKind(sv); k == "entryPoint" || k == "exitPoint" { + points = append(points, sv) + } + } + } + return points +} + +// pointOwner is the state or state machine an entry or exit point belongs to, +// through the region a tool may have listed it in; nil for another vertex. +func pointOwner(v *sysmlv1.Element) *sysmlv1.Element { + if k := pseudoKind(v); k != "entryPoint" && k != "exitPoint" { + return nil + } + owner := v.Parent + if owner != nil && owner.Type == "Region" { + owner = owner.Parent + } + return owner +} + +// memberOwner is the element whose body vertex v is written in: the state whose +// connection point it is, however a tool listed it, else v's parent. +func memberOwner(v *sysmlv1.Element) *sysmlv1.Element { + if owner := pointOwner(v); owner != nil && owner.Type == "State" { + return owner + } + return v.Parent +} + +// pointForm says how a composite state's entry or exit point is written: as +// the pseudostate kw of the state's body, or not at all. +type pointForm struct { + // kw is junction, fork or join; "" when the point is written as no member. + kw string + // note is what the report says of a point that is written. + note string + // defaultEntry marks an entry point no transition leaves: a transition to it + // enters its state by the state's default entry, so it is written to the state. + defaultEntry bool + // why says why a point has no v2 form; "" when it has one. + why string +} + +// statePointForm settles how connection point v of composite state owner is written from the +// transitions through it: a junction, a fork starting several regions, or a join they leave by. +func (m *migration) statePointForm(v, owner *sysmlv1.Element) pointForm { + if pseudoKind(v) == "entryPoint" { + return m.entryPointForm(v, owner) + } + return m.exitPointForm(v, owner) +} + +func (m *migration) entryPointForm(v, owner *sysmlv1.Element) pointForm { + out := m.outgoing[v] + if len(out) == 0 { + note := "no transition leaves the entry point, so entering through it enters " + describe(owner) + " by its default entry; a transition to it is written to the state" + if without := regionsWithoutInitial(m.populatedRegions(owner)); len(without) > 0 { + note += "; no initial pseudostate starts the " + pluralRegion(len(without)) + " " + strings.Join(without, ", ") + ", which v1 too leaves inactive on entering the state" + } + return pointForm{defaultEntry: true, note: note} + } + for _, t := range out { + if why := m.entryBranchWhy(t, owner); why != "" { + return pointForm{why: describe(t) + " leads from the entry point " + why} + } + } + regions := m.regionsCrossed(out, owner, "target") + if len(out) < 2 || len(regions) < 2 { + return pointForm{kw: "junction", note: "written as a junction of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction"} + } + if len(regions) < len(out) { + return pointForm{why: "the entry point starts several regions of " + describe(owner) + ", as a fork does, but two of its outgoing transitions enter the same region"} + } + for _, t := range out { + if why := m.forkBranchWhy(t, m.model.Ref(t, "target")); why != "" { + return pointForm{why: "the entry point starts several regions of " + describe(owner) + ", as a fork does, but " + describe(t) + " " + why} + } + } + return pointForm{kw: "fork", note: "written as a fork of its state, whose branches start its regions; a transition entering through it runs the state's entry behavior, then the branches"} +} + +// entryBranchWhy says why transition t, leaving an entry point of owner, keeps the point from being +// written: it has a trigger, or its target is missing, owner itself or outside it, a history, or an exit point of owner. +func (m *migration) entryBranchWhy(t, owner *sysmlv1.Element) string { + tgt := m.model.Ref(t, "target") + switch { + case len(t.Owned("trigger")) > 0: + return "with a trigger, which no transition out of a pseudostate takes; the runtime would follow it without waiting for the event" + case tgt == nil: + return "to no target" + case tgt == owner: + return "back to the state itself, which v1 enters by its default entry while the runtime would leave and re-enter it" + case pseudoKind(tgt) == "exitPoint" && pointOwner(tgt) == owner: + return "straight to the exit point " + describe(tgt) + " of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior" + case pseudoKind(tgt) == "shallowHistory" || pseudoKind(tgt) == "deepHistory": + return "on into the history pseudostate " + describe(tgt) + ", which the runtime does not follow from a junction" + case regionWithin(tgt, owner) == nil: + return "out of the state, to " + describe(tgt) + } + return "" +} + +func (m *migration) exitPointForm(v, owner *sysmlv1.Element) pointForm { + out := m.outgoing[v] + if len(out) == 0 { + return pointForm{why: "no transition leaves the exit point, so a transition into it leaves " + describe(owner) + " for nowhere; the runtime would halt at the junction"} + } + for _, t := range out { + if why := m.exitBranchWhy(t, owner); why != "" { + return pointForm{why: describe(t) + " leads from the exit point " + why} + } + } + // in holds the transitions from within the state that shape the point; one from the + // state itself is its local transition, one from its entry point is refused with that. + var in, local []*sysmlv1.Element + for _, t := range m.incoming[v] { + src := m.model.Ref(t, "source") + switch { + case src == nil: + return pointForm{why: describe(t) + " leads to the exit point from no source"} + case src == owner: + local = append(local, t) + case pseudoKind(src) == "entryPoint" && pointOwner(src) == owner: + case regionWithin(src, owner) == nil: + return pointForm{why: describe(t) + " leads to the exit point from outside the state, from " + describe(src) + "; v1 never enters the state, while the runtime would enter and leave it, running its entry and exit behaviors"} + default: + in = append(in, t) + } + } + regions := m.regionsCrossed(in, owner, "source") + if len(in) < 2 || len(regions) < 2 { + return pointForm{kw: "junction", note: "written as a junction of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it"} + } + joinBut := "several regions of " + describe(owner) + " leave through the exit point, as through a join, but " + if len(regions) < len(in) { + return pointForm{why: joinBut + "two of its incoming transitions leave the same region"} + } + if len(local) > 0 { + return pointForm{why: joinBut + describe(local[0]) + " leaves the state itself, which a join does not wait for"} + } + for _, t := range in { + if src := m.model.Ref(t, "source"); src.Type != "State" { + return pointForm{why: joinBut + describe(t) + " leaves " + describe(src) + ", a " + kindOf(src) + " rather than a state"} + } + } + return pointForm{kw: "join", note: "written as a join of its state, which its regions leave through together; the transitions into the join run, then the state's exit behavior, then the transition leaving it"} +} + +// exitBranchWhy says why transition t, leaving an exit point of owner, keeps the point from +// being written: it has a trigger, or its target is missing, a history, or owner or a vertex within it. +func (m *migration) exitBranchWhy(t, owner *sysmlv1.Element) string { + tgt := m.model.Ref(t, "target") + switch { + case len(t.Owned("trigger")) > 0: + return "with a trigger, which no transition out of a pseudostate takes; the runtime would follow it without waiting for the event" + case tgt == nil: + return "to no target" + case tgt == owner: + return "back to the state itself, which v1 leaves and re-enters by its default entry while the runtime, moving from a member of the state to the state, would stay in it, running neither its exit nor its entry" + case pseudoKind(tgt) == "shallowHistory" || pseudoKind(tgt) == "deepHistory": + return "on into the history pseudostate " + describe(tgt) + ", which the runtime does not follow from a junction" + case regionWithin(tgt, owner) != nil || pointOwner(tgt) == owner: + return "back into the state, to " + describe(tgt) + ", which v1 leaves and re-enters while the runtime, moving between members of the state, would stay in it" + } + return "" +} + +// regionsCrossed lists the distinct regions of owner the given ends of the +// transitions lie in; an end outside owner counts as none. +func (m *migration) regionsCrossed(transitions []*sysmlv1.Element, owner *sysmlv1.Element, role string) []*sysmlv1.Element { + var regions []*sysmlv1.Element + for _, t := range transitions { + if r := regionWithin(m.model.Ref(t, role), owner); r != nil && !slices.Contains(regions, r) { + regions = append(regions, r) + } + } + return regions +} + +// regionWithin is the region of owner that holds vertex v, however deep; nil +// when v lies outside owner. +func regionWithin(v, owner *sysmlv1.Element) *sysmlv1.Element { + for cur := v; cur != nil && cur.Parent != nil; cur = cur.Parent { + if cur.Parent == owner && cur.Type == "Region" { + return cur + } + } + return nil +} + +// forkBranchWhy says why transition t into tgt, a route entryBranchWhy passed, cannot be a +// fork's branch, which enters a state with no guard of its own; "" when it can. +func (m *migration) forkBranchWhy(t, tgt *sysmlv1.Element) string { + g := m.guardOf(t) + switch { + case tgt.Type != "State": + return "enters " + describe(tgt) + ", a " + kindOf(tgt) + " rather than a state" + case g != nil && !trueLiteral(firstOwned(g, "specification")): + return "has a guard" + } + return "" +} + +// guardOf returns transition t's guard: its guard child, or the owned rule its guard +// reference names, which is how some exporters serialize the composite property. +func (m *migration) guardOf(t *sysmlv1.Element) *sysmlv1.Element { + if g := firstOwned(t, "guard"); g != nil { + return g + } + if g := m.model.Ref(t, "guard"); g != nil && g.Type == "Constraint" { + return g + } + return nil +} + +// trueLiteral reports whether spec is a LiteralBoolean holding true; a value the +// serialization omits is the UML default, false. +func trueLiteral(spec *sysmlv1.Element) bool { + return spec != nil && spec.Type == "LiteralBoolean" && (spec.Attrs["value"] == "true" || spec.Attrs["value"] == "1") +} + +// populatedRegions returns writtenRegions and reports the regions it leaves out: an +// empty one, or one listing only its state's connection points, has nothing to +// enter, so it is skipped rather than written as a sub-state no entry starts. +func (m *migration) populatedRegions(owner *sysmlv1.Element) []*sysmlv1.Element { for _, r := range owner.Owned("region") { - if len(r.Owned("subvertex")) == 0 { + switch { + case len(r.Owned("subvertex")) == 0: m.add(r, Skipped, "", unreferencedNote+": the region holds no vertex, so nothing enters it and no state is written for it") - continue + case pointsOnly(r, owner): + m.add(r, Skipped, "", unreferencedNote+": the region lists only connection points of its state, which are written in the state's body, so nothing enters it and no state is written for it") + } + } + return writtenRegions(owner) +} + +// writtenRegions lists the regions of a machine or state a body is written for: +// those holding a vertex of their own. +func writtenRegions(owner *sysmlv1.Element) []*sysmlv1.Element { + var out []*sysmlv1.Element + for _, r := range owner.Owned("region") { + if len(r.Owned("subvertex")) > 0 && !pointsOnly(r, owner) { + out = append(out, r) } - out = append(out, r) } return out } @@ -355,7 +683,7 @@ func (s *stateRegion) initial(vertices, transitions []*sysmlv1.Element, entered note = "an initial transition takes no trigger; its triggers are dropped" s.m.add(tr, Unmapped, "", "a trigger of an initial transition, which takes none, is dropped") } - if g := firstOwned(t, "guard"); g != nil { + if g := s.m.guardOf(t); g != nil { text := "the guard " + describe(g) if spec := firstOwned(g, "specification"); spec != nil { text = "[" + describeValue(spec) + "]" @@ -407,6 +735,10 @@ func (s *stateRegion) vertex(v *sysmlv1.Element) { case "terminate": s.m.add(v, Approximated, "done", "a terminate pseudostate ends the machine; a transition to it is written to done, which ends its region") case "entryPoint", "exitPoint": + if pointOwner(v).Type == "State" { + // Written in the body of the state it belongs to. + return + } s.m.connectionPoint(v) default: s.m.unmapped(v, "no v2 form for a "+pseudoKind(v)+" pseudostate") @@ -418,12 +750,12 @@ func (s *stateRegion) vertex(v *sysmlv1.Element) { } } -// connectionPoint writes an entry or exit point as a state of the state def: entered at the entry -// point's state, and leaving through the exit point's state completes the submachine state's transition. +// connectionPoint writes an entry or exit point of a state machine as a state of the state def: entered +// at the entry point's state, and leaving through the exit point's state completes the submachine state's transition. func (m *migration) connectionPoint(v *sysmlv1.Element) { name, ok := m.vertexNames[v] if !ok { - m.unmapped(v, "a "+pseudoKind(v)+" owned by a "+v.Parent.Type+" is not written; only a state machine's connection points are") + m.unmapped(v, "a "+pseudoKind(v)+" owned by a "+v.Parent.Type+" is not written; only a state machine's or a state's connection points are") return } m.w.line(stateKw + writeName(name) + ";") @@ -434,6 +766,38 @@ func (m *migration) connectionPoint(v *sysmlv1.Element) { m.add(v, Mapped, name, "written as a state; a transition entering a submachine state through the entry point enters this state") } +// statePoints writes the connection points of composite state v in its body, in +// the form namePoints settled on; it reports how many members were written. +func (m *migration) statePoints(v *sysmlv1.Element) int { + written := 0 + for _, cp := range m.connectionPoints(v) { + f := m.points[cp] + switch { + case f.why != "": + m.unmapped(cp, f.why) + case f.defaultEntry: + m.add(cp, Mapped, m.vertexNames[v], f.note) + default: + name := writeName(m.vertexNames[cp]) + m.w.line(f.kw + " " + name + ";") + m.add(cp, Mapped, name, f.note) + written++ + } + } + return written +} + +// writtenPoints counts the connection points of v written as members of its body. +func (m *migration) writtenPoints(v *sysmlv1.Element) int { + n := 0 + for _, cp := range m.connectionPoints(v) { + if m.points[cp].kw != "" { + n++ + } + } + return n +} + // state writes a state, typed by its submachine's state def when it has one, // with its entry, do and exit actions, deferrals and regions. func (s *stateRegion) state(v *sysmlv1.Element) { @@ -459,8 +823,11 @@ func (s *stateRegion) state(v *sysmlv1.Element) { regions := s.m.populatedRegions(v) entry, do, exit := s.m.behaviorIn(v, "entry"), s.m.behaviorIn(v, "doActivity"), s.m.behaviorIn(v, "exit") inv := firstOwned(v, "stateInvariant") - if entry == nil && do == nil && exit == nil && inv == nil && len(regions) == 0 && len(defers) == 0 { + points := s.m.connectionPoints(v) + pointRegs := pointRegions(v) + if entry == nil && do == nil && exit == nil && inv == nil && len(regions) == 0 && len(defers) == 0 && s.m.writtenPoints(v) == 0 && !hasTransitions(pointRegs) { s.m.w.line(head + ";") + s.m.statePoints(v) return } s.m.w.block(head, func() { @@ -477,15 +844,38 @@ func (s *stateRegion) state(v *sysmlv1.Element) { if exit != nil { s.m.inlineBehavior(exitAction, exit, v) } + if len(points) > 0 { + s.m.statePoints(v) + } } if len(regions) == 0 { between() - return + } else { + s.m.regions(v, regions, entered, between) + } + for _, pr := range pointRegs { + // Named from the state's one region when it has one, so its vertices need no path. + host := pr + if len(regions) == 1 { + host = regions[0] + } + for _, t := range pr.Owned("transition") { + s.m.region(host).transition(t) + } } - s.m.regions(v, regions, entered, between) }) } +// hasTransitions reports whether any of the regions owns a transition. +func hasTransitions(regions []*sysmlv1.Element) bool { + for _, r := range regions { + if len(r.Owned("transition")) > 0 { + return true + } + } + return false +} + // invariant keeps a state invariant, which v2 has no form for, as a comment. func (m *migration) invariant(inv *sysmlv1.Element) { note := "a state invariant has no v2 form" @@ -710,7 +1100,7 @@ func (s *stateRegion) path(v *sysmlv1.Element) (string, bool) { if !ok || machineOf(v) != s.machine { return "", false } - if v.Parent == s.r || v.Parent == s.machine { + if owner := memberOwner(v); owner == s.r || owner == s.machine { return writeName(name), true } segs := s.m.segments(v) @@ -724,7 +1114,7 @@ func (s *stateRegion) endpoint(t, v *sysmlv1.Element, role string) (string, bool if !ok { return "", false } - if v.Parent != s.r && v.Parent != s.machine { + if owner := memberOwner(v); owner != s.r && owner != s.machine { s.m.add(t, Mapped, "", "the "+role+" "+describe(v)+" lies in another region and is named by its path "+p) } return p, true @@ -751,12 +1141,31 @@ func (s *stateRegion) target(t, v *sysmlv1.Element) (string, bool) { case "terminate": return "done", true case "exitPoint": + if s.m.points[v].why != "" { + return "", false + } if p, ok := s.endpoint(t, v, "target"); ok { return p, true } + if pointOwner(v).Type == "State" { + return "", false + } s.m.add(t, Approximated, "", "the exit point "+describe(v)+" belongs to another machine; the transition is written to done") return "done", true - case "choice", "junction", "fork", "join", "shallowHistory", "deepHistory", "entryPoint": + case "entryPoint": + f := s.m.points[v] + if f.why != "" { + return "", false + } + if f.defaultEntry { + p, ok := s.endpoint(t, pointOwner(v), "target") + if ok { + s.m.add(t, Mapped, "", "written to "+p+": no transition leaves the entry point "+describe(v)+", so entering through it enters the state by its default entry") + } + return p, ok + } + return s.endpoint(t, v, "target") + case "choice", "junction", "fork", "join", "shallowHistory", "deepHistory": return s.endpoint(t, v, "target") } case "ConnectionPointReference": @@ -776,7 +1185,12 @@ func (s *stateRegion) source(t, v *sysmlv1.Element) (string, bool) { return s.endpoint(t, v, "source") case "Pseudostate": switch pseudoKind(v) { - case "choice", "junction", "fork", "join", "shallowHistory", "deepHistory", "entryPoint", "exitPoint": + case "entryPoint", "exitPoint": + if s.m.points[v].why != "" { + return "", false + } + return s.endpoint(t, v, "source") + case "choice", "junction", "fork", "join", "shallowHistory", "deepHistory": return s.endpoint(t, v, "source") } case "ConnectionPointReference": @@ -785,6 +1199,25 @@ func (s *stateRegion) source(t, v *sysmlv1.Element) (string, bool) { return "", false } +// noForm says why vertex v is no end for a transition: its connection point's refusal, +// else that it lies outside the machine or has no v2 form. +func (s *stateRegion) noForm(v *sysmlv1.Element) string { + if why := s.m.points[v].why; why != "" { + return " has no v2 form: " + why + } + return isA + kindOf(v) + outsideMachine +} + +// transient reports a pseudostate a compound transition passes through in one +// step, whose outgoing transitions the runtime follows on reaching it. +func transient(v *sysmlv1.Element) bool { + switch pseudoKind(v) { + case "junction", "choice", "entryPoint", "exitPoint": + return true + } + return false +} + // connection names the state of a submachine's state def a connection point // reference stands for, through the submachine state: `sub::point`. The // reference is reported once, where the first transition passes through it. @@ -836,15 +1269,19 @@ func (s *stateRegion) transition(t *sysmlv1.Element) { if !ok { return } + if transient(src) && (pseudoKind(tgt) == "shallowHistory" || pseudoKind(tgt) == "deepHistory") { + s.m.unmapped(t, "the runtime does not follow a transition from a "+pseudoKind(src)+" pseudostate on into the history pseudostate "+describe(tgt)) + return + } from, ok := s.source(t, src) if !ok { - s.m.unmapped(t, "the source "+describe(src)+isA+kindOf(src)+outsideMachine) + s.m.unmapped(t, "the source "+describe(src)+s.noForm(src)) return } to := from if !internal { if to, ok = s.target(t, tgt); !ok { - s.m.unmapped(t, "the target "+describe(tgt)+isA+kindOf(tgt)+outsideMachine) + s.m.unmapped(t, "the target "+describe(tgt)+s.noForm(tgt)) return } } @@ -1151,7 +1588,7 @@ func inParameters(b *sysmlv1.Element) []*sysmlv1.Element { // comment when it is not a v2 expression the state machine's owner resolves; // an else guard out of a choice or junction is the unguarded transition. func (s *stateRegion) guard(t, src *sysmlv1.Element) (string, string) { - g := firstOwned(t, "guard") + g := s.m.guardOf(t) if g == nil { return "", "" } @@ -1160,7 +1597,7 @@ func (s *stateRegion) guard(t, src *sysmlv1.Element) (string, string) { s.m.add(g, Unmapped, "", "the guard has no specification") return "", "the guard " + describe(g) + " has no specification and is dropped" } - if spec.Type == "LiteralBoolean" && (spec.Attrs["value"] == "true" || spec.Attrs["value"] == "") { + if trueLiteral(spec) { s.m.add(g, Mapped, "", "a true guard is not written") return "", "" } diff --git a/tests/corpus/testdata/pssm_migration_expected.txt b/tests/corpus/testdata/pssm_migration_expected.txt index 0394614f5..8c0110b56 100644 --- a/tests/corpus/testdata/pssm_migration_expected.txt +++ b/tests/corpus/testdata/pssm_migration_expected.txt @@ -5,8 +5,8 @@ # ratchet whose every movement is adjudicated. See docs/project/pssm-migration.md. # Regenerate with: # go test ./tests/corpus -run TestPSSMSuiteMigration -update-pssm-migration -13562 approximated -23512 mapped +13681 approximated +23731 mapped 6 skipped -10229 unmapped +10242 unmapped 21 validation-errors diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go index bdada96c8..f3d2dd2ac 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -297,7 +297,7 @@ func TestRejectsNonXMI(t *testing.T) { // constructFixtures are the XMI documents under testdata/xmi that each exercise // one family of behavioral constructs; their notation and report are golden. -var constructFixtures = []string{"plant_states", "rig_interactions", "heater_receptions", "ported_calls", "empty_behaviors"} +var constructFixtures = []string{"plant_states", "station_points", "rig_interactions", "heater_receptions", "ported_calls", "empty_behaviors"} // migrateFixtureFile migrates testdata/xmi/.xmi. func migrateFixtureFile(t *testing.T, name string) *migrate.Result { diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index 86cbe9ac0..a86ba7e8c 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -178,6 +178,163 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { } } +// testdata/xmi/station_points.xmi: entry and exit points owned by composite states — on a +// nested state, on a state with orthogonal regions, beside a default initial pseudostate +// and a shallow history — are written as junctions, a fork and a join of their state; an +// entry point leading straight to an exit point of its state is refused. Every entry, exit +// and effect behavior appends a two-digit code to trace, so a run pins the UML order: +// 11 Work entry, 12 Work exit, 13 Prep entry, 14 Run entry, 15 Run exit, 17 Fast entry, +// 21 Deep→Fast, 22 Fast→Out, 23 Start→Run, 24 Run→Leave, 25 Out→Prep, 31 Sync entry, +// 32 Sync exit, 33/34 A1 entry/exit, 35/36 B1 entry/exit, 37 A1→Gather, 38 B1→Gather, +// 41 Idle→Start, 42 Idle→Deep, 43 Leave→Idle, 44 Work→Idle, 46 Gather→Idle. +func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { + r := migrateFixtureFile(t, "station_points") + for _, line := range []string{ + "state Work {", + "junction Start;", + "junction Leave;", + "junction Deep;", + "junction Out;", + "history H;", + "transition first Work::Run::Deep", + "transition first Fast accept Back", + "then Work::Run::Out;", + "transition first Work::Start", + "transition first Run accept Finish", + "then Work::Leave;", + "transition first Work::Run::Out", + "fork Both;", + "join Gather;", + "transition first Sync::Both then A1;", + "transition first Sync::Both then B1;", + "then Sync::Gather;", + "then Work::Start;", + "transition first Idle accept Enter then Work;", + "then Work::Run::Deep;", + "transition first Idle accept Resume then Work::H;", + "transition first Work::Leave", + "transition first Idle accept Split then Sync::Both;", + "transition first Sync::Gather", + "/* not migrated: Pseudostate 'Through' — (_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior */", + } { + wantLine(t, r.Notation, line) + } + if strings.Contains(string(r.Notation), "state Start") || strings.Contains(string(r.Notation), "then Through") { + t.Errorf("a connection point was written as a state, or a refused one was named:\n%s", r.Notation) + } + wantNote(t, r, "_start", migrate.Mapped, "written as a junction of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction") + wantNote(t, r, "_leave", migrate.Mapped, "written as a junction of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it") + wantNote(t, r, "_deep", migrate.Mapped, "written as a junction of its state") + wantNote(t, r, "_out", migrate.Mapped, "written as a junction of its state") + wantNote(t, r, "_plain", migrate.Mapped, "no transition leaves the entry point, so entering through it enters 'Work' by its default entry; a transition to it is written to the state") + wantNote(t, r, "_tEnter", migrate.Mapped, "written to Work: no transition leaves the entry point 'Plain'") + wantNote(t, r, "_both", migrate.Mapped, "written as a fork of its state, whose branches start its regions; a transition entering through it runs the state's entry behavior, then the branches") + wantNote(t, r, "_gather", migrate.Mapped, "written as a join of its state, which its regions leave through together; the transitions into the join run, then the state's exit behavior, then the transition leaving it") + wantNote(t, r, "_tGo", migrate.Mapped, "named by its path Work::Start") + wantNote(t, r, "_tDive", migrate.Mapped, "named by its path Work::Run::Deep") + wantNote(t, r, "_tBothA", migrate.Mapped, "named by its path Sync::Both") + wantNote(t, r, "_tAg", migrate.Mapped, "named by its path Sync::Gather") + wantNote(t, r, "_hist", migrate.Mapped, "written as a shallow history") + wantNote(t, r, "_through", migrate.Unmapped, "leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior") + wantNote(t, r, "_tThrough", migrate.Unmapped, "the source 'Through' has no v2 form") + wantNote(t, r, "_tSkip", migrate.Unmapped, "the target 'Through' has no v2 form") + + s := session(t, r) + trace := func(object, want string) { + t.Helper() + if out := meta(t, s, "%eval in "+object+" : trace"); strings.TrimSpace(out[strings.LastIndex(out, "=")+1:]) != want { + t.Errorf("trace of %s: want %s, got\n%s", object, want, out) + } + } + current := func(want string) { + t.Helper() + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: "+want) { + t.Errorf("want the current state %s:\n%s", want, out) + } + } + station := func() string { + t.Helper() + out := meta(t, s, "%instantiate Station") + _, id, ok := strings.Cut(out, "ID: ") + if !ok { + t.Fatalf("%%instantiate Station: %s", out) + } + id, _, _ = strings.Cut(id, "\n") + object := "#" + strings.TrimSpace(id) + meta(t, s, "%state Station::Cycle "+object) + return object + } + drive := func(signal, fires string) { + t.Helper() + if out := meta(t, s, "%send "+signal); !strings.Contains(out, "transition "+fires+" fires on it") { + t.Errorf("%%send %s: %s", signal, out) + } + meta(t, s, "%step") + } + + // Through the entry point Start: the effect into it, Work's entry, the + // transition out of it, then Run and its default Slow. Out through Leave + // from the nested Run: Run's exit, the transition into it, Work's exit, + // then the transition out of it. + one := station() + drive("Go", "Idle -> Start") + current("Slow") + trace(one, "41112314") + drive("Finish", "Run -> Leave") + current("Idle") + trace(one, "4111231415241243") + meta(t, s, "%stop") + + // Into the nested Run through its own entry point Deep: Work's entry, Run's + // entry, the transition out of Deep, then Fast rather than the default Slow. + // Out of Run through its exit point Out: the transition into it, Run's exit, + // then the transition out of it into Prep, still inside Work. + two := station() + drive("Dive", "Idle -> Deep") + current("Fast") + trace(two, "4211142117") + drive("Back", "Fast -> Out") + current("Prep") + trace(two, "421114211722152513") + meta(t, s, "%stop") + + // An entry point no transition leaves enters Work by its default Prep. After + // Work is left from Run, the shallow history beside the entry points brings + // Run back, with its default Slow. + three := station() + drive("Enter", "Idle -> Work") + current("Prep") + trace(three, "1113") + drive("Next", "Prep -> Run") + drive("Stop", "Work -> Idle") + trace(three, "111314151244") + drive("Resume", "Idle -> H") + current("Slow") + trace(three, "1113141512441114") + meta(t, s, "%stop") + + // The entry point Both of the orthogonal Sync starts both regions at once + // after Sync's entry; each region then leaves through the exit point Gather, + // which joins them: both exits and effects, Sync's exit, then the transition out. + four := station() + drive("Split", "Idle -> Both") + current("A1 | B1") + trace(four, "313335") + meta(t, s, "%step") + current("Idle") + trace(four, "313335343736383246") + meta(t, s, "%stop") + + // Entered plainly, Sync's regions start at A0 and B0 and reach Gather the same way. + five := station() + drive("Pair", "Idle -> Sync") + current("A0 | B0") + drive("Bump", "A0 -> A1 and transition B0 -> B1") + meta(t, s, "%step") + current("Idle") + trace(five, "313335343736383246") +} + // gateMachine has an empty region beside the one holding its states, and transitions // between Idle and the nested Busy::Inner across nesting levels. const gateMachine = ` @@ -435,3 +592,862 @@ func TestTargetlessInternalTransitionsStayInTheirSource(t *testing.T) { t.Errorf("the internal transition left Idle:\n%s", out) } } + +const emptyEffectMachine = ` + + + + + + + + + + + + + + + + ` + +const emptyEffectApplications = ` + ` + +// A transition whose effect activity has no nodes keeps its braces, `do action effect { }`, +// so the `then` clause that follows still belongs to the transition; the result parses and runs. +func TestEmptyTransitionEffectKeepsItsBraces(t *testing.T) { + r := migrateDocument(t, emptyEffectMachine, emptyEffectApplications) + wantLine(t, r.Notation, "do action effect { }\n") + wantLine(t, r.Notation, "then Busy;") + if strings.Contains(string(r.Notation), "do action effect;") { + t.Errorf("an empty effect ended the transition clause:\n%s", r.Notation) + } + s := session(t, r) + meta(t, s, "%instantiate Blank") + meta(t, s, "%state Blank::Blanking") + if out := meta(t, s, "%send Go"); !strings.Contains(out, "transition Idle -> Busy fires on it") { + t.Errorf("%%send Go: %s", out) + } + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Busy") { + t.Errorf("the transition with the empty effect did not fire:\n%s", out) + } +} + +// joinShapesMachine has three orthogonal states whose exit point several regions reach, each +// also reached in a way a join cannot take: twice from one region, from outside the state, +// and from a junction. +const joinShapesMachine = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const joinShapesApplications = ` + ` + +// An exit point several regions reach is a join; one also reached twice from one region or from +// a pseudostate is refused with that shape named, one reached from outside its state with that +// transition named, and their transitions with them. +func TestExitPointJoinShapesAreRefusedPrecisely(t *testing.T) { + r := migrateDocument(t, joinShapesMachine, joinShapesApplications) + if strings.Contains(string(r.Notation), "join out;") { + t.Errorf("a refused exit point was written as a join:\n%s", r.Notation) + } + wantNote(t, r, "_jxTwice", migrate.Unmapped, "as through a join, but two of its incoming transitions leave the same region") + wantNote(t, r, "_jxOuter", migrate.Unmapped, "(_jtIn) leads to the exit point from outside the state, from 'Idle'") + wantNote(t, r, "_jxPseudo", migrate.Unmapped, "as through a join, but (_jpaT2) leaves 'j', a Pseudostate rather than a state") + for _, id := range []string{"_jtaT1", "_jtaT2", "_jtbT1", "_jtTwice", "_joaT1", "_jobT1", "_jtIn", "_jtOuter", "_jpaT2", "_jpbT1", "_jtPseudo"} { + wantNote(t, r, id, migrate.Unmapped, "has no v2 form") + } +} + +// entryShapesMachine has five composite states whose entry point leaves by a route a junction +// cannot take: out of the state, on into the state's history, to no target, back to the state, +// and on a trigger. +const entryShapesMachine = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const entryShapesApplications = ` + ` + +// An entry point whose route leaves the state, runs on into a history, reaches no target, leads +// back to the state or waits on a trigger is refused with that route named, the transitions +// through it with it, and no junction is written. +func TestEntryPointRoutesAreRefusedPrecisely(t *testing.T) { + r := migrateDocument(t, entryShapesMachine, entryShapesApplications) + if strings.Contains(string(r.Notation), "junction in;") { + t.Errorf("a refused entry point was written as a junction:\n%s", r.Notation) + } + wantNote(t, r, "_eaIn", migrate.Unmapped, "(_etOut) leads from the entry point out of the state, to 'Idle'") + wantNote(t, r, "_ebIn", migrate.Unmapped, "(_ebrT1) leads from the entry point on into the history pseudostate 'H', which the runtime does not follow from a junction") + wantNote(t, r, "_ecIn", migrate.Unmapped, "(_ecrT1) leads from the entry point to no target") + wantNote(t, r, "_edIn", migrate.Unmapped, "(_edrT1) leads from the entry point back to the state itself, which v1 enters by its default entry while the runtime would leave and re-enter it") + wantNote(t, r, "_eeIn", migrate.Unmapped, "(_eerT1) leads from the entry point with a trigger, which no transition out of a pseudostate takes; the runtime would follow it without waiting for the event") + for _, id := range []string{"_etAway", "_etOut", "_etBack", "_etBare", "_etSelf", "_edrT1", "_etWait", "_eerT1"} { + wantNote(t, r, id, migrate.Unmapped, "has no v2 form") + } + wantNote(t, r, "_ebrT1", migrate.Unmapped, "does not follow a transition from a entryPoint pseudostate on into the history pseudostate") + wantNote(t, r, "_ecrT1", migrate.Unmapped, "lacks an end") +} + +// guardedEntryMachine has a composite state whose entry point leaves by a guarded transition, +// and whose entry behavior falsifies that guard. +const guardedEntryMachine = ` + + + + + + + + + + + + + + JavaScript + armed = false; + + + + + + + + + + + JavaScript + armed + + + + + + + + + + + + ` + +const guardedEntryApplications = ` + ` + +// A guard on the transition leaving an entry point is kept: UML evaluates it with the compound +// transition's other guards before the transition fires, as the runtime does at a junction, so +// the owning state's entry behavior falsifying it does not turn the route. +func TestGuardedEntryPointRouteIsKept(t *testing.T) { + r := migrateDocument(t, guardedEntryMachine, guardedEntryApplications) + for _, line := range []string{"junction arm;", "transition first Work::arm if this.armed then W2;", "transition first Idle accept Go then Work::arm;"} { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + wantNote(t, r, "_gIn", migrate.Mapped, "written as a junction of its state") + wantNote(t, r, "_gGuard", migrate.Mapped, "") + s := session(t, r) + meta(t, s, "%instantiate Rig") + meta(t, s, "%state Rig::Main #1") + if out := meta(t, s, "%send Go"); !strings.Contains(out, "transition Idle -> arm fires on it") { + t.Errorf("%%send Go: %s", out) + } + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: W2") { + t.Errorf("the guarded entry route was not taken on the guard's value before Work's entry:\n%s", out) + } + if out := meta(t, s, "%eval in #1 : armed"); !strings.Contains(out, "= false") { + t.Errorf("Work's entry behavior did not run: %s", out) + } +} + +// nestedMachines has a machine Front whose submachine state enters Outer through a connection +// point reference, so Outer is named before it is written, and a machine Inner nested in Outer +// whose composite state Par has an entry point forking into its two regions. +const nestedMachines = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const nestedMachinesApplications = ` + ` + +// A machine nested in another is indexed once however early the outer one is named, so the +// entry point of its composite state still forks into two regions rather than four transitions. +func TestNestedMachineTransitionsAreIndexedOnce(t *testing.T) { + r := migrateDocument(t, nestedMachines, nestedMachinesApplications) + for _, line := range []string{ + "fork split;", + "transition first Par::split then A2;", + "transition first Par::split then B2;", + "transition first Idle accept Go then Par::split;", + } { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + wantNote(t, r, "_niIn", migrate.Mapped, "written as a fork of its state") +} + +// regionListedPoints has an orthogonal state Sync whose entry point Both and exit point Gather a +// tool listed among the vertices of its regions rather than as its connection points. +const regionListedPoints = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const regionListedPointsApplications = ` + ` + +// A connection point a tool lists in a region of an orthogonal state is written in the state's +// body, and every transition through it names it there, not in the region it was listed in. +func TestRegionListedPointsArePathedFromTheirState(t *testing.T) { + r := migrateDocument(t, regionListedPoints, regionListedPointsApplications) + for _, line := range []string{ + "fork Both;", + "join Gather;", + "transition first Idle accept Go then Sync::Both;", + "transition first Sync::Both then A2;", + "transition first Sync::Both then B2;", + "transition first A2 then Sync::Gather;", + "transition first B2 then Sync::Gather;", + "transition first Sync::Gather then Idle;", + } { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + if strings.Contains(string(r.Notation), "regions::") { + t.Errorf("a connection point is named through the region it was listed in:\n%s", r.Notation) + } + wantNote(t, r, "_rBoth", migrate.Mapped, "written as a fork of its state") + wantNote(t, r, "_rGather", migrate.Mapped, "written as a join of its state") + session(t, r) +} + +// pointOnlyRegion has a state Sync with one region of its own, a, and a second, b, in which a +// tool listed nothing but Sync's entry point Both and exit point Gather and the transition +// leaving Both. +const pointOnlyRegion = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const pointOnlyRegionApplications = ` + ` + +// A region listing only its state's connection points holds nothing to enter: it is skipped, so the +// state has one region and its points are junctions, not a fork and a join into a parallel state +// with an empty branch; the transitions the region holds are written in the state's body. +func TestPointOnlyRegionIsNotAParallelBranch(t *testing.T) { + r := migrateDocument(t, pointOnlyRegion, pointOnlyRegionApplications) + for _, line := range []string{ + "junction Both;", + "junction Gather;", + "transition first Idle accept Go then Sync::Both;", + "transition first Sync::Both then A2;", + "transition first A2 accept Stop then Sync::Gather;", + "transition first Sync::Gather then Idle;", + } { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + for _, bad := range []string{"parallel", "regions::", "fork ", "join "} { + if strings.Contains(string(r.Notation), bad) { + t.Errorf("the point-only region was written as a parallel branch (%q):\n%s", bad, r.Notation) + } + } + wantNote(t, r, "_pb", migrate.Skipped, "the region lists only connection points of its state, which are written in the state's body") + wantNote(t, r, "_pBoth", migrate.Mapped, "written as a junction of its state") + wantNote(t, r, "_pGather", migrate.Mapped, "written as a junction of its state") + wantNote(t, r, "_pbT1", migrate.Mapped, "") + s := session(t, r) + meta(t, s, "%instantiate Rig") + meta(t, s, "%state Rig::Main #1") + meta(t, s, "%send Go") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: A2") { + t.Errorf("entering through Both did not reach A2:\n%s", out) + } + meta(t, s, "%send Stop") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Idle") { + t.Errorf("leaving through Gather did not reach Idle:\n%s", out) + } +} + +// noDefaultEntryMachine has a composite state Work whose region holds a state but no initial +// pseudostate, and an entry point in that no transition leaves. +const noDefaultEntryMachine = ` + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const noDefaultEntryApplications = ` + ` + +// An entry point no transition leaves enters its state by the state's default entry, which, with no +// initial pseudostate in the region, enters the state and leaves the region inactive in v1 and in +// the runtime alike; the transition is written to the state and the report says so. +func TestDefaultEntryPointOnOwnerWithoutInitialEntersTheState(t *testing.T) { + r := migrateDocument(t, noDefaultEntryMachine, noDefaultEntryApplications) + for _, line := range []string{ + "transition first Idle accept Go then Work;", + "transition first Work accept Stop then Idle;", + "the region has no initial pseudostate: nothing enters it", + } { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + if strings.Contains(string(r.Notation), "junction via;") { + t.Errorf("an entry point no transition leaves was written as a junction:\n%s", r.Notation) + } + wantNote(t, r, "_dIn", migrate.Mapped, "no initial pseudostate starts the region 'r', which v1 too leaves inactive on entering the state") + wantNote(t, r, "_dT1", migrate.Mapped, "written to Work: no transition leaves the entry point 'via'") + s := session(t, r) + meta(t, s, "%instantiate Rig") + meta(t, s, "%state Rig::Main #1") + meta(t, s, "%send Go") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Work") || strings.Contains(out, "W1") { + t.Errorf("entering Work by its default entry did not leave its region inactive:\n%s", out) + } + meta(t, s, "%send Stop") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Idle") { + t.Errorf("Work was not left on Stop:\n%s", out) + } +} + +// exitShapesMachine has six composite states with one exit point each, every one reached from a +// state within by a triggered transition: Wait's leaves by a triggered transition, Bare's to no +// target, Back's into a history, Self's to Self itself, Inner's back to a state within, Dead's not at all. +const exitShapesMachine = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const exitShapesApplications = ` + ` + +// An exit point whose outgoing route waits on a trigger, reaches no target, runs on into a +// history, leads back to the state or into it, or that no transition leaves is refused with that +// route named, the transitions through it with it, and no junction is written. +func TestExitPointRoutesAreRefusedPrecisely(t *testing.T) { + r := migrateDocument(t, exitShapesMachine, exitShapesApplications) + if strings.Contains(string(r.Notation), "junction leave;") { + t.Errorf("a refused exit point was written as a junction:\n%s", r.Notation) + } + wantNote(t, r, "_xaOut", migrate.Unmapped, "(_xT1) leads from the exit point with a trigger, which no transition out of a pseudostate takes; the runtime would follow it without waiting for the event") + wantNote(t, r, "_xbOut", migrate.Unmapped, "(_xT2) leads from the exit point to no target") + wantNote(t, r, "_xcOut", migrate.Unmapped, "(_xT3) leads from the exit point on into the history pseudostate 'H', which the runtime does not follow from a junction") + wantNote(t, r, "_xdOut", migrate.Unmapped, "(_xT4) leads from the exit point back to the state itself, which v1 leaves and re-enters by its default entry while the runtime, moving from a member of the state to the state, would stay in it, running neither its exit nor its entry") + wantNote(t, r, "_xeOut", migrate.Unmapped, "(_xeT2) leads from the exit point back into the state, to 'E2', which v1 leaves and re-enters while the runtime, moving between members of the state, would stay in it") + wantNote(t, r, "_xfOut", migrate.Unmapped, "no transition leaves the exit point, so a transition into it leaves 'Dead' for nowhere; the runtime would halt at the junction") + for _, id := range []string{"_xaT1", "_xT1", "_xbT1", "_xcT1", "_xdT1", "_xT4", "_xeT1", "_xeT2", "_xfT1"} { + wantNote(t, r, id, migrate.Unmapped, "has no v2 form") + } + wantNote(t, r, "_xT2", migrate.Unmapped, "lacks an end") + wantNote(t, r, "_xT3", migrate.Unmapped, "does not follow a transition from a exitPoint pseudostate on into the history pseudostate") +} + +// outsideExitMachine has two composite states with one exit point each: Work's is reached from +// Idle, outside Work, besides from within; Self's by a local transition of Self itself. +const outsideExitMachine = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const outsideExitApplications = ` + ` + +// An exit point one transition reaches from outside its state is refused with that transition named +// however few transitions reach it: written as a junction, the runtime would enter the state to leave +// it, running entry and exit behaviors v1 never runs. One a local transition of the state itself +// reaches is a junction, the transition approximated as the local rule says. +func TestExitPointReachedFromOutsideIsRefused(t *testing.T) { + r := migrateDocument(t, outsideExitMachine, outsideExitApplications) + if n := strings.Count(string(r.Notation), "junction leave;"); n != 1 { + t.Errorf("want Self's exit point alone written as a junction, got %d:\n%s", n, r.Notation) + } + for _, line := range []string{"transition first Self accept Go then Self::leave;", "transition first Self::leave then Idle;"} { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + wantNote(t, r, "_oOut", migrate.Unmapped, "(_oT1) leads to the exit point from outside the state, from 'Idle'; v1 never enters the state, while the runtime would enter and leave it") + for _, id := range []string{"_owT1", "_oT1", "_oT2"} { + wantNote(t, r, id, migrate.Unmapped, "has no v2 form") + } + wantNote(t, r, "_oSOut", migrate.Mapped, "written as a junction of its state") + wantNote(t, r, "_oT3", migrate.Approximated, "a local transition is written external: the composite state Self exits and re-enters") + wantNote(t, r, "_oT4", migrate.Mapped, "") +} + +// An entry point a tool lists in a region of an orthogonal state is a member of the state's +// body, so it is renamed when the body has a member of its name, as a sibling would be; +// a point written as no member, here one named like the first free name, is no obstacle. +func TestRegionListedPointKeepsClearOfTheStatesMembers(t *testing.T) { + entry := ` + + ` + xmi := strings.Replace(regionListedPoints, ``, entry, 1) + r := migrateDocument(t, xmi, regionListedPointsApplications) + for _, line := range []string{ + "entry action Both", + "fork 'Both 2';", + "transition first Idle accept Go then Sync::'Both 2';", + "transition first Sync::'Both 2' then A2;", + } { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + wantNote(t, r, "_rBoth", migrate.Approximated, "written as Both 2 since a sibling is also named Both") + wantNote(t, r, "_rBoth2", migrate.Mapped, "no transition leaves the entry point") + session(t, r) +} + +// A connection point that is written as no member takes no name from the state's body: +// an entry behavior named like an entry point nothing leaves keeps its name. +func TestUnwrittenPointLeavesTheStatesMembersTheirNames(t *testing.T) { + xmi := regionListedPoints + for _, leaving := range []string{ + ``, + ``, + } { + xmi = strings.Replace(xmi, leaving, "", 1) + } + xmi = strings.Replace(xmi, ` + `, ` + + `, 1) + r := migrateDocument(t, xmi, regionListedPointsApplications) + for _, line := range []string{ + "entry action Both", + "transition first Idle accept Go then Sync;", + } { + if !strings.Contains(string(r.Notation), line) { + t.Errorf("missing %q in:\n%s", line, r.Notation) + } + } + if strings.Contains(string(r.Notation), "Both 2") { + t.Errorf("a member is renamed for a connection point that is written as no member:\n%s", r.Notation) + } + wantNote(t, r, "_rBoth", migrate.Mapped, "no transition leaves the entry point") + session(t, r) +} diff --git a/tests/migrate/testdata/xmi/station_points.golden.report.txt b/tests/migrate/testdata/xmi/station_points.golden.report.txt new file mode 100644 index 000000000..8803e1282 --- /dev/null +++ b/tests/migrate/testdata/xmi/station_points.golden.report.txt @@ -0,0 +1,134 @@ +# SysML v1 to v2 migration report: station_points.xmi +# migrated 126 element(s): 96 mapped, 26 approximated, 4 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) + +## unmapped (4) +SignalEvent _skipEv (every trigger referring to the event is dropped: it belongs to a behavior that is not written, or to an initial transition) +Transition Station::Cycle::main:: _tSkip (the target 'Through' has no v2 form: (_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior) +Pseudostate Station::Cycle::main::Work::Through _through ((_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior) +Transition Station::Cycle::main::Work::steps:: _tThrough (the source 'Through' has no v2 form: (_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior) + +## approximated (26) +Class Station _station -> Station (a plain UML class without «Block» is written as a part def) +StateMachine Station::Cycle _cycle -> Station::Cycle (the classifier behavior is run by every object of Station as its usage cycle) +OpaqueBehavior Station::Cycle::main:::: _goEff -> Station::Cycle::unnamed::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main:::: _diveEff -> Station::Cycle::unnamed2::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main:::: _leaveEff -> Station::Cycle::unnamed3::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main:::: _stopEff -> Station::Cycle::unnamed4::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main:::: _gatherEff -> Station::Cycle::unnamed5::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync:: _syncEntry -> Station::Cycle::Sync::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync:: _syncExit -> Station::Cycle::Sync::unnamed2 (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync::a:::: _agEff -> Station::Cycle::Sync::regions::a::unnamed::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync::a::A1:: _a1Entry -> Station::Cycle::Sync::regions::a::A1::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync::a::A1:: _a1Exit -> Station::Cycle::Sync::regions::a::A1::unnamed2 (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync::b:::: _bgEff -> Station::Cycle::Sync::regions::b::unnamed::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync::b::B1:: _b1Entry -> Station::Cycle::Sync::regions::b::B1::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Sync::b::B1:: _b1Exit -> Station::Cycle::Sync::regions::b::B1::unnamed2 (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work:: _workEntry -> Station::Cycle::Work::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work:: _workExit -> Station::Cycle::Work::unnamed2 (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps:::: _startEff -> Station::Cycle::Work::unnamed::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps:::: _finishEff -> Station::Cycle::Work::unnamed2::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps:::: _outEff -> Station::Cycle::Work::unnamed3::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps::Prep:: _prepEntry -> Station::Cycle::Work::Prep::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps::Run:: _runEntry -> Station::Cycle::Work::Run::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps::Run:: _runExit -> Station::Cycle::Work::Run::unnamed2 (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps::Run::pace:::: _deepEff -> Station::Cycle::Work::Run::unnamed::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps::Run::pace:::: _backEff -> Station::Cycle::Work::Run::unnamed2::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) +OpaqueBehavior Station::Cycle::main::Work::steps::Run::pace::Fast:: _fastEntry -> Station::Cycle::Work::Run::Fast::unnamed (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) + +## mapped (96) +SignalEvent _backEv (written where a trigger refers to it, as accept Back) +SignalEvent _nextEv (written where a trigger refers to it, as accept Next) +SignalEvent _finishEv (written where a trigger refers to it, as accept Finish) +SignalEvent _bumpEv (written where a trigger refers to it, as accept Bump) +SignalEvent _goEv (written where a trigger refers to it, as accept Go) +SignalEvent _enterEv (written where a trigger refers to it, as accept Enter) +SignalEvent _diveEv (written where a trigger refers to it, as accept Dive) +SignalEvent _resumeEv (written where a trigger refers to it, as accept Resume) +SignalEvent _stopEv (written where a trigger refers to it, as accept Stop) +SignalEvent _pairEv (written where a trigger refers to it, as accept Pair) +SignalEvent _splitEv (written where a trigger refers to it, as accept Split) +Signal Back _back -> Back +Signal Bump _bump -> Bump +Signal Dive _dive -> Dive +Signal Enter _enter -> Enter +Signal Finish _finish -> Finish +Signal Go _go -> Go +Model Model _m (the root model's members are written at the top level) +Signal Next _next -> Next +Signal Pair _pair -> Pair +Signal Resume _resume -> Resume +Signal Skip _skip -> Skip +Signal Split _split -> Split +Region Station::Cycle::main _main (the one region is written as the body of its owner) +Pseudostate Station::Cycle::main:: _init0 (written as the entry of the region) +Transition Station::Cycle::main:: _t0 +Transition Station::Cycle::main:: _tGo (the target 'Start' lies in another region and is named by its path Work::Start) +Transition Station::Cycle::main:: _tNever +Transition Station::Cycle::main:: _tEnter (written to Work: no transition leaves the entry point 'Plain', so entering through it enters the state by its default entry) +Transition Station::Cycle::main:: _tDive (the target 'Deep' lies in another region and is named by its path Work::Run::Deep) +Transition Station::Cycle::main:: _tResume (the target 'H' lies in another region and is named by its path Work::H) +Transition Station::Cycle::main:: _tLeave (the source 'Leave' lies in another region and is named by its path Work::Leave) +Transition Station::Cycle::main:: _tStop +Transition Station::Cycle::main:: _tPair +Transition Station::Cycle::main:: _tSplit (the target 'Both' lies in another region and is named by its path Sync::Both) +Transition Station::Cycle::main:: _tGather (the source 'Gather' lies in another region and is named by its path Sync::Gather) +Trigger Station::Cycle::main:::: _trGo +Trigger Station::Cycle::main:::: _trNever +Trigger Station::Cycle::main:::: _trEnter +Trigger Station::Cycle::main:::: _trDive +Trigger Station::Cycle::main:::: _trResume +Trigger Station::Cycle::main:::: _trStop +Trigger Station::Cycle::main:::: _trPair +Trigger Station::Cycle::main:::: _trSplit +Constraint Station::Cycle::main::::never _neverGuard +State Station::Cycle::main::Idle _idle -> Idle +State Station::Cycle::main::Sync _sync -> Sync +Pseudostate Station::Cycle::main::Sync::Both _both -> Both (written as a fork of its state, whose branches start its regions; a transition entering through it runs the state's entry behavior, then the branches) +Pseudostate Station::Cycle::main::Sync::Gather _gather -> Gather (written as a join of its state, which its regions leave through together; the transitions into the join run, then the state's exit behavior, then the transition leaving it) +Region Station::Cycle::main::Sync::a _ra -> a (the one region is written as the body of its owner; an orthogonal region is written as a sub-state of the parallel state regions) +Pseudostate Station::Cycle::main::Sync::a:: _initA (written as the entry of the region) +Transition Station::Cycle::main::Sync::a:: _tAi +Transition Station::Cycle::main::Sync::a:: _tBothA (the source 'Both' lies in another region and is named by its path Sync::Both) +Transition Station::Cycle::main::Sync::a:: _tA +Transition Station::Cycle::main::Sync::a:: _tAg (the target 'Gather' lies in another region and is named by its path Sync::Gather) +Constraint Station::Cycle::main::Sync::a:::: _bothAGuard (a true guard is not written) +Trigger Station::Cycle::main::Sync::a:::: _trA +State Station::Cycle::main::Sync::a::A0 _a0 -> A0 +State Station::Cycle::main::Sync::a::A1 _a1 -> A1 +Region Station::Cycle::main::Sync::b _rb -> b (the one region is written as the body of its owner; an orthogonal region is written as a sub-state of the parallel state regions) +Pseudostate Station::Cycle::main::Sync::b:: _initB (written as the entry of the region) +Transition Station::Cycle::main::Sync::b:: _tBi +Transition Station::Cycle::main::Sync::b:: _tBothB (the source 'Both' lies in another region and is named by its path Sync::Both) +Transition Station::Cycle::main::Sync::b:: _tB +Transition Station::Cycle::main::Sync::b:: _tBg (the target 'Gather' lies in another region and is named by its path Sync::Gather) +Trigger Station::Cycle::main::Sync::b:::: _trB +State Station::Cycle::main::Sync::b::B0 _b0 -> B0 +State Station::Cycle::main::Sync::b::B1 _b1 -> B1 +State Station::Cycle::main::Work _work -> Work +Pseudostate Station::Cycle::main::Work::Leave _leave -> Leave (written as a junction of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it) +Pseudostate Station::Cycle::main::Work::Plain _plain -> Work (no transition leaves the entry point, so entering through it enters 'Work' by its default entry; a transition to it is written to the state) +Pseudostate Station::Cycle::main::Work::Start _start -> Start (written as a junction of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction) +Region Station::Cycle::main::Work::steps _steps (the one region is written as the body of its owner) +Pseudostate Station::Cycle::main::Work::steps:: _initW (written as the entry of the region) +Transition Station::Cycle::main::Work::steps:: _tWi +Transition Station::Cycle::main::Work::steps:: _tNext +Transition Station::Cycle::main::Work::steps:: _tStart (the source 'Start' lies in another region and is named by its path Work::Start) +Transition Station::Cycle::main::Work::steps:: _tFinish (the target 'Leave' lies in another region and is named by its path Work::Leave) +Transition Station::Cycle::main::Work::steps:: _tOut (the source 'Out' lies in another region and is named by its path Work::Run::Out) +Trigger Station::Cycle::main::Work::steps:::: _trNext +Trigger Station::Cycle::main::Work::steps:::: _trFinish +Pseudostate Station::Cycle::main::Work::steps::H _hist -> H (written as a shallow history, which re-enters the substate active when its state was last left) +State Station::Cycle::main::Work::steps::Prep _prep -> Prep +State Station::Cycle::main::Work::steps::Run _run -> Run +Pseudostate Station::Cycle::main::Work::steps::Run::Deep _deep -> Deep (written as a junction of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction) +Pseudostate Station::Cycle::main::Work::steps::Run::Out _out -> Out (written as a junction of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it) +Region Station::Cycle::main::Work::steps::Run::pace _pace (the one region is written as the body of its owner) +Pseudostate Station::Cycle::main::Work::steps::Run::pace:: _initR (written as the entry of the region) +Transition Station::Cycle::main::Work::steps::Run::pace:: _tRi +Transition Station::Cycle::main::Work::steps::Run::pace:: _tDeep (the source 'Deep' lies in another region and is named by its path Work::Run::Deep) +Transition Station::Cycle::main::Work::steps::Run::pace:: _tBack (the target 'Out' lies in another region and is named by its path Work::Run::Out) +Trigger Station::Cycle::main::Work::steps::Run::pace:::: _trBack +State Station::Cycle::main::Work::steps::Run::pace::Fast _fast -> Fast +State Station::Cycle::main::Work::steps::Run::pace::Slow _slow -> Slow +Property Station::trace _trace -> Station::trace +Signal Stop _stop -> Stop diff --git a/tests/migrate/testdata/xmi/station_points.golden.sysml b/tests/migrate/testdata/xmi/station_points.golden.sysml new file mode 100644 index 000000000..2352bb778 --- /dev/null +++ b/tests/migrate/testdata/xmi/station_points.golden.sysml @@ -0,0 +1,165 @@ +item def Go; +item def Enter; +item def Dive; +item def Resume; +item def Skip; +item def Next; +item def Finish; +item def Back; +item def Stop; +item def Pair; +item def Split; +item def Bump; +part def Station { + attribute trace : ScalarValues::Integer default = 0; + state def Cycle { + entry; then Idle; + state Idle; + state Work { + entry action { + assign this.trace := this.trace * 100 + 11; + } + then Prep; + exit action { + assign this.trace := this.trace * 100 + 12; + } + junction Start; + /* not migrated: Pseudostate 'Through' — (_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior */ + junction Leave; + state Prep { + entry action { + assign this.trace := this.trace * 100 + 13; + } + } + state Run { + entry action { + assign this.trace := this.trace * 100 + 14; + } + then Slow; + exit action { + assign this.trace := this.trace * 100 + 15; + } + junction Deep; + junction Out; + state Slow; + state Fast { + entry action { + assign this.trace := this.trace * 100 + 17; + } + } + transition first Work::Run::Deep + do action { + assign this.trace := this.trace * 100 + 21; + } + then Fast; + transition first Fast accept Back + do action { + assign this.trace := this.trace * 100 + 22; + } + then Work::Run::Out; + } + history H; + transition first Prep accept Next then Run; + transition first Work::Start + do action { + assign this.trace := this.trace * 100 + 23; + } + then Run; + /* not migrated: Transition (_tThrough) — the source 'Through' has no v2 form: (_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior */ + transition first Run accept Finish + do action { + assign this.trace := this.trace * 100 + 24; + } + then Work::Leave; + transition first Work::Run::Out + do action { + assign this.trace := this.trace * 100 + 25; + } + then Prep; + } + state Sync { + entry action { + assign this.trace := this.trace * 100 + 31; + } + then regions; + exit action { + assign this.trace := this.trace * 100 + 32; + } + fork Both; + join Gather; + state regions parallel { + state a { + entry; then A0; + state A0; + state A1 { + entry action { + assign this.trace := this.trace * 100 + 33; + } + exit action { + assign this.trace := this.trace * 100 + 34; + } + } + transition first Sync::Both then A1; + transition first A0 accept Bump then A1; + transition first A1 + do action { + assign this.trace := this.trace * 100 + 37; + } + then Sync::Gather; + } + state b { + entry; then B0; + state B0; + state B1 { + entry action { + assign this.trace := this.trace * 100 + 35; + } + exit action { + assign this.trace := this.trace * 100 + 36; + } + } + transition first Sync::Both then B1; + transition first B0 accept Bump then B1; + transition first B1 + do action { + assign this.trace := this.trace * 100 + 38; + } + then Sync::Gather; + } + } + transition first regions then done; + } + transition first Idle accept Go + do action { + assign this.trace := this.trace * 100 + 41; + } + then Work::Start; + transition first Idle accept Go if false then Work; + transition first Idle accept Enter then Work; + transition first Idle accept Dive + do action { + assign this.trace := this.trace * 100 + 42; + } + then Work::Run::Deep; + transition first Idle accept Resume then Work::H; + /* not migrated: Transition (_tSkip) — the target 'Through' has no v2 form: (_tThrough) leads from the entry point straight to the exit point 'Leave' of the same state, crossing it without settling in it; the runtime would then run neither its entry nor its exit behavior */ + transition first Work::Leave + do action { + assign this.trace := this.trace * 100 + 43; + } + then Idle; + transition first Work accept Stop + do action { + assign this.trace := this.trace * 100 + 44; + } + then Idle; + transition first Idle accept Pair then Sync; + transition first Idle accept Split then Sync::Both; + transition first Sync::Gather + do action { + assign this.trace := this.trace * 100 + 46; + } + then Idle; + } + exhibit state cycle : Cycle; +} diff --git a/tests/migrate/testdata/xmi/station_points.xmi b/tests/migrate/testdata/xmi/station_points.xmi new file mode 100644 index 000000000..86c2fab25 --- /dev/null +++ b/tests/migrate/testdata/xmi/station_points.xmi @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript + trace = trace * 100 + 11; + + + JavaScript + trace = trace * 100 + 12; + + + + + + + + + + JavaScript + trace = trace * 100 + 13; + + + + + JavaScript + trace = trace * 100 + 14; + + + JavaScript + trace = trace * 100 + 15; + + + + + + + + + JavaScript + trace = trace * 100 + 17; + + + + + + JavaScript + trace = trace * 100 + 21; + + + + + + JavaScript + trace = trace * 100 + 22; + + + + + + + + + + + + JavaScript + trace = trace * 100 + 23; + + + + + + + JavaScript + trace = trace * 100 + 24; + + + + + JavaScript + trace = trace * 100 + 25; + + + + + + + + JavaScript + trace = trace * 100 + 31; + + + JavaScript + trace = trace * 100 + 32; + + + + + + + + + JavaScript + trace = trace * 100 + 33; + + + JavaScript + trace = trace * 100 + 34; + + + + + + + + + + + + + + JavaScript + trace = trace * 100 + 37; + + + + + + + + + JavaScript + trace = trace * 100 + 35; + + + JavaScript + trace = trace * 100 + 36; + + + + + + + + + + JavaScript + trace = trace * 100 + 38; + + + + + + + + + + JavaScript + trace = trace * 100 + 41; + + + + + + + + + + + + + + + JavaScript + trace = trace * 100 + 42; + + + + + + + + + + + JavaScript + trace = trace * 100 + 43; + + + + + + JavaScript + trace = trace * 100 + 44; + + + + + + + + + + + JavaScript + trace = trace * 100 + 46; + + + + + + + diff --git a/tests/parser/testdata/parse/state_nested_pseudostate_qualified_ends.golden b/tests/parser/testdata/parse/state_nested_pseudostate_qualified_ends.golden new file mode 100644 index 000000000..e9ce55515 --- /dev/null +++ b/tests/parser/testdata/parse/state_nested_pseudostate_qualified_ends.golden @@ -0,0 +1,41 @@ +(RootNamespace + (Membership visibility="default" + (Definition kind="state" abstract=false variation=false name="Line" + (EntryMember) + (SuccessionEdge source="@entry" target="Idle") + (SubstateMember name="Idle") + (Membership visibility="default" + (Usage kind="state" name="Work" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (EntryMember + (Membership visibility="default" + (Usage kind="action" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false prefix="entry"))) + (SuccessionEdge source="@entry" target="Prep") + (PseudostateNode kind="junction" name="start") + (PseudostateNode kind="junction" name="leave") + (PseudostateNode kind="fork" name="spread") + (PseudostateNode kind="join" name="gather") + (SubstateMember name="Prep") + (Membership visibility="default" + (Usage kind="state" name="Run" ref=false direction="none" composite=false derived=false ordered=false nonunique=false parallel=true + (Membership visibility="default" + (Usage kind="state" name="a" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (EntryMember) + (SuccessionEdge source="@entry" target="a1") + (SubstateMember name="a1") + (TransitionMember source="a1" target="gather"))) + (Membership visibility="default" + (Usage kind="state" name="b" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (EntryMember) + (SuccessionEdge source="@entry" target="b1") + (SubstateMember name="b1") + (TransitionMember source="b1" target="gather"))))) + (TransitionMember source="start" target="Run") + (TransitionMember source="spread" target="Run::a::a1") + (TransitionMember source="spread" target="Run::b::b1") + (TransitionMember source="Prep" target="leave" trigger="Next"))) + (TransitionMember source="Idle" target="Work::start" trigger="Go") + (TransitionMember source="Idle" target="Work::spread" trigger="Split") + (TransitionMember source="Work::leave" target="Idle" + (Membership visibility="default" + (Usage kind="action" name="effect" ref=false direction="none" composite=false derived=false ordered=false nonunique=false))) + (TransitionMember source="Work::gather" target="Idle")))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/state_nested_pseudostate_qualified_ends.sysml b/tests/parser/testdata/parse/state_nested_pseudostate_qualified_ends.sysml new file mode 100644 index 000000000..f85e7e6f6 --- /dev/null +++ b/tests/parser/testdata/parse/state_nested_pseudostate_qualified_ends.sysml @@ -0,0 +1,25 @@ +state def Line { + entry; then Idle; + state Idle; + state Work { + entry action { } + then Prep; + junction start; + junction leave; + fork spread; + join gather; + state Prep; + state Run parallel { + state a { entry; then a1; state a1; transition first a1 then gather; } + state b { entry; then b1; state b1; transition first b1 then gather; } + } + transition first start then Run; + transition first spread then Run::a::a1; + transition first spread then Run::b::b1; + transition first Prep accept Next then leave; + } + transition first Idle accept Go then Work::start; + transition first Idle accept Split then Work::spread; + transition first Work::leave do action effect { } then Idle; + transition first Work::gather then Idle; +}