From af2099ce99d6253d93b8e2d123ef0891c05df512 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:20:49 +0000 Subject: [PATCH 01/19] feat(migrate): preserve opaque bodies as textual representations Co-Authored-By: jason.han --- internal/ir/lower/calc_body.go | 4 +- internal/translate/migrate/activity.go | 18 ++- internal/translate/migrate/behavior.go | 32 ++++- internal/translate/migrate/states.go | 14 +- internal/translate/migrate/values.go | 45 ++++++ tests/migrate/behavior_test.go | 6 +- tests/migrate/opaque_migration_test.go | 12 +- tests/migrate/opaque_textual_rep_test.go | 136 ++++++++++++++++++ tests/migrate/owner_reads_test.go | 2 +- tests/migrate/script_locals_test.go | 7 +- tests/migrate/states_test.go | 3 +- .../testdata/xmi/acquisition.golden.sysml | 9 ++ .../testdata/xmi/calc_context.golden.sysml | 1 + .../xmi/control_nodes.golden.report.txt | 10 +- .../testdata/xmi/control_nodes.golden.sysml | 8 +- .../control_nodes.layout.golden.report.txt | 10 +- .../xmi/control_nodes.layout.golden.sysml | 8 +- .../xmi/decision_else.golden.report.txt | 8 +- .../testdata/xmi/decision_else.golden.sysml | 4 +- ...ion_property_probability.golden.report.txt | 14 +- ...decision_property_probability.golden.sysml | 16 +-- ...cted_probability_default.golden.report.txt | 10 +- ..._rejected_probability_default.golden.sysml | 8 +- .../xmi/diagram_edges.golden.report.txt | 2 +- .../testdata/xmi/diagram_edges.golden.sysml | 4 +- .../diagram_edges.layout.golden.report.txt | 2 +- .../xmi/diagram_edges.layout.golden.sysml | 4 +- .../testdata/xmi/diagrams.golden.sysml | 3 + .../xmi/empty_behaviors.golden.report.txt | 10 +- .../testdata/xmi/empty_behaviors.golden.sysml | 20 +-- .../testdata/xmi/figures.golden.report.txt | 10 +- .../migrate/testdata/xmi/figures.golden.sysml | 8 +- .../xmi/figures.mdzip.golden.report.txt | 10 +- .../testdata/xmi/figures.mdzip.golden.sysml | 8 +- .../migrate/testdata/xmi/layout.golden.sysml | 1 + .../testdata/xmi/layout.layout.golden.sysml | 1 + .../testdata/xmi/meter.golden.report.txt | 30 ++-- tests/migrate/testdata/xmi/meter.golden.sysml | 82 +++++------ .../testdata/xmi/montecarlo_case.golden.sysml | 2 + .../testdata/xmi/nested_context.golden.sysml | 1 + .../xmi/object_flows.golden.report.txt | 8 +- .../testdata/xmi/object_flows.golden.sysml | 24 +++- .../xmi/parking_usecases.golden.report.txt | 8 +- .../xmi/parking_usecases.golden.sysml | 4 +- .../testdata/xmi/plant.golden.report.txt | 4 +- tests/migrate/testdata/xmi/plant.golden.sysml | 18 ++- .../testdata/xmi/plant_states.golden.sysml | 5 +- ...ility_nested_def_context.golden.report.txt | 10 +- ...robability_nested_def_context.golden.sysml | 8 +- ...ility_nested_def_default.golden.report.txt | 10 +- ...robability_nested_def_default.golden.sysml | 8 +- ...bability_nested_def_part.golden.report.txt | 10 +- .../probability_nested_def_part.golden.sysml | 8 +- ...nested_def_part_optional.golden.report.txt | 10 +- ...lity_nested_def_part_optional.golden.sysml | 8 +- ...y_nested_def_part_plural.golden.report.txt | 10 +- ...bility_nested_def_part_plural.golden.sysml | 8 +- ...nested_def_part_uncalled.golden.report.txt | 10 +- ...lity_nested_def_part_uncalled.golden.sysml | 8 +- ...probability_part_private.golden.report.txt | 10 +- .../xmi/probability_part_private.golden.sysml | 8 +- ...robability_part_shadowed.golden.report.txt | 12 +- .../probability_part_shadowed.golden.sysml | 12 +- .../testdata/xmi/reactor.golden.report.txt | 30 ++-- .../migrate/testdata/xmi/reactor.golden.sysml | 69 ++++----- .../xmi/script_guarded_reads.golden.sysml | 16 +++ .../xmi/script_locals.golden.report.txt | 4 +- .../testdata/xmi/script_locals.golden.sysml | 13 +- .../xmi/script_unset_reads.golden.sysml | 12 ++ .../testdata/xmi/station_points.golden.sysml | 24 ++++ .../xmi/submachine_params.golden.sysml | 1 + .../xmi/user_library.golden.report.txt | 8 +- .../testdata/xmi/user_library.golden.sysml | 1 - .../xmi/weighted_decision.golden.report.txt | 12 +- .../xmi/weighted_decision.golden.sysml | 12 +- 75 files changed, 606 insertions(+), 400 deletions(-) create mode 100644 tests/migrate/opaque_textual_rep_test.go diff --git a/internal/ir/lower/calc_body.go b/internal/ir/lower/calc_body.go index cb53efb25a..77ec115073 100644 --- a/internal/ir/lower/calc_body.go +++ b/internal/ir/lower/calc_body.go @@ -58,8 +58,8 @@ func calcStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { return nil, false } return usageStatement(m, scope) - case *ast.Definition, *ast.Documentation, *ast.Comment, *ast.Import, *ast.Alias: - // Declares a member of the calculation, not a step of it. + case *ast.Definition, *ast.Documentation, *ast.Comment, *ast.TextualRepresentation, *ast.Import, *ast.Alias: + // An annotation or declaration describes the calculation, not a step of it. return nil, false case *ast.SuccessionEdge: // A calculation body runs its steps in declaration order, so a diff --git a/internal/translate/migrate/activity.go b/internal/translate/migrate/activity.go index c3d9086c03..7150407f04 100644 --- a/internal/translate/migrate/activity.go +++ b/internal/translate/migrate/activity.go @@ -2293,17 +2293,25 @@ func (a *activity) unassigned(pin *sysmlv1.Element) bool { return pin.Parent != nil && pin.Parent.Type == "OpaqueAction" && !a.opaqueOf(pin.Parent).assigned[pin] } -// opaqueAction writes an opaque action as an action of assignments when its body is -// a sequence of them, else with the body kept as a comment; its pins stay unwritten. +// opaqueAction writes translated assignments and keeps every opaque body as text. func (a *activity) opaqueAction(n *sysmlv1.Element, name string) { r := a.opaqueOf(n) a.m.w.block(actionKw+name, func() { a.pins(n, nil) + a.m.textualReps(n) if !r.ok { a.inert[n] = true - body, lang := opaqueBody(n) - a.m.opaqueComment(body, lang, r.note) - a.m.add(n, Approximated, name, "the body is kept as a comment: "+r.note) + if r.note == "the body is empty" && opaqueBodiesEmpty(n) { + a.m.add(n, Mapped, name, "its body is empty, so it is written as an action that does nothing") + return + } + a.m.opaqueBodyComments(n, r.note) + _, lang := opaqueBody(n) + keptAs := "a textual representation, which is not executed" + if lang == "" { + keptAs = "a comment" + } + a.m.add(n, Approximated, name, "the body is kept as "+keptAs+": "+r.note) return } a.m.w.lines(r.lines) diff --git a/internal/translate/migrate/behavior.go b/internal/translate/migrate/behavior.go index 5479b93a06..1748c3daf4 100644 --- a/internal/translate/migrate/behavior.go +++ b/internal/translate/migrate/behavior.go @@ -894,24 +894,32 @@ func (m *migration) calcBody(e *sysmlv1.Element) { m.parameters(e, e) expr, _, _, translated := m.calcExprHow(e) _, lang := opaqueBody(e) + m.textualReps(e) m.w.line(expr) if lang != "" && !translated { m.downgrade(e, "the "+lang+" body is written verbatim as the result expression, since it is also v2 expression syntax") } } -// opaqueBehaviorBody writes an opaque or function behavior's body that is no single -// expression: as assignments when every statement is one, else as a comment. +// opaqueBehaviorBody writes assignments when possible and keeps every body as text. func (m *migration) opaqueBehaviorBody(e, scope *sysmlv1.Element) { m.unwrittenMembers(e) + m.textualReps(e) body, lang := opaqueBody(e) lines, ok, note := m.statements(body, lang, scope) if !ok { + if note == "the body is empty" && opaqueBodiesEmpty(e) { + return + } if r := m.resultRefusal(e); r != "" && r != note { note = "as the result expression, " + r + "; as statements, " + note } - m.opaqueComment(body, lang, note) - m.downgrade(e, "the body is kept as a comment: "+note) + m.opaqueBodyComments(e, note) + keptAs := "a textual representation, which is not executed" + if lang == "" { + keptAs = "a comment" + } + m.downgrade(e, "the body is kept as "+keptAs+": "+note) return } name := m.freshName(scope, "body") @@ -923,6 +931,22 @@ func (m *migration) opaqueBehaviorBody(e, scope *sysmlv1.Element) { } } +func (m *migration) textualReps(e *sysmlv1.Element) { + for _, pair := range opaqueBodyPairs(e) { + if pair.body != "" && pair.language != "" { + m.w.lines(prefixFirst("rep language "+stringLiteral(pair.language)+" ", commentLines(pair.body))) + } + } +} + +func (m *migration) opaqueBodyComments(e *sysmlv1.Element, note string) { + for _, pair := range opaqueBodyPairs(e) { + if pair.body != "" && pair.language == "" { + m.opaqueComment(pair.body, "", note) + } + } +} + // opaqueComment keeps an opaque body the mapping cannot write as a comment. func (m *migration) opaqueComment(body, lang, note string) { text := "body not migrated" diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index 4ed10fe3b2..3d3c4a5675 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -2203,13 +2203,15 @@ func (m *migration) writeInlineBody(kw string, b, owner *sysmlv1.Element, header case "OpaqueBehavior", "FunctionBehavior": body, lang := opaqueBody(b) lines, ok, note := m.statements(body, lang, b) + empty := !ok && note == "the body is empty" && opaqueBodiesEmpty(b) enclose(header, func() { m.comments(b) m.parameters(b, b) + m.textualReps(b) if ok { m.w.lines(lines) - } else { - m.opaqueComment(body, lang, note) + } else if !empty { + m.opaqueBodyComments(b, note) } if m.keeping != "" { m.w.line(m.keeping) @@ -2217,8 +2219,14 @@ func (m *migration) writeInlineBody(kw string, b, owner *sysmlv1.Element, header }) if ok { m.add(b, Approximated, m.v2Name(b), "the "+langName(lang)+" body is written as v2 assignments") + } else if empty { + m.add(b, Mapped, m.v2Name(b), "") } else { - m.add(b, Approximated, m.v2Name(b), "the body is kept as a comment: "+note) + keptAs := "a textual representation, which is not executed" + if lang == "" { + keptAs = "a comment" + } + m.add(b, Approximated, m.v2Name(b), "the body is kept as "+keptAs+": "+note) } return true } diff --git a/internal/translate/migrate/values.go b/internal/translate/migrate/values.go index 564d14d816..d4722d6212 100644 --- a/internal/translate/migrate/values.go +++ b/internal/translate/migrate/values.go @@ -350,6 +350,51 @@ func opaqueBody(v *sysmlv1.Element) (body, lang string) { return body, lang } +type opaqueBodyPair struct { + body string + language string +} + +func opaqueBodyPairs(v *sysmlv1.Element) []opaqueBodyPair { + bodies := opaqueTexts(v, "body") + languages := opaqueTexts(v, "language") + n := max(len(bodies), len(languages)) + pairs := make([]opaqueBodyPair, n) + for i := range pairs { + if i < len(bodies) { + pairs[i].body = bodies[i] + } + if i < len(languages) { + pairs[i].language = languages[i] + } + } + return pairs +} + +func opaqueTexts(v *sysmlv1.Element, role string) []string { + owned := v.Owned(role) + if len(owned) > 0 { + texts := make([]string, len(owned)) + for i, e := range owned { + texts[i] = strings.TrimSpace(e.Text) + } + return texts + } + if text := strings.TrimSpace(v.Attrs[role]); text != "" { + return []string{text} + } + return nil +} + +func opaqueBodiesEmpty(v *sysmlv1.Element) bool { + for _, pair := range opaqueBodyPairs(v) { + if pair.body != "" { + return false + } + } + return true +} + // reference is one name an expression refers to: the segments of a qualified // name, then the members feature chains select from it (chain is true). A // chain on a name the expression's own body declares (local) starts from that diff --git a/tests/migrate/behavior_test.go b/tests/migrate/behavior_test.go index 750aa1b1cf..5a444fa659 100644 --- a/tests/migrate/behavior_test.go +++ b/tests/migrate/behavior_test.go @@ -153,7 +153,7 @@ func TestActivityMigratesToAnExecutableActionDef(t *testing.T) { } { wantLine(t, r.Notation, line) } - wantNote(t, r, "_stamp", migrate.Approximated, "the body is kept as a comment") + wantNote(t, r, "_stamp", migrate.Approximated, "the body is kept as a textual representation, which is not executed") wantNote(t, r, "_e8", migrate.Approximated, "the guard [Focus lost] is kept as a comment and the edge written unguarded") wantNote(t, r, "_dcA", migrate.Approximated, "written as a fixed wait of 3.0 s before 'Point'") wantNote(t, r, "_dcB", migrate.Approximated, "written as a wait drawn uniformly over [1.0, 8.0] s before 'Focus'") @@ -889,7 +889,7 @@ func TestActivityWithSendAcceptAndOperationCalls(t *testing.T) { wantNote(t, r, "_twice", migrate.Mapped, "") wantNote(t, r, "_tr", migrate.Approximated, "the return parameter is written as an out parameter") wantNote(t, r, "_rcv", migrate.Approximated, "the reception has no method, so it only accepts the signal") - wantNote(t, r, "_js", migrate.Approximated, "the body is kept as a comment") + wantNote(t, r, "_js", migrate.Approximated, "the body is kept as a textual representation, which is not executed") wantNote(t, r, "_log", migrate.Approximated, "the pin 't' it passes for the parameter t of Station::Logging receives none: 'compute', which feeds it, produces no value; v1 runs the callee without the value, so the parameter is declared admitting none") wantNote(t, r, "_point", migrate.Mapped, "written as an action usage of Telescope, which a call on an object performs, so its body runs on the object and reaches its features") wantNote(t, r, "_callTgt", migrate.Mapped, "the call performs the usage point of the target this.tel") @@ -1001,7 +1001,7 @@ func TestNestedDefProbabilityReadsThroughContext(t *testing.T) { t.Run("held by a part a node of the body is named like", func(t *testing.T) { r := migrateFixtureFile(t, "probability_part_shadowed") wantLine(t, r.Notation, "part mission : Mission;") - wantLine(t, r.Notation, "action mission {") + wantLine(t, r.Notation, "action mission;") wantLine(t, r.Notation, "first 'decide' then a { @Stochastic::Probability { p = Sub::mission.pr; } }") wantLine(t, r.Notation, "first 'decide' then b { @Stochastic::Probability { p = 1.0 - Sub::mission.pr; } }") wantNote(t, r, "_ea", migrate.Mapped, "the probability reads the property pr of the object performing the action") diff --git a/tests/migrate/opaque_migration_test.go b/tests/migrate/opaque_migration_test.go index efb9df1dcc..a991ff1c67 100644 --- a/tests/migrate/opaque_migration_test.go +++ b/tests/migrate/opaque_migration_test.go @@ -281,8 +281,8 @@ func TestTranslatorRefusalsAndConfiguredClockName(t *testing.T) { "_mixed": `the construct "+" is outside the translated subset: string concatenation has no v2 form in the subset`, "_python": `the Python body is written as v2 assignments`, "_partial": `the call "label.trim" is not in the translated function table`, - "_root": `the body is kept as a comment: as the result expression, the types at "Math.sqrt(x)" disagree: the expression is a Real, not the Boolean wanted; as statements, the call "Math.sqrt" is not in the translated function table: a call is not a statement of the subset`, - "_split": `the body is kept as a comment: as the result expression, the behavior has 2 output parameters; one result expression can stand for none of them; as statements, the construct "x" is outside the translated subset: an expression that assigns nothing is not a statement of the subset`, + "_root": `the body is kept as a textual representation, which is not executed: as the result expression, the types at "Math.sqrt(x)" disagree: the expression is a Real, not the Boolean wanted; as statements, the call "Math.sqrt" is not in the translated function table: a call is not a statement of the subset`, + "_split": `the body is kept as a textual representation, which is not executed: as the result expression, the behavior has 2 output parameters; one result expression can stand for none of them; as statements, the construct "x" is outside the translated subset: an expression that assigns nothing is not a statement of the subset`, } { wantNote(t, r, id, migrate.Approximated, want) } @@ -391,8 +391,8 @@ func TestTranslatedOutputPinsFeedTheirFlows(t *testing.T) { wantNote(t, r, "_log", migrate.Approximated, "the JavaScript body is translated to v2; the console print print(…) is left out, as it writes to the tool's console and changes nothing of the model; the console print println(…) is left out, as it writes to the tool's console and changes nothing of the model") wantNote(t, r, "_shout", migrate.Approximated, "the JavaScript body is translated to v2; the console print println(…) is left out, as it writes to the tool's console and changes nothing of the model") wantLine(t, r.Notation, "assign ticks := ticks + 1;") - wantLine(t, r.Notation, "action shout;") - wantNoLine(t, r.Notation, "print(") + wantLine(t, r.Notation, "action shout {") + wantLine(t, r.Notation, `rep language "JavaScript" /* println("done"); */`) s := session(t, r) meta(t, s, "%instantiate Meter") @@ -435,7 +435,7 @@ func TestNonScalarFeaturesAndScriptLiterals(t *testing.T) { `/* default value not migrated: {JavaScript} Math.sqrt(total) — the types at "Math.sqrt(total)" disagree: the expression is a Real, not the Boolean wanted */`, "in limit : ScalarValues::Integer[1] default = count + 1;", "/* guard not migrated: [{JavaScript} ready and enabled] — the text \"and\" is not expression syntax: text follows the expression */", - "/* body not migrated (the text \"and\" is not expression syntax: a statement ends at `;` or a newline) {JavaScript}:", + `rep language "JavaScript" /* flag = ready and enabled */`, "assign flag := ready and enabled;", } { wantLine(t, r.Notation, line) @@ -507,7 +507,7 @@ func TestPluralPathsStayCollections(t *testing.T) { wantNote(t, r, "_peak", migrate.Mapped, "the JavaScript body is translated to v2") for id, want := range map[string]string{ "_spread": `the types at "+" disagree: an operand is a collection, not a number`, - "_reset": `the body is kept as a comment: the construct "cells.reading" is outside the translated subset: cells is a collection, so the assignment would write through several objects`, + "_reset": `the body is kept as a textual representation, which is not executed: the construct "cells.reading" is outside the translated subset: cells is a collection, so the assignment would write through several objects`, "_calib": `the construct "reading" is outside the translated subset: this.cells is a collection, so the assignment would write through several objects`, "_span": `the types at "total =" disagree: one side is a collection and the other a single value`, "_ticking": "its swimlane represents this.cells, a collection of objects, so none of them performs the call, which runs in the caller's context; the behavior acts on a Gauge through its parameter context, which is left unbound: the caller is a Meter, which is no Gauge and has 4 parts that are one, so no one of them is chosen; the behavior belongs to Gauge and runs here in the caller's context", diff --git a/tests/migrate/opaque_textual_rep_test.go b/tests/migrate/opaque_textual_rep_test.go new file mode 100644 index 0000000000..2e597ac9f3 --- /dev/null +++ b/tests/migrate/opaque_textual_rep_test.go @@ -0,0 +1,136 @@ +package migrate_test + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/translate/migrate" +) + +func opaqueActionDocument(t *testing.T, body string) *migrate.Result { + t.Helper() + return migrateDocument(t, ` + + + `+body+` + + + + `, "") +} + +func TestOpaqueActionTextualRepresentations(t *testing.T) { + t.Run("translated", func(t *testing.T) { + r := opaqueActionDocument(t, `JavaScriptx = 1;`) + wantLine(t, r.Notation, `rep language "JavaScript" /* x = 1; */`) + wantLine(t, r.Notation, "assign x := 1;") + wantClean(t, "t.sysml", r) + }) + + t.Run("untranslated", func(t *testing.T) { + r := opaqueActionDocument(t, `Javawork();`) + wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantNoLine(t, r.Notation, "body not migrated") + wantNote(t, r, "_opaque", migrate.Approximated, "the body is kept as a textual representation, which is not executed") + wantClean(t, "t.sysml", r) + }) + + t.Run("without language", func(t *testing.T) { + r := opaqueActionDocument(t, `not a statement`) + wantLine(t, r.Notation, "body not migrated") + wantLine(t, r.Notation, "not a statement") + wantNote(t, r, "_opaque", migrate.Approximated, "the body is kept as a comment") + wantClean(t, "t.sysml", r) + }) + + t.Run("pairs by index", func(t *testing.T) { + r := opaqueActionDocument(t, `Javafirst();Pythonsecond()`) + wantLine(t, r.Notation, `rep language "Java" /* first(); */`) + wantLine(t, r.Notation, `rep language "Python" /* second() */`) + wantNoLine(t, r.Notation, "body not migrated") + wantClean(t, "t.sysml", r) + }) + + t.Run("attribute fallback", func(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + `, "") + wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantClean(t, "t.sysml", r) + }) + + t.Run("empty", func(t *testing.T) { + r := opaqueActionDocument(t, "") + wantNoLine(t, r.Notation, "body not migrated") + wantNoLine(t, r.Notation, "rep language") + wantNote(t, r, "_opaque", migrate.Mapped, "its body is empty, so it is written as an action that does nothing") + wantClean(t, "t.sysml", r) + }) + + t.Run("comment terminator", func(t *testing.T) { + r := opaqueActionDocument(t, `Pythonprint("*/")`) + wantLine(t, r.Notation, `rep language "Python" /* print("* /") */`) + wantClean(t, "t.sysml", r) + }) +} + +func TestOpaqueBehaviorTextualRepresentations(t *testing.T) { + t.Run("action definition", func(t *testing.T) { + r := migrateDocument(t, ` + + Java + work(); + `, "") + wantLine(t, r.Notation, "action def Work {") + wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantNoLine(t, r.Notation, "body not migrated") + wantNote(t, r, "_behavior", migrate.Approximated, "the body is kept as a textual representation, which is not executed") + wantClean(t, "t.sysml", r) + }) + + t.Run("calculation definition", func(t *testing.T) { + r := migrateDocument(t, ` + + JavaScript + 1 + 2 + `, "") + wantLine(t, r.Notation, `rep language "JavaScript" /* 1 + 2 */`) + rep := strings.Index(string(r.Notation), `rep language "JavaScript"`) + result := strings.LastIndex(string(r.Notation), "1 + 2") + if rep < 0 || result < 0 || rep >= result { + t.Errorf("textual representation must precede the calc result:\n%s", r.Notation) + } + wantClean(t, "t.sysml", r) + }) + + t.Run("empty behaviors", func(t *testing.T) { + for _, kind := range []string{"OpaqueBehavior", "FunctionBehavior"} { + t.Run(kind, func(t *testing.T) { + r := migrateDocument(t, ` + `, "") + wantNoLine(t, r.Notation, "body not migrated") + wantNoLine(t, r.Notation, "rep language") + wantNote(t, r, "_behavior", migrate.Mapped, "") + wantClean(t, "t.sysml", r) + }) + } + }) + + t.Run("operation-written definition", func(t *testing.T) { + r := migrateDocument(t, ` + + + + Java + work(); + + `, ``) + wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantClean(t, "t.sysml", r) + }) +} diff --git a/tests/migrate/owner_reads_test.go b/tests/migrate/owner_reads_test.go index be48c66804..5873760b57 100644 --- a/tests/migrate/owner_reads_test.go +++ b/tests/migrate/owner_reads_test.go @@ -331,7 +331,7 @@ func TestLocalsABodyDeclaresAreNoReadsOfTheOwner(t *testing.T) { r := migrateDocument(t, localsModel, ``) for _, line := range []string{ "action def Compute {", - `the construct "let count" is outside the translated subset: count is already a feature here, which a declaration would shadow`, + `rep language "JavaScript" /* let count = 1; count += 1; */`, "action tally {", "attribute n : ScalarValues::Integer;", "assign count := n + 1;", diff --git a/tests/migrate/script_locals_test.go b/tests/migrate/script_locals_test.go index 248657a4a8..ec5d585364 100644 --- a/tests/migrate/script_locals_test.go +++ b/tests/migrate/script_locals_test.go @@ -28,7 +28,10 @@ func TestScriptAssignmentsDeclareUndeclaredNames(t *testing.T) { } wantNoLine(t, r.Notation, "attribute stepsOld") wantClean(t, "t.sysml", r) + wantLine(t, r.Notation, `rep language "JavaScript" /* t1 = simtime;`) + wantLine(t, r.Notation, `rep language "Java" /* tdiffOld = tdiff;`) + wantLine(t, r.Notation, `rep language "JavaScript" /* steps = steps + stepsOld;`) wantNote(t, r, "_diffTime", migrate.Approximated, "the JavaScript body is translated to v2; the clock variable simtime, named by the configuration Timing, reads the local clock; t1 is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it; tdiffOld is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it; the console print print(…) is left out, as it writes to the tool's console and changes nothing of the model; the JavaScript body is written as v2 assignments") - wantNote(t, r, "_javaDiff", migrate.Approximated, `the body is kept as a comment: the name "tdiffOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::javaDiff is called tdiffOld`) - wantNote(t, r, "_readFirst", migrate.Approximated, `the body is kept as a comment: the name "stepsOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::readFirst is called stepsOld`) + wantNote(t, r, "_javaDiff", migrate.Approximated, `the body is kept as a textual representation, which is not executed: the name "tdiffOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::javaDiff is called tdiffOld`) + wantNote(t, r, "_readFirst", migrate.Approximated, `the body is kept as a textual representation, which is not executed: the name "stepsOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::readFirst is called stepsOld`) } diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index 00269b9041..249f88c4eb 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -30,7 +30,8 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { "transition first Work.Run accept Stop then Idle;", "transition first Idle accept Resume then Work.Run;", "transition first Pause accept Resume", - "do action log { }", + "do action log {", + `rep language "JavaScript" /* println("resuming at " + count); */`, "then Work.last;", "transition first Idle accept Enter then Cell.warmStart;", "transition first Cell.spent then Idle;", diff --git a/tests/migrate/testdata/xmi/acquisition.golden.sysml b/tests/migrate/testdata/xmi/acquisition.golden.sysml index bc219ed125..4d1185e1d5 100644 --- a/tests/migrate/testdata/xmi/acquisition.golden.sysml +++ b/tests/migrate/testdata/xmi/acquisition.golden.sysml @@ -44,11 +44,13 @@ part def Observatory { } first stamp9 then 'start timer'; action 'start timer' { + rep language "JavaScript" /* Time_Acq_Total = simtime; */ assign Time_Acq_Total := localClock.currentTime; } first 'start timer' then seed; action seed { in Retries : ScalarValues::Integer[1] = 2; + rep language "JavaScript" /* GS_Found = Retries < 1; */ assign tcs.GS_Found := Retries < 1; } first seed then stamp; @@ -60,6 +62,9 @@ part def Observatory { } first stamp then 'first attempt'; action 'first attempt' { + rep language "JavaScript" /* i = 1; + * GS_Found = false; + */ assign tcs.i := 1; assign tcs.GS_Found := false; } @@ -115,6 +120,7 @@ part def Observatory { } first stamp4 then 'guide star found'; action 'guide star found' { + rep language "JavaScript" /* GS_Found = true; */ assign tcs.GS_Found := true; } first 'guide star found' then stamp2; @@ -128,6 +134,7 @@ part def Observatory { } first stamp2 then merge4; action 'next attempt' { + rep language "JavaScript" /* i += 1; */ assign tcs.i := tcs.i + 1; } first 'next attempt' then 'merge'; @@ -136,6 +143,7 @@ part def Observatory { } first stamp7 then abort; action abort { + rep language "JavaScript" /* GS_Found = false; */ assign tcs.GS_Found := false; } first abort then stamp8; @@ -149,6 +157,7 @@ part def Observatory { first fork2 then merge3; first fork2 then merge2; action 'stop timer' { + rep language "JavaScript" /* Time_Acq_Total = simtime - Time_Acq_Total; */ assign Time_Acq_Total := localClock.currentTime - Time_Acq_Total; } first 'stop timer' then stamp10; diff --git a/tests/migrate/testdata/xmi/calc_context.golden.sysml b/tests/migrate/testdata/xmi/calc_context.golden.sysml index 1bd4e30c88..307042d786 100644 --- a/tests/migrate/testdata/xmi/calc_context.golden.sysml +++ b/tests/migrate/testdata/xmi/calc_context.golden.sysml @@ -4,6 +4,7 @@ part def Tank { in ref context : Tank[1]; in factor : ScalarValues::Real[1]; out result : ScalarValues::Real[1]; + rep language "English" /* level * factor */ context.level * factor } } diff --git a/tests/migrate/testdata/xmi/control_nodes.golden.report.txt b/tests/migrate/testdata/xmi/control_nodes.golden.report.txt index b8b13e5df3..56df8aa51f 100644 --- a/tests/migrate/testdata/xmi/control_nodes.golden.report.txt +++ b/tests/migrate/testdata/xmi/control_nodes.golden.report.txt @@ -1,12 +1,8 @@ # SysML v1 to v2 migration report: control_nodes.xmi # exported by Example UML Tool -# migrated 20 element(s): 18 mapped, 2 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 20 element(s): 20 mapped, 0 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (2) -OpaqueAction Control::Controller::Run::cool _cn_cool -> cool (the body is kept as a comment: the body is empty) -OpaqueAction Control::Controller::Run::heat _cn_heat -> heat (the body is kept as a comment: the body is empty) - -## mapped (18) +## mapped (20) Package Control _pkg_control -> Control «Block» Class Control::Controller _blk_controller -> Control::Controller Activity Control::Controller::Run _act_run -> Control::Controller::Run @@ -24,4 +20,6 @@ InitialNode Control::Controller::Run:: _cn_init -> start JoinNode Control::Controller::Run:: _cn_join -> 'join' Diagram Control::Controller::Run::Run _diag_run -> Control::Controller::Run::Run (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Control::Controller::Run, whose graph the rendering draws with the 16 shown nodes and edges of it) DecisionNode Control::Controller::Run::check _cn_check -> check +OpaqueAction Control::Controller::Run::cool _cn_cool -> cool (its body is empty, so it is written as an action that does nothing) +OpaqueAction Control::Controller::Run::heat _cn_heat -> heat (its body is empty, so it is written as an action that does nothing) Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/control_nodes.golden.sysml b/tests/migrate/testdata/xmi/control_nodes.golden.sysml index 38a8ee0275..a4a1858010 100644 --- a/tests/migrate/testdata/xmi/control_nodes.golden.sysml +++ b/tests/migrate/testdata/xmi/control_nodes.golden.sysml @@ -9,13 +9,9 @@ package Control { fork 'fork'; succession 'fork to heat' first 'fork' then heat; succession 'fork to cool' first 'fork' then cool; - action heat { - /* body not migrated (the body is empty) */ - } + action heat; succession 'heat to join' first heat then 'join'; - action cool { - /* body not migrated (the body is empty) */ - } + action cool; succession 'cool to join' first cool then 'join'; join 'join'; succession 'join to check' first 'join' then check; diff --git a/tests/migrate/testdata/xmi/control_nodes.layout.golden.report.txt b/tests/migrate/testdata/xmi/control_nodes.layout.golden.report.txt index c54ea8de0f..c42fef120f 100644 --- a/tests/migrate/testdata/xmi/control_nodes.layout.golden.report.txt +++ b/tests/migrate/testdata/xmi/control_nodes.layout.golden.report.txt @@ -1,6 +1,6 @@ # SysML v1 to v2 migration report: control_nodes.xmi # exported by Example UML Tool -# migrated 20 element(s): 18 mapped, 2 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 1 of 1 diagrams from control_nodes.layout.xml: 6 elements positioned, 8 connectors routed, 0 styled, 0 notes +# migrated 20 element(s): 20 mapped, 0 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 1 of 1 diagrams from control_nodes.layout.xml: 6 elements positioned, 8 connectors routed, 0 styled, 0 notes ## layout # source: control_nodes.layout.xml (2022x v1.0.0, cameo 2022x Refresh2, exported 2025-06-12T21:38:50.779458500Z) @@ -9,11 +9,7 @@ # styles: 0 of 0 written; notes: 0 written (0 anchored, 0 freed of an anchor the view does not lay out) # routes of ControlFlow: 8 written -## approximated (2) -OpaqueAction Control::Controller::Run::cool _cn_cool -> cool (the body is kept as a comment: the body is empty) -OpaqueAction Control::Controller::Run::heat _cn_heat -> heat (the body is kept as a comment: the body is empty) - -## mapped (18) +## mapped (20) Package Control _pkg_control -> Control «Block» Class Control::Controller _blk_controller -> Control::Controller Activity Control::Controller::Run _act_run -> Control::Controller::Run @@ -31,4 +27,6 @@ InitialNode Control::Controller::Run:: _cn_init -> start JoinNode Control::Controller::Run:: _cn_join -> 'join' Diagram Control::Controller::Run::Run _diag_run -> Control::Controller::Run::Run (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Control::Controller::Run, whose graph the rendering draws with the 16 shown nodes and edges of it; laid out from control_nodes.layout.xml: 6 of 8 shown elements positioned (2 not exposed), 8 of 8 connectors routed) DecisionNode Control::Controller::Run::check _cn_check -> check +OpaqueAction Control::Controller::Run::cool _cn_cool -> cool (its body is empty, so it is written as an action that does nothing) +OpaqueAction Control::Controller::Run::heat _cn_heat -> heat (its body is empty, so it is written as an action that does nothing) Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/control_nodes.layout.golden.sysml b/tests/migrate/testdata/xmi/control_nodes.layout.golden.sysml index b986a0f31b..6ffed5b7da 100644 --- a/tests/migrate/testdata/xmi/control_nodes.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/control_nodes.layout.golden.sysml @@ -24,13 +24,9 @@ package Control { fork 'fork'; succession 'fork to heat' first 'fork' then heat; succession 'fork to cool' first 'fork' then cool; - action heat { - /* body not migrated (the body is empty) */ - } + action heat; succession 'heat to join' first heat then 'join'; - action cool { - /* body not migrated (the body is empty) */ - } + action cool; succession 'cool to join' first cool then 'join'; join 'join'; succession 'join to check' first 'join' then check; diff --git a/tests/migrate/testdata/xmi/decision_else.golden.report.txt b/tests/migrate/testdata/xmi/decision_else.golden.report.txt index 064b353d2e..8f9bd98221 100644 --- a/tests/migrate/testdata/xmi/decision_else.golden.report.txt +++ b/tests/migrate/testdata/xmi/decision_else.golden.report.txt @@ -1,11 +1,8 @@ # SysML v1 to v2 migration report: decision_else.xmi # exported by Example UML Tool -# migrated 15 element(s): 14 mapped, 1 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 15 element(s): 15 mapped, 0 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (1) -OpaqueAction Recovery::Retrier::Recover::retry _de_retry -> retry (the body is kept as a comment: the body is empty) - -## mapped (14) +## mapped (15) Model Model _m (the root model's members are written at the top level) Package Recovery _pkg -> Recovery «Block» Class Recovery::Retrier _blk_retrier -> Recovery::Retrier @@ -19,4 +16,5 @@ InitialNode Recovery::Retrier::Recover:: _de_init -> start DecisionNode Recovery::Retrier::Recover::decide _de_decide -> 'decide' Comment Recovery::Retrier::Recover::decide:: _de_cmt MergeNode Recovery::Retrier::Recover::merge _de_merge -> 'merge' +OpaqueAction Recovery::Retrier::Recover::retry _de_retry -> retry (its body is empty, so it is written as an action that does nothing) Property Recovery::Retrier::attempts _attempts -> Recovery::Retrier::attempts diff --git a/tests/migrate/testdata/xmi/decision_else.golden.sysml b/tests/migrate/testdata/xmi/decision_else.golden.sysml index 8b326a9a48..25492ffb78 100644 --- a/tests/migrate/testdata/xmi/decision_else.golden.sysml +++ b/tests/migrate/testdata/xmi/decision_else.golden.sysml @@ -7,9 +7,7 @@ package Recovery { else 'merge'; comment /* Retries until the budget is spent. */ first 'decide' if attempts < 3 then retry; - action retry { - /* body not migrated (the body is empty) */ - } + action retry; first retry then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/decision_property_probability.golden.report.txt b/tests/migrate/testdata/xmi/decision_property_probability.golden.report.txt index 430957d4af..78b567ce65 100644 --- a/tests/migrate/testdata/xmi/decision_property_probability.golden.report.txt +++ b/tests/migrate/testdata/xmi/decision_property_probability.golden.report.txt @@ -1,15 +1,11 @@ # SysML v1 to v2 migration report: decision_property_probability.xmi -# migrated 20 element(s): 14 mapped, 6 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 20 element(s): 18 mapped, 2 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (6) +## approximated (2) ControlFlow Chooser::Route:: _eRemainder (the edge carries no «Probability»: it is weighted 1.0 - pDefault, its share of what the marked branches leave of 1, read when the decision is reached) ControlFlow Chooser::Route:: _eMissingRemainder (the edge carries no «Probability»: it is weighted 1.0 - pMissing, its share of what the marked branches leave of 1, read when the decision is reached) -OpaqueAction Chooser::Route::defaulted _defaulted -> defaulted (the body is kept as a comment: the body is empty) -OpaqueAction Chooser::Route::missing default _missingDefault -> 'missing default' (the body is kept as a comment: the body is empty) -OpaqueAction Chooser::Route::missing remainder _missingRemainder -> 'missing remainder' (the body is kept as a comment: the body is empty) -OpaqueAction Chooser::Route::remainder _remainder -> remainder (the body is kept as a comment: the body is empty) -## mapped (14) +## mapped (18) «Block» Class Chooser _chooser -> Chooser Activity Chooser::Route _route -> Chooser::route (written as an action usage every object of Chooser performs from creation, so its body runs on the object and reaches its features) ActivityFinalNode Chooser::Route:: _done -> final @@ -19,6 +15,10 @@ ControlFlow Chooser::Route:: _next «Probability» ControlFlow Chooser::Route:: _eMissing (the probability reads the property pMissing of the object performing the action, so a run checks it lies in [0, 1] and the branches sum to 1 when the decision is reached) ControlFlow Chooser::Route:: _finishMissing InitialNode Chooser::Route:: _initial -> start +OpaqueAction Chooser::Route::defaulted _defaulted -> defaulted (its body is empty, so it is written as an action that does nothing) +OpaqueAction Chooser::Route::missing default _missingDefault -> 'missing default' (its body is empty, so it is written as an action that does nothing) +OpaqueAction Chooser::Route::missing remainder _missingRemainder -> 'missing remainder' (its body is empty, so it is written as an action that does nothing) +OpaqueAction Chooser::Route::remainder _remainder -> remainder (its body is empty, so it is written as an action that does nothing) DecisionNode Chooser::Route::with default _withDefault -> 'with default' DecisionNode Chooser::Route::without default _withoutDefault -> 'without default' Property Chooser::pDefault _default -> Chooser::pDefault diff --git a/tests/migrate/testdata/xmi/decision_property_probability.golden.sysml b/tests/migrate/testdata/xmi/decision_property_probability.golden.sysml index 3f374c10f5..9472934ba5 100644 --- a/tests/migrate/testdata/xmi/decision_property_probability.golden.sysml +++ b/tests/migrate/testdata/xmi/decision_property_probability.golden.sysml @@ -6,24 +6,16 @@ part def Chooser { decide 'with default'; first 'with default' then defaulted { @Stochastic::Probability { p = pDefault; } } first 'with default' then remainder { @Stochastic::Probability { p = 1.0 - pDefault; } } - action defaulted { - /* body not migrated (the body is empty) */ - } + action defaulted; first defaulted then 'merge'; - action remainder { - /* body not migrated (the body is empty) */ - } + action remainder; first remainder then 'without default'; decide 'without default'; first 'without default' then 'missing default' { @Stochastic::Probability { p = pMissing; } } first 'without default' then 'missing remainder' { @Stochastic::Probability { p = 1.0 - pMissing; } } - action 'missing default' { - /* body not migrated (the body is empty) */ - } + action 'missing default'; first 'missing default' then 'merge'; - action 'missing remainder' { - /* body not migrated (the body is empty) */ - } + action 'missing remainder'; merge 'merge'; first 'merge' then final; action final terminate; diff --git a/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.report.txt b/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.report.txt index 01d06b976c..ff3384d98e 100644 --- a/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.report.txt +++ b/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.report.txt @@ -1,11 +1,7 @@ # SysML v1 to v2 migration report: decision_rejected_probability_default.xmi -# migrated 14 element(s): 12 mapped, 2 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 14 element(s): 14 mapped, 0 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (2) -OpaqueAction Chooser::Route::defaulted _defaulted -> defaulted (the body is kept as a comment: the body is empty) -OpaqueAction Chooser::Route::missing _missingBranch -> missing (the body is kept as a comment: the body is empty) - -## mapped (12) +## mapped (14) «Block» Class Chooser _chooser -> Chooser Activity Chooser::Route _route -> Chooser::route (written as an action usage every object of Chooser performs from creation, so its body runs on the object and reaches its features) ActivityFinalNode Chooser::Route:: _final -> final @@ -15,6 +11,8 @@ ControlFlow Chooser::Route:: _finishDefault ControlFlow Chooser::Route:: _finishMissing InitialNode Chooser::Route:: _initial -> start DecisionNode Chooser::Route::decide _decide -> 'decide' +OpaqueAction Chooser::Route::defaulted _defaulted -> defaulted (its body is empty, so it is written as an action that does nothing) +OpaqueAction Chooser::Route::missing _missingBranch -> missing (its body is empty, so it is written as an action that does nothing) Property Chooser::pDefault _default -> Chooser::pDefault Property Chooser::pMissing _missing -> Chooser::pMissing Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.sysml b/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.sysml index b3349df836..965cb3a712 100644 --- a/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.sysml +++ b/tests/migrate/testdata/xmi/decision_rejected_probability_default.golden.sysml @@ -6,13 +6,9 @@ part def Chooser { decide 'decide'; first 'decide' then defaulted { @Stochastic::Probability { p = pDefault; } } first 'decide' if pMissing > 0.0 then missing { @Stochastic::Probability { p = pMissing; } } - action defaulted { - /* body not migrated (the body is empty) */ - } + action defaulted; first defaulted then 'merge'; - action missing { - /* body not migrated (the body is empty) */ - } + action missing; first missing then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt index 4e58721006..ff25f560c6 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt @@ -4,7 +4,7 @@ ## approximated (9) Transition Behavior::Modes:::: _t_restart -> Behavior::Modes::'Halted accept Go then Running' (written as 2 transitions, one per trigger) -OpaqueBehavior Behavior::Modes::::Running::spin _st_running_do -> Behavior::Modes::Running::spin (the body is kept as a comment: the statement "keep turning" is not an assignment of a v2 expression (language English)) +OpaqueBehavior Behavior::Modes::::Running::spin _st_running_do -> Behavior::Modes::Running::spin (the body is kept as a textual representation, which is not executed: the statement "keep turning" is not an assignment of a v2 expression (language English)) Package Requirements _pkg_reqs -> RequirementsModel (written as RequirementsModel since a root package named Requirements would be hidden by the standard library's Requirements) «Satisfy» Abstraction Requirements:: _dep_satisfy_pairs (written as 2 relationships, one per client–supplier pair) «TestCase» Activity Requirements::Mass Test _tc_mass -> RequirementsModel::'Mass Test' (the test case's behavior is not migrated; only its verified requirements are) diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml index 6402734d41..9002b34214 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml @@ -86,9 +86,7 @@ package Behavior { state Idle; state Running { do action spin { - /* body not migrated (the statement "keep turning" is not an assignment of a v2 expression (language English)) {English}: - * keep turning - */ + rep language "English" /* keep turning */ } } state Halted; diff --git a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt index 869eb2e6b4..a5d74404de 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt @@ -25,7 +25,7 @@ ## approximated (9) Transition Behavior::Modes:::: _t_restart -> Behavior::Modes::'Halted accept Go then Running' (written as 2 transitions, one per trigger) -OpaqueBehavior Behavior::Modes::::Running::spin _st_running_do -> Behavior::Modes::Running::spin (the body is kept as a comment: the statement "keep turning" is not an assignment of a v2 expression (language English)) +OpaqueBehavior Behavior::Modes::::Running::spin _st_running_do -> Behavior::Modes::Running::spin (the body is kept as a textual representation, which is not executed: the statement "keep turning" is not an assignment of a v2 expression (language English)) Package Requirements _pkg_reqs -> RequirementsModel (written as RequirementsModel since a root package named Requirements would be hidden by the standard library's Requirements) «Satisfy» Abstraction Requirements:: _dep_satisfy_pairs (written as 2 relationships, one per client–supplier pair) «TestCase» Activity Requirements::Mass Test _tc_mass -> RequirementsModel::'Mass Test' (the test case's behavior is not migrated; only its verified requirements are) diff --git a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml index 933da07501..e4169bfecb 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml @@ -114,9 +114,7 @@ package Behavior { state Idle; state Running { do action spin { - /* body not migrated (the statement "keep turning" is not an assignment of a v2 expression (language English)) {English}: - * keep turning - */ + rep language "English" /* keep turning */ } } state Halted; diff --git a/tests/migrate/testdata/xmi/diagrams.golden.sysml b/tests/migrate/testdata/xmi/diagrams.golden.sysml index 6e1b0e3c26..0c1d67e23a 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.sysml +++ b/tests/migrate/testdata/xmi/diagrams.golden.sysml @@ -33,6 +33,7 @@ package Structure { } succession 'start to ignite' first start then ignite; action ignite { + rep language "JavaScript" /* ignited = true; */ attribute ignited : ScalarValues::Boolean; assign ignited := true; } @@ -47,6 +48,7 @@ package Structure { expose Engine; render Views::asTextualNotation; } + rep language "JavaScript" /* ignited = false; */ first start then body; action body { attribute ignited : ScalarValues::Boolean; @@ -92,6 +94,7 @@ package Behavior { } succession 'start to start engine' first start then 'start engine'; action 'start engine' { + rep language "JavaScript" /* running = true; */ attribute running : ScalarValues::Boolean; assign running := true; } diff --git a/tests/migrate/testdata/xmi/empty_behaviors.golden.report.txt b/tests/migrate/testdata/xmi/empty_behaviors.golden.report.txt index 0c81c69685..baeeeeb8d5 100644 --- a/tests/migrate/testdata/xmi/empty_behaviors.golden.report.txt +++ b/tests/migrate/testdata/xmi/empty_behaviors.golden.report.txt @@ -15,14 +15,14 @@ StateMachine Gate::Blink _blink -> Gate::Blink ('Warm' names it as its entry act StateMachine Gate::Cycle _cycle -> Gate::Cycle (the classifier behavior is run by every object of Gate as its usage cycle) Transition Gate::Cycle::main:: _tStopOff (its do action Gate::Blink is not run: it is written as a state def) Activity Gate::Cycle::main::::effect _effGoCool -> Gate::Cycle::unnamed2::effect (written as the do action of (_tGoCool); none of its 1 action is migrated, so it runs nothing) -OpaqueBehavior Gate::Cycle::main::::effect _effGoWarm -> Gate::Cycle::unnamed3::effect (the body is kept as a comment: the statement "CALL WARM(count)" is not an assignment of a v2 expression (language Fortran)) +OpaqueBehavior Gate::Cycle::main::::effect _effGoWarm -> Gate::Cycle::unnamed3::effect (the body is kept as a textual representation, which is not executed: the statement "CALL WARM(count)" is not an assignment of a v2 expression (language Fortran)) Activity Gate::Cycle::main::::effect _effColdOff -> Gate::Cycle::unnamed5::effect (written as the do action of (_tColdOff); none of its 1 action is migrated, so it runs nothing) -OpaqueBehavior Gate::Cycle::main::::effect _effOffIdle -> Gate::Cycle::unnamed6::effect (the body is kept as a comment: the statement "CALL RESTART(count)" is not an assignment of a v2 expression (language Fortran)) +OpaqueBehavior Gate::Cycle::main::::effect _effOffIdle -> Gate::Cycle::unnamed6::effect (the body is kept as a textual representation, which is not executed: the statement "CALL RESTART(count)" is not an assignment of a v2 expression (language Fortran)) Activity Gate::Cycle::main::Busy::open _busyEntry -> Gate::Cycle::Busy::open (written as the entry action of 'Busy'; none of its 2 actions is migrated, so it runs nothing) -OpaqueBehavior Gate::Cycle::main::Busy::poll _busyDo -> Gate::Cycle::Busy::poll (the body is kept as a comment: the statement "CALL POLL(count)" is not an assignment of a v2 expression (language Fortran)) +OpaqueBehavior Gate::Cycle::main::Busy::poll _busyDo -> Gate::Cycle::Busy::poll (the body is kept as a textual representation, which is not executed: the statement "CALL POLL(count)" is not an assignment of a v2 expression (language Fortran)) Activity Gate::Cycle::main::Cool::seal _coolExit -> Gate::Cycle::Cool::seal (written as the exit action of 'Cool'; none of its 1 action is migrated, so it runs nothing) -OpaqueBehavior Gate::Cycle::main::Cool::vent _coolEntry -> Gate::Cycle::Cool::vent (the body is kept as a comment: the statement "CALL VENT(count)" is not an assignment of a v2 expression (language Fortran)) -OpaqueBehavior Gate::Cycle::main::Idle::flush _idleExit -> Gate::Cycle::Idle::flush (the body is kept as a comment: the statement "CALL FLUSH(count)" is not an assignment of a v2 expression (language Fortran)) +OpaqueBehavior Gate::Cycle::main::Cool::vent _coolEntry -> Gate::Cycle::Cool::vent (the body is kept as a textual representation, which is not executed: the statement "CALL VENT(count)" is not an assignment of a v2 expression (language Fortran)) +OpaqueBehavior Gate::Cycle::main::Idle::flush _idleExit -> Gate::Cycle::Idle::flush (the body is kept as a textual representation, which is not executed: the statement "CALL FLUSH(count)" is not an assignment of a v2 expression (language Fortran)) Activity Gate::Cycle::main::Idle::spin _idleDo -> Gate::Cycle::Idle::spin (written as the do action of 'Idle'; none of its 1 action is migrated, so it runs nothing) State Gate::Cycle::main::Warm _warm -> Warm (its entry action Gate::Blink is not run: it is written as a state def) Activity Gate::Reset _reset -> Gate::Reset (also run as the do action of (_tGoHot); also run as the do action of (_tHotIdle)) diff --git a/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml b/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml index 2b655aa41c..3843a18656 100644 --- a/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml +++ b/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml @@ -19,9 +19,7 @@ part def Gate { } } exit action flush { - /* body not migrated (the statement "CALL FLUSH(count)" is not an assignment of a v2 expression (language Fortran)) {Fortran}: - * CALL FLUSH(count) - */ + rep language "Fortran" /* CALL FLUSH(count) */ } } state Busy { @@ -40,17 +38,13 @@ part def Gate { metadata MigrationMetadata::StandIn about 'fork'; } do action poll { - /* body not migrated (the statement "CALL POLL(count)" is not an assignment of a v2 expression (language Fortran)) {Fortran}: - * CALL POLL(count) - */ + rep language "Fortran" /* CALL POLL(count) */ } exit action close; } state Cool { entry action vent { - /* body not migrated (the statement "CALL VENT(count)" is not an assignment of a v2 expression (language Fortran)) {Fortran}: - * CALL VENT(count) - */ + rep language "Fortran" /* CALL VENT(count) */ } do action wait; exit action seal { @@ -79,9 +73,7 @@ part def Gate { then Cool; transition first Cool accept Go do action effect { - /* body not migrated (the statement "CALL WARM(count)" is not an assignment of a v2 expression (language Fortran)) {Fortran}: - * CALL WARM(count) - */ + rep language "Fortran" /* CALL WARM(count) */ } then Warm; transition first Warm @@ -97,9 +89,7 @@ part def Gate { then Off; transition first Off do action effect { - /* body not migrated (the statement "CALL RESTART(count)" is not an assignment of a v2 expression (language Fortran)) {Fortran}: - * CALL RESTART(count) - */ + rep language "Fortran" /* CALL RESTART(count) */ } then Idle; transition first Off accept Go diff --git a/tests/migrate/testdata/xmi/figures.golden.report.txt b/tests/migrate/testdata/xmi/figures.golden.report.txt index 1914fdaf06..fd2ae1474b 100644 --- a/tests/migrate/testdata/xmi/figures.golden.report.txt +++ b/tests/migrate/testdata/xmi/figures.golden.report.txt @@ -1,12 +1,12 @@ # SysML v1 to v2 migration report: figures.xmi # exported by Example UML Tool -# migrated 90 element(s): 70 mapped, 18 approximated, 2 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 90 element(s): 72 mapped, 16 approximated, 2 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## unmapped (2) «CollectThingsOnDiagram» CallBehaviorAction Plant Viewpoints::Shown Viewpoint::Shown Method::Collect Things On Diagram _st_shown_collect (it collects what the SysML Block Definition Diagram 'Partial' shows beyond the 1 element the tool lists, whose symbols cannot be read and what the SysML Block Definition Diagram 'Unlisted' shows, which the archive does not record) «TableStructure» StructuredActivityNode Plant Viewpoints::Shown Viewpoint::Shown Method::Shown Blocks _st_shown_table (the elements it shows pass through «CollectThingsOnDiagram» Plant Viewpoints::Shown Viewpoint::Shown Method::Collect Things On Diagram is not migrated: it collects what the SysML Block Definition Diagram 'Partial' shows beyond the 1 element the tool lists, whose symbols cannot be read and what the SysML Block Definition Diagram 'Unlisted' shows, which the archive does not record) -## approximated (18) +## approximated (16) «Document» Class Plant Documents::Plant Handbook _doc_plant -> 'Plant Documents'::'Plant Handbook' (a plain UML class without «Block» is written as a part def) Activity Plant Viewpoints::Figures Viewpoint::Figures Method _act_figures -> 'Plant Viewpoints'::'Figures Viewpoint'::'Figures Method' (the classifier behavior is run by every object of Plant Viewpoints::Figures Viewpoint as its usage figures Method) «Image» CallBehaviorAction Plant Viewpoints::Figures Viewpoint::Figures Method::Image _figures_image -> Image (a step with no behavior and no duration; it passes the token on) @@ -21,12 +21,10 @@ Activity Plant Viewpoints::Shown Viewpoint::Shown Method _act_shown -> 'Plant Vi «TableAttributeColumn» CallBehaviorAction Plant Viewpoints::Shown Viewpoint::Shown Method::Shown Blocks::Name _shown_col_name -> Name (a step with no behavior and no duration; it passes the token on) Diagram Plant::Blank _diag_blank -> Plant::Blank (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; the diagram shows nothing; the view exposes nothing) Diagram Plant::Poster _diag_poster -> Plant::Poster (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; the diagram shows nothing; the view exposes nothing) -OpaqueAction Plant::Pump::Prime::Fill _prime_fill -> Fill (the body is kept as a comment: the body is empty) -OpaqueAction Plant::Pump::Prime::Vent _prime_vent -> Vent (the body is kept as a comment: the body is empty) Diagram Plant::Silent _diag_silent -> Plant::Silent (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; the diagram shows nothing; the view exposes nothing) Diagram Plant::Unlisted _diag_unlisted -> Plant::Unlisted (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; the diagram shows nothing; the view exposes nothing) -## mapped (70) +## mapped (72) Model Model _m (the root model's members are written at the top level) Package Plant _pkg_plant -> Plant Package Plant Documents _pkg_docs -> 'Plant Documents' @@ -92,7 +90,9 @@ ActivityFinalNode Plant::Pump::Prime:: _prime_final -> final ControlFlow Plant::Pump::Prime:: _prime_e2 ControlFlow Plant::Pump::Prime:: _prime_e3 InitialNode Plant::Pump::Prime:: _prime_init -> start +OpaqueAction Plant::Pump::Prime::Fill _prime_fill -> Fill (its body is empty, so it is written as an action that does nothing) Diagram Plant::Pump::Prime::Priming _diag_prime -> Plant::Pump::Prime::Priming (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Plant::Pump::Prime, whose graph the rendering draws with the 2 shown nodes and edges of it) +OpaqueAction Plant::Pump::Prime::Vent _prime_vent -> Vent (its body is empty, so it is written as an action that does nothing) Diagram Plant::Roster _diag_roster -> Plant::Roster (a Generic Table written as a view rendered asElementTable) Diagram Plant::Stale _diag_stale -> Plant::Stale (a SysML Block Definition Diagram written as a view rendered asTreeDiagram) Signal Plant::Start _sig_start -> Plant::Start diff --git a/tests/migrate/testdata/xmi/figures.golden.sysml b/tests/migrate/testdata/xmi/figures.golden.sysml index 17ead25abe..1c5f856813 100644 --- a/tests/migrate/testdata/xmi/figures.golden.sysml +++ b/tests/migrate/testdata/xmi/figures.golden.sysml @@ -19,13 +19,9 @@ package Plant { render Views::asInterconnectionDiagram; } first start then Fill; - action Fill { - /* body not migrated (the body is empty) */ - } + action Fill; first Fill then Vent; - action Vent { - /* body not migrated (the body is empty) */ - } + action Vent; first Vent then final; action final terminate; metadata MigrationMetadata::SynthesizedName about final; diff --git a/tests/migrate/testdata/xmi/figures.mdzip.golden.report.txt b/tests/migrate/testdata/xmi/figures.mdzip.golden.report.txt index 5a09210fe9..da7b333625 100644 --- a/tests/migrate/testdata/xmi/figures.mdzip.golden.report.txt +++ b/tests/migrate/testdata/xmi/figures.mdzip.golden.report.txt @@ -1,6 +1,6 @@ # SysML v1 to v2 migration report: figures.mdzip # exported by Example UML Tool -# migrated 90 element(s): 74 mapped, 15 approximated, 1 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 7 of 9 diagrams from the diagrams' own symbol streams: 7 elements positioned, 2 connectors routed, 2 styled, 4 notes; wrote 1 image file(s) +# migrated 90 element(s): 76 mapped, 13 approximated, 1 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 7 of 9 diagrams from the diagrams' own symbol streams: 7 elements positioned, 2 connectors routed, 2 styled, 4 notes; wrote 1 image file(s) ## layout # source: the diagrams' own symbol streams @@ -15,7 +15,7 @@ ## unmapped (1) ImageShape Plant::Pump::Modes::Pump Modes _sym_torn (the pasted image's bytes do not read: octet 19 is "xx", not a hexadecimal byte) -## approximated (15) +## approximated (13) «Document» Class Plant Documents::Plant Handbook _doc_plant -> 'Plant Documents'::'Plant Handbook' (a plain UML class without «Block» is written as a part def) Activity Plant Viewpoints::Figures Viewpoint::Figures Method _act_figures -> 'Plant Viewpoints'::'Figures Viewpoint'::'Figures Method' (the classifier behavior is run by every object of Plant Viewpoints::Figures Viewpoint as its usage figures Method) «Image» CallBehaviorAction Plant Viewpoints::Figures Viewpoint::Figures Method::Image _figures_image -> Image (a step with no behavior and no duration; it passes the token on) @@ -27,12 +27,10 @@ Activity Plant Viewpoints::Shown Viewpoint::Shown Method _act_shown -> 'Plant Vi «TableAttributeColumn» CallBehaviorAction Plant Viewpoints::Shown Viewpoint::Shown Method::Shown Blocks::Name _shown_col_name -> Name (a step with no behavior and no duration; it passes the token on) Diagram Plant::Blank _diag_blank -> Plant::Blank (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; the diagram draws nothing at all; the view exposes nothing) Diagram Plant::Pump::Modes::Pump Modes _diag_modes -> Plant::Pump::Modes::'Pump Modes' (a SysML State Machine Diagram written as a view rendered asInterconnectionDiagram of StateTransitionView; the view exposes state def Plant::Pump::Modes, whose graph the rendering draws with the 5 shown nodes and edges of it; laid out from the diagram's own symbol stream: 2 of 3 shown elements positioned (1 not exposed), 2 of 2 connectors routed, 2 pasted images written as images/Plant_from_the_north.png, 2 pasted images not written: the pasted image "logo.png" is not in the archive and the pasted image of symbol _sym_torn has bytes that do not read (octet 19 is "xx", not a hexadecimal byte), 2 of 2 symbols drawn in their own colours or font styled, 3 notes written, 2 anchored, free symbols not represented: 2 ImageShape) -OpaqueAction Plant::Pump::Prime::Fill _prime_fill -> Fill (the body is kept as a comment: the body is empty) -OpaqueAction Plant::Pump::Prime::Vent _prime_vent -> Vent (the body is kept as a comment: the body is empty) Diagram Plant::Roster _diag_roster -> Plant::Roster (a Generic Table written as a view rendered asElementTable; laid out from the diagram's own symbol stream: 1 of 1 shown elements positioned, 0 of 0 connectors routed, 1 pasted image written as images/Plant_from_the_north.png, which a view rendered asElementTable does not draw) Diagram Plant::Silent _diag_silent -> Plant::Silent (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; the diagram shows nothing; the view exposes nothing) -## mapped (74) +## mapped (76) Model Model _m (the root model's members are written at the top level) Package Plant _pkg_plant -> Plant Package Plant Documents _pkg_docs -> 'Plant Documents' @@ -102,7 +100,9 @@ ActivityFinalNode Plant::Pump::Prime:: _prime_final -> final ControlFlow Plant::Pump::Prime:: _prime_e2 ControlFlow Plant::Pump::Prime:: _prime_e3 InitialNode Plant::Pump::Prime:: _prime_init -> start +OpaqueAction Plant::Pump::Prime::Fill _prime_fill -> Fill (its body is empty, so it is written as an action that does nothing) Diagram Plant::Pump::Prime::Priming _diag_prime -> Plant::Pump::Prime::Priming (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Plant::Pump::Prime, whose graph the rendering draws with the 2 shown nodes and edges of it) +OpaqueAction Plant::Pump::Prime::Vent _prime_vent -> Vent (its body is empty, so it is written as an action that does nothing) Diagram Plant::Stale _diag_stale -> Plant::Stale (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; laid out from the diagram's own symbol stream: 1 of 1 shown elements positioned, 0 of 0 connectors routed) Signal Plant::Start _sig_start -> Plant::Start «Block» Class Plant::Tank _blk_tank -> Plant::Tank diff --git a/tests/migrate/testdata/xmi/figures.mdzip.golden.sysml b/tests/migrate/testdata/xmi/figures.mdzip.golden.sysml index 3f48cb6317..cc8b3dc679 100644 --- a/tests/migrate/testdata/xmi/figures.mdzip.golden.sysml +++ b/tests/migrate/testdata/xmi/figures.mdzip.golden.sysml @@ -30,13 +30,9 @@ package Plant { render Views::asInterconnectionDiagram; } first start then Fill; - action Fill { - /* body not migrated (the body is empty) */ - } + action Fill; first Fill then Vent; - action Vent { - /* body not migrated (the body is empty) */ - } + action Vent; first Vent then final; action final terminate; metadata MigrationMetadata::SynthesizedName about final; diff --git a/tests/migrate/testdata/xmi/layout.golden.sysml b/tests/migrate/testdata/xmi/layout.golden.sysml index 75e80d2069..f29ab16b5f 100644 --- a/tests/migrate/testdata/xmi/layout.golden.sysml +++ b/tests/migrate/testdata/xmi/layout.golden.sysml @@ -30,6 +30,7 @@ package Behavior { action def Drive { succession 'start to step' first start then 'step'; action 'step' { + rep language "JavaScript" /* going = true; */ attribute going : ScalarValues::Boolean; assign going := true; } diff --git a/tests/migrate/testdata/xmi/layout.layout.golden.sysml b/tests/migrate/testdata/xmi/layout.layout.golden.sysml index 3163651495..6473fad7fe 100644 --- a/tests/migrate/testdata/xmi/layout.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/layout.layout.golden.sysml @@ -38,6 +38,7 @@ package Behavior { action def Drive { succession 'start to step' first start then 'step'; action 'step' { + rep language "JavaScript" /* going = true; */ attribute going : ScalarValues::Boolean; assign going := true; } diff --git a/tests/migrate/testdata/xmi/meter.golden.report.txt b/tests/migrate/testdata/xmi/meter.golden.report.txt index aa9fab426a..4cedaa1dd1 100644 --- a/tests/migrate/testdata/xmi/meter.golden.report.txt +++ b/tests/migrate/testdata/xmi/meter.golden.report.txt @@ -4,13 +4,13 @@ ## approximated (38) ControlFlow Meter::Arm:: _a2 (the guard [{JavaScript} ready and enabled] is kept as a comment and the edge written unguarded: the text "and" is not expression syntax: text follows the expression) Parameter Meter::Arm::armed _armed -> Meter::arm::armed (default value not migrated: the types at "Math.sqrt(total)" disagree: the expression is a Real, not the Boolean wanted) -OpaqueAction Meter::Arm::fuse _fuse -> fuse (the body is kept as a comment: the text "and" is not expression syntax: a statement ends at `;` or a newline) -OpaqueAction Meter::Flip::alias _alias -> 'alias' (the body is kept as a comment: the construct "==" is outside the translated subset: Java compares strings by identity with `==`, which a comparison of their values does not reproduce; `equals` compares their content) -OpaqueAction Meter::Flip::bump _bump -> bump (the body is kept as a comment: the types at "+" disagree: an operand is a Mode, not a number) -OpaqueAction Meter::Flip::huge _huge -> huge (the body is kept as a comment: the construct "9007199254740993" is outside the translated subset: a script rounds a whole number beyond 9007199254740991 to the nearest floating-point value) -OpaqueAction Meter::Flip::match _match -> match (the body is kept as a comment: the types at "mode =" disagree: a Gauge is assigned to the Mode mode holds) -OpaqueAction Meter::Flip::narrow _narrow -> narrow (the body is kept as a comment: the types at "hand =" disagree: a Gauge is assigned to the Needle hand holds) -OpaqueAction Meter::Flip::pick _pick -> pick (the body is kept as a comment: the types at "count =" disagree: a Mode is assigned to the Integer count holds) +OpaqueAction Meter::Arm::fuse _fuse -> fuse (the body is kept as a textual representation, which is not executed: the text "and" is not expression syntax: a statement ends at `;` or a newline) +OpaqueAction Meter::Flip::alias _alias -> 'alias' (the body is kept as a textual representation, which is not executed: the construct "==" is outside the translated subset: Java compares strings by identity with `==`, which a comparison of their values does not reproduce; `equals` compares their content) +OpaqueAction Meter::Flip::bump _bump -> bump (the body is kept as a textual representation, which is not executed: the types at "+" disagree: an operand is a Mode, not a number) +OpaqueAction Meter::Flip::huge _huge -> huge (the body is kept as a textual representation, which is not executed: the construct "9007199254740993" is outside the translated subset: a script rounds a whole number beyond 9007199254740991 to the nearest floating-point value) +OpaqueAction Meter::Flip::match _match -> match (the body is kept as a textual representation, which is not executed: the types at "mode =" disagree: a Gauge is assigned to the Mode mode holds) +OpaqueAction Meter::Flip::narrow _narrow -> narrow (the body is kept as a textual representation, which is not executed: the types at "hand =" disagree: a Gauge is assigned to the Needle hand holds) +OpaqueAction Meter::Flip::pick _pick -> pick (the body is kept as a textual representation, which is not executed: the types at "count =" disagree: a Mode is assigned to the Integer count holds) OpaqueAction Meter::Flip::same _same -> same (the Java body is translated to v2; label holds no initial value and the body reads it before assigning it, so a run reaching the read first stops) OpaqueAction Meter::Measure::cc _cc -> cc (the JavaCC body is written as v2 assignments) ValuePin Meter::Measure::judge::ok _judgeok -> Meter::measure::judge.ok (the value pin's value {JavaScript} Math.sqrt(total) is not written: the types at "Math.sqrt(total)" disagree: the expression is a Real, not the Boolean wanted) @@ -18,20 +18,20 @@ OpaqueAction Meter::Measure::log _log -> log (the JavaScript body is translated OpaqueAction Meter::Measure::shout _shout -> shout (the JavaScript body is translated to v2; the console print println(…) is left out, as it writes to the tool's console and changes nothing of the model) ObjectFlow Meter::Skip:: _o2 (the flow is kept as a comment: the body of 'idle' never assigns 'z', so no value leaves it) ObjectFlow Meter::Skip:: _o3 (the flow is kept as a comment: its source 'dark' is not migrated, so no value reaches 'q') -OpaqueAction Meter::Skip::dark _dark -> dark (the body is kept as a comment: the statement assigns q, which nothing visible from Meter::Skip::dark is called (language Python)) +OpaqueAction Meter::Skip::dark _dark -> dark (the body is kept as a textual representation, which is not executed: the statement assigns q, which nothing visible from Meter::Skip::dark is called (language Python)) OpaqueAction Meter::Skip::drain _drain -> drain (the JavaScript body is translated to v2; total must hold a value, so it is assigned only when w, which may hold none, holds one) InputPin Meter::Skip::drain::w _drainw -> Meter::skip::drain.w (it is declared admitting no value: 'dark', which feeds it, produces no value) OpaqueAction Meter::Skip::sink _sink -> sink (the JavaScript body is translated to v2; total must hold a value, so it is assigned only when w, which may hold none, holds one) InputPin Meter::Skip::sink::w _sinkw -> Meter::skip::sink.w (it is declared admitting no value: 'idle', which feeds it, produces no value) ActivityPartition Meter::Sweep::bank _bank_lane (a partition names who performs its nodes; it is kept as a comment; the partition represents a Bank through the part Meter::banks, but the multiplicity of Meter::banks is not written in numbers, so whether it holds one value cannot be told) ActivityPartition Meter::Sweep::banks _banks_lane (a partition names who performs its nodes; it is kept as a comment; the partition represents Meter::banks, but the multiplicity of Meter::banks is not written in numbers, so whether it holds one value cannot be told) -OpaqueAction Meter::Sweep::calib _calib -> calib (the body is kept as a comment: the construct "reading" is outside the translated subset: this.cells is a collection, so the assignment would write through several objects) -OpaqueAction Meter::Sweep::charge _charge -> charge (the body is kept as a comment: the name "level" resolves to nothing readable: nothing visible from Meter::Sweep::charge is called level) -OpaqueAction Meter::Sweep::reset _reset -> reset (the body is kept as a comment: the construct "cells.reading" is outside the translated subset: cells is a collection, so the assignment would write through several objects) -OpaqueAction Meter::Sweep::settle _settle -> settle (the body is kept as a comment: the name "level" resolves to nothing readable: nothing visible from Meter::Sweep::settle is called level) -OpaqueAction Meter::Sweep::span _span -> span (the body is kept as a comment: the types at "total =" disagree: one side is a collection and the other a single value) -OpaqueAction Meter::Sweep::spread _spread -> spread (the body is kept as a comment: the types at "+" disagree: an operand is a collection, not a number) -OpaqueAction Meter::Sweep::tally _tally -> tally (the body is kept as a comment: the construct "banks.level" is outside the translated subset: the multiplicity of Meter::banks is not written in numbers, so whether it holds one value cannot be told) +OpaqueAction Meter::Sweep::calib _calib -> calib (the body is kept as a textual representation, which is not executed: the construct "reading" is outside the translated subset: this.cells is a collection, so the assignment would write through several objects) +OpaqueAction Meter::Sweep::charge _charge -> charge (the body is kept as a textual representation, which is not executed: the name "level" resolves to nothing readable: nothing visible from Meter::Sweep::charge is called level) +OpaqueAction Meter::Sweep::reset _reset -> reset (the body is kept as a textual representation, which is not executed: the construct "cells.reading" is outside the translated subset: cells is a collection, so the assignment would write through several objects) +OpaqueAction Meter::Sweep::settle _settle -> settle (the body is kept as a textual representation, which is not executed: the name "level" resolves to nothing readable: nothing visible from Meter::Sweep::settle is called level) +OpaqueAction Meter::Sweep::span _span -> span (the body is kept as a textual representation, which is not executed: the types at "total =" disagree: one side is a collection and the other a single value) +OpaqueAction Meter::Sweep::spread _spread -> spread (the body is kept as a textual representation, which is not executed: the types at "+" disagree: an operand is a collection, not a number) +OpaqueAction Meter::Sweep::tally _tally -> tally (the body is kept as a textual representation, which is not executed: the construct "banks.level" is outside the translated subset: the multiplicity of Meter::banks is not written in numbers, so whether it holds one value cannot be told) CallBehaviorAction Meter::Sweep::ticking _ticking -> ticking (its swimlane represents this.cells, a collection of objects, so none of them performs the call, which runs in the caller's context; the behavior acts on a Gauge through its parameter context, which is left unbound: the caller is a Meter, which is no Gauge and has 4 parts that are one, so no one of them is chosen; the behavior belongs to Gauge and runs here in the caller's context) Property Meter::banks _banks -> Meter::banks (multiplicity 1..n is not a range of natural numbers and is not written) Property Meter::gain _gain -> Meter::gain (default value not migrated: the call "total" is not in the translated function table) diff --git a/tests/migrate/testdata/xmi/meter.golden.sysml b/tests/migrate/testdata/xmi/meter.golden.sysml index a93fab9198..7d2e413c6d 100644 --- a/tests/migrate/testdata/xmi/meter.golden.sysml +++ b/tests/migrate/testdata/xmi/meter.golden.sysml @@ -59,15 +59,18 @@ part def Meter { action sense { in x : ScalarValues::Real[1] = 1.5; out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = x * 2; */ assign y := x * 2; } action record { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* total = v + 1; peak = Math.max(v); */ assign total := v + 1; assign peak := v; } first record then split; action split { + rep language "Java" /* half = ticks / 2; ratio = total / 2; floored = Math.floor(total) / 8; rounded = Math.round(total) / 8; */ assign half := OpenSysMLMathFunctions::quotient(ticks, 2); assign ratio := total / 2; assign floored := RealFunctions::floor(total) / 8; @@ -75,10 +78,12 @@ part def Meter { } first split then quarterly; action quarterly { + rep language "Java 1.8.0_202" /* quarter = ticks / 4; */ assign quarter := OpenSysMLMathFunctions::quotient(ticks, 4); } first quarterly then cc; action cc { + rep language "JavaCC" /* eighth = ticks / 8; */ assign eighth := ticks / 8; } first cc then judge; @@ -86,14 +91,21 @@ part def Meter { in ok : ScalarValues::Boolean[1]; in n : ScalarValues::Integer[1] = RealFunctions::floor(total); in m : ScalarValues::Integer[1] = 2; + rep language "JavaScript" /* count = n + m; */ assign count := n + m; } first judge then log; action log { + rep language "JavaScript" /* print("total so far: " + (total + 1)); + * ticks = ticks + 1; + * println(ticks); + */ assign ticks := ticks + 1; } first log then shout; - action shout; + action shout { + rep language "JavaScript" /* println("done"); */ + } first shout then final; action final terminate; succession flow of ScalarValues::Real from sense.y to record.v; @@ -103,23 +115,24 @@ part def Meter { first start then idle; action idle { out z : ScalarValues::Real[1]; + rep language "JavaScript" /* total = total * 1; */ assign total := total * 1; } first idle then sink; action sink { in w : ScalarValues::Real[0..1]; + rep language "JavaScript" /* total = w; */ if w->SequenceFunctions::notEmpty() { assign total := w; } } first sink then dark; action dark { out q : ScalarValues::Real[1]; - /* body not migrated (the statement assigns q, which nothing visible from Meter::Skip::dark is called (language Python)) {Python}: - * q = 1 - */ + rep language "Python" /* q = 1 */ } first dark then drain; action drain { in w : ScalarValues::Real[0..1]; + rep language "JavaScript" /* total = w; */ if w->SequenceFunctions::notEmpty() { assign total := w; } } first drain then final; @@ -131,52 +144,43 @@ part def Meter { action flip { first start then bump; action bump { - /* body not migrated (the types at "+" disagree: an operand is a Mode, not a number) {JavaScript}: - * count = mode + 1; - */ + rep language "JavaScript" /* count = mode + 1; */ } first bump then pick; action pick { - /* body not migrated (the types at "count =" disagree: a Mode is assigned to the Integer count holds) {JavaScript}: - * count = mode; - */ + rep language "JavaScript" /* count = mode; */ } first pick then match; action match { - /* body not migrated (the types at "mode =" disagree: a Gauge is assigned to the Mode mode holds) {JavaScript}: - * mode = dial; - */ + rep language "JavaScript" /* mode = dial; */ } first match then widen; action widen { + rep language "JavaScript" /* dial = hand; */ assign dial := hand; } first widen then narrow; action narrow { - /* body not migrated (the types at "hand =" disagree: a Gauge is assigned to the Needle hand holds) {JavaScript}: - * hand = dial; - */ + rep language "JavaScript" /* hand = dial; */ } first narrow then huge; action huge { - /* body not migrated (the construct "9007199254740993" is outside the translated subset: a script rounds a whole number beyond 9007199254740991 to the nearest floating-point value) {JavaScript}: - * count = 9007199254740993; - */ + rep language "JavaScript" /* count = 9007199254740993; */ } first huge then smile; action smile { + rep language "JavaScript" /* label = "\uD83D\uDE00"; count = 9007199254740991; */ assign label := "😀"; assign count := 9007199254740991; } first smile then same; action same { + rep language "Java" /* lit = label.equals("\uD83D\uDE00"); */ assign lit := label == "😀"; } first same then 'alias'; action 'alias' { - /* body not migrated (the construct "==" is outside the translated subset: Java compares strings by identity with `==`, which a comparison of their values does not reproduce; `equals` compares their content) {Java}: - * lit = label == "\uD83D\uDE00"; - */ + rep language "Java" /* lit = label == "\uD83D\uDE00"; */ } first 'alias' then final; action final terminate; @@ -189,13 +193,12 @@ part def Meter { in limit : ScalarValues::Integer[1] default = count + 1; first start then fuse; action fuse { - /* body not migrated (the text "and" is not expression syntax: a statement ends at `;` or a newline) {JavaScript}: - * flag = ready and enabled - */ + rep language "JavaScript" /* flag = ready and enabled */ } /* guard not migrated: [{JavaScript} ready and enabled] — the text "and" is not expression syntax: text follows the expression */ first fuse then latch; action latch { + rep language "JavaScript" /* flag = ready && enabled */ assign flag := ready and enabled; } first latch then final; @@ -205,51 +208,38 @@ part def Meter { action sweep { first start then spread; action spread { - /* body not migrated (the types at "+" disagree: an operand is a collection, not a number) {JavaScript}: - * total = cells.reading + 1; - */ + rep language "JavaScript" /* total = cells.reading + 1; */ } first spread then reset; action reset { - /* body not migrated (the construct "cells.reading" is outside the translated subset: cells is a collection, so the assignment would write through several objects) {JavaScript}: - * cells.reading = 1; - */ + rep language "JavaScript" /* cells.reading = 1; */ } first reset then peak; action peak { + rep language "JavaScript" /* total = java.util.Collections.max(cells.reading); */ assign total := cells.reading->ControlFunctions::reduce { in x; in y; RealFunctions::max(x, y) }; } first peak then calib; action calib { - /* body not migrated (the construct "reading" is outside the translated subset: this.cells is a collection, so the assignment would write through several objects) {JavaScript}: - * reading = 2; - */ + rep language "JavaScript" /* reading = 2; */ } first calib then span; action span { - /* body not migrated (the types at "total =" disagree: one side is a collection and the other a single value) {JavaScript}: - * total = reading; - */ + rep language "JavaScript" /* total = reading; */ } first span then ticking; action ticking : Gauge::Tick; first ticking then tally; action tally { - /* body not migrated (the construct "banks.level" is outside the translated subset: the multiplicity of Meter::banks is not written in numbers, so whether it holds one value cannot be told) {JavaScript}: - * total = banks.level + 1; - */ + rep language "JavaScript" /* total = banks.level + 1; */ } first tally then charge; action charge { - /* body not migrated (the name "level" resolves to nothing readable: nothing visible from Meter::Sweep::charge is called level) {JavaScript}: - * level = 1; - */ + rep language "JavaScript" /* level = 1; */ } first charge then settle; action settle { - /* body not migrated (the name "level" resolves to nothing readable: nothing visible from Meter::Sweep::settle is called level) {JavaScript}: - * level = 0; - */ + rep language "JavaScript" /* level = 0; */ } first settle then final; action final terminate; diff --git a/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml b/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml index 36ffe00a67..77ebd94976 100644 --- a/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml +++ b/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml @@ -7,6 +7,7 @@ part def Sensor { perform action settle { first start then 'start timer'; action 'start timer' { + rep language "JavaScript" /* settleTime = simtime; */ assign settleTime := localClock.currentTime; } first 'start timer' then wait; @@ -15,6 +16,7 @@ part def Sensor { action settle; first settle then 'stop timer'; action 'stop timer' { + rep language "JavaScript" /* settleTime = simtime - settleTime; */ assign settleTime := localClock.currentTime - settleTime; } first 'stop timer' then final; diff --git a/tests/migrate/testdata/xmi/nested_context.golden.sysml b/tests/migrate/testdata/xmi/nested_context.golden.sysml index 68a3aa0cef..8003e04913 100644 --- a/tests/migrate/testdata/xmi/nested_context.golden.sysml +++ b/tests/migrate/testdata/xmi/nested_context.golden.sysml @@ -52,6 +52,7 @@ part def Host { action direct { first start then 'write state data'; action 'write state data' { + rep language "JavaScript Rhino" /* k = 2 */ assign 'aO Sequencer'.k := 2; } first 'write state data' then 'decide'; diff --git a/tests/migrate/testdata/xmi/object_flows.golden.report.txt b/tests/migrate/testdata/xmi/object_flows.golden.report.txt index afab679ce1..da6466c9f6 100644 --- a/tests/migrate/testdata/xmi/object_flows.golden.report.txt +++ b/tests/migrate/testdata/xmi/object_flows.golden.report.txt @@ -1,11 +1,10 @@ # SysML v1 to v2 migration report: object_flows.xmi -# migrated 126 element(s): 124 mapped, 2 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 126 element(s): 125 mapped, 1 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (2) +## approximated (1) OpaqueAction Pipeline::Count::step _cs -> 'step' (the JavaScript body is translated to v2; several edges leave the node, which a fork fork carries) -OpaqueAction Pipeline::Pair::keep _pc -> keep (the body is kept as a comment: the body is empty) -## mapped (124) +## mapped (125) Model Model _m (the root model's members are written at the top level) «Block» Class Pipeline _pipe -> Pipeline Activity Pipeline::Count _count -> Pipeline::count (written as an action usage of Pipeline, which a call on an object performs, so its body runs on the object and reaches its features) @@ -87,6 +86,7 @@ ForkNode Pipeline::Pair::both _pfk -> both JoinNode Pipeline::Pair::gather _pj -> gather OpaqueAction Pipeline::Pair::high _ph -> high (the JavaScript body is translated to v2) OutputPin Pipeline::Pair::high::y _phy -> Pipeline::Pair::high.y +OpaqueAction Pipeline::Pair::keep _pc -> keep (its body is empty, so it is written as an action that does nothing) InputPin Pipeline::Pair::keep::v _pcv -> Pipeline::Pair::keep.v OpaqueAction Pipeline::Pair::low _pl -> low (the JavaScript body is translated to v2) OutputPin Pipeline::Pair::low::y _ply -> Pipeline::Pair::low.y diff --git a/tests/migrate/testdata/xmi/object_flows.golden.sysml b/tests/migrate/testdata/xmi/object_flows.golden.sysml index b2611d29f8..3abca35311 100644 --- a/tests/migrate/testdata/xmi/object_flows.golden.sysml +++ b/tests/migrate/testdata/xmi/object_flows.golden.sysml @@ -7,10 +7,12 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 2.5; */ assign y := 2.5; } action consume { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* a = v; */ assign a := v; } first consume then final; @@ -22,10 +24,12 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 4.0; */ assign y := 4.0; } action consume { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* a = v; */ assign a := v; } first consume then final; @@ -38,6 +42,7 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 3.0; */ assign y := 3.0; } fork copy { @@ -47,11 +52,13 @@ part def Pipeline { } action left { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* a = v; */ assign a := v; } first left then both; action right { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* b = v * 2; */ assign b := v * 2; } first right then both; @@ -67,14 +74,17 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 5.0; */ assign y := 5.0; } action left { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* a = v; */ assign a := v; } action right { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* b = v + 1; */ assign b := v + 1; } succession flow of ScalarValues::Real from produce.y to left.v; @@ -87,9 +97,11 @@ part def Pipeline { first 'start both' then warm; action produce { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 7.0; */ assign y := 7.0; } action warm { + rep language "JavaScript" /* n = n + 1; */ assign n := n + 1; } first warm then ready; @@ -99,6 +111,7 @@ part def Pipeline { } action consume { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* a = v; */ assign a := v; } first consume then final; @@ -114,10 +127,12 @@ part def Pipeline { first which if not on then low; action high { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 9.0; */ assign y := 9.0; } action low { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 1.0; */ assign y := 1.0; } merge pick { @@ -127,6 +142,7 @@ part def Pipeline { } action consume { in v : ScalarValues::Real[1]; + rep language "JavaScript" /* a = v; */ assign a := v; } first consume then final; @@ -140,6 +156,7 @@ part def Pipeline { first start then zero; action zero { out y : ScalarValues::Integer[1]; + rep language "JavaScript" /* y = 0; */ assign y := 0; } merge again { @@ -150,6 +167,9 @@ part def Pipeline { action 'step' { in x : ScalarValues::Integer[1]; out y : ScalarValues::Integer[1]; + rep language "JavaScript" /* y = x + 1; + * n = y; + */ assign y := x + 1; assign n := y; } @@ -174,10 +194,12 @@ part def Pipeline { first both then high; action low { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 1.5; */ assign y := 1.5; } action high { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 8.5; */ assign y := 8.5; } join gather { @@ -187,7 +209,6 @@ part def Pipeline { } action keep { in v : ScalarValues::Real[2]; - /* body not migrated (the body is empty) */ } succession flow of ScalarValues::Real from low.y to gather.inputObject1; succession flow of ScalarValues::Real from high.y to gather.inputObject2; @@ -201,6 +222,7 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = 6.0; */ assign y := 6.0; } first produce then feed; diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt index dab7b55b9a..78e19987bf 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt @@ -1,12 +1,12 @@ # SysML v1 to v2 migration report: parking_usecases.xmi -# migrated 44 element(s): 29 mapped, 12 approximated, 3 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 44 element(s): 30 mapped, 11 approximated, 3 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## unmapped (3) Extend Use Cases::Leave Garage:: _extLost (the extended use case is not in the document; 1 extendedCase reference(s) resolve to nothing in the document (_lostCase)) Include Use Cases::Leave Garage:: _incLost (the included use case is not in the document; 1 addition reference(s) resolve to nothing in the document (_lostCase)) ExtensionPoint Use Cases::Park Car::garage full _epFull (v2 has no extension points; an extending use case is written as a dependency on the extended one) -## approximated (12) +## approximated (11) Actor Context::Attendant _attendant -> Context::Attendant (a UML actor is written as a part def) Actor Context::Bank _bank -> Context::Bank (a UML actor is written as a part def) Actor Context::Driver _driver -> Context::Driver (a UML actor is written as a part def) @@ -14,13 +14,12 @@ Association Use Cases:: _aParkWait -> 'Use Cases'::unnamed (the ano Property Use Cases:::: _eParkD -> 'Use Cases'::'Park Car'::driver (the end at the use case is the use case def the actor is declared in) Property Use Cases:::: _eParkA -> 'Use Cases'::'Park Car'::attendant (the end at the use case is the use case def the actor is declared in) Activity Use Cases::Park Car::Park Flow _parkFlow -> 'Use Cases'::'Park Car'::'Park Flow' (the classifier behavior is run by every object of Use Cases::Park Car as its usage park Flow) -OpaqueAction Use Cases::Park Car::Park Flow::Enter _pfEnter -> Enter (the body is kept as a comment: the body is empty) UseCase Use Cases::Pay Fee _pay -> 'Use Cases'::'Pay Fee' (v2 admits one subject per case, so the subject Context::Ticket is written as the reference usage ticket) Property Use Cases::Settlement:: _ePayB -> 'Use Cases'::'Pay Fee'::bank (the end at the use case is the use case def the actor is declared in) Property Use Cases::Valet Service::offer _svcCase -> 'Use Cases'::'Valet Service'::offer (a property typed by a behavior is written as a reference use case usage) Extend Use Cases::Wait For Space:: _extFull (v2 has no extend: the extension is written as a plain dependency on the extended use case; it applies at extension point(s) 'garage full' when {English} no bay is free, which only a comment keeps) -## mapped (29) +## mapped (30) Package Context _ctx -> Context Comment Context::Driver:: _driverDoc «Block» Class Context::Garage _garage -> Context::Garage @@ -43,6 +42,7 @@ Include Use Cases::Park Car:: _incPay -> 'Use Cases'::'Park Car'::'pay ActivityFinalNode Use Cases::Park Car::Park Flow:: _pfEnd -> final ControlFlow Use Cases::Park Car::Park Flow:: _pfE2 InitialNode Use Cases::Park Car::Park Flow:: _pfStart -> start +OpaqueAction Use Cases::Park Car::Park Flow::Enter _pfEnter -> Enter (its body is empty, so it is written as an action that does nothing) Property Use Cases::Pay Fee::amount _payAmount -> 'Use Cases'::'Pay Fee'::amount Include Use Cases::Pay Fee::authorise _incAuth -> 'Use Cases'::'Pay Fee'::authorise Association Use Cases::Settlement _aBankPay -> 'Use Cases'::Settlement (the association is also written as the actor bank of the use case 'Use Cases'::'Pay Fee') diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml index fdb21ebbbd..15ddff292a 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml @@ -15,9 +15,7 @@ package 'Use Cases' { /* not migrated: ExtensionPoint 'garage full' — v2 has no extension points; an extending use case is written as a dependency on the extended one */ action def 'Park Flow' { first start then Enter; - action Enter { - /* body not migrated (the body is empty) */ - } + action Enter; first Enter then final; action final terminate; metadata MigrationMetadata::SynthesizedName about final; diff --git a/tests/migrate/testdata/xmi/plant.golden.report.txt b/tests/migrate/testdata/xmi/plant.golden.report.txt index 8a8c604680..73442d72a8 100644 --- a/tests/migrate/testdata/xmi/plant.golden.report.txt +++ b/tests/migrate/testdata/xmi/plant.golden.report.txt @@ -6,8 +6,8 @@ ControlFlow Plant::Fill:: _f3a (the guard [{JavaScript} on] is kept ActivityPartition Plant::Fill::Elsewhere _gone (a partition names who performs its nodes; it is kept as a comment; the partition represents an element outside the document) ActivityPartition Plant::Fill::Operator _unset (a partition names who performs its nodes; it is kept as a comment; the partition represents nothing) ActivityPartition Plant::Fill::Spare _spare (a partition names who performs its nodes; it is kept as a comment; the partition represents the context's tank, read as this.tank, but nothing in it names the object's features) -OpaqueAction Plant::Fill::mine _mine -> mine (the body is kept as a comment: the name "this.level" resolves to nothing readable: Tank has no feature level) -OpaqueAction Plant::Fill::torn _torn -> torn (the body is kept as a comment: the name "volume" resolves to nothing readable: nothing visible from Plant::Fill::torn is called volume; it is in the partitions 'Tank' (this.tank) and 'Pump' (this.pump), which represent different objects, so names resolve through no partition) +OpaqueAction Plant::Fill::mine _mine -> mine (the body is kept as a textual representation, which is not executed: the name "this.level" resolves to nothing readable: Tank has no feature level) +OpaqueAction Plant::Fill::torn _torn -> torn (the body is kept as a textual representation, which is not executed: the name "volume" resolves to nothing readable: nothing visible from Plant::Fill::torn is called volume; it is in the partitions 'Tank' (this.tank) and 'Pump' (this.pump), which represent different objects, so names resolve through no partition) ## mapped (56) «Block» Class Control _control -> Control diff --git a/tests/migrate/testdata/xmi/plant.golden.sysml b/tests/migrate/testdata/xmi/plant.golden.sysml index 03c445a4b7..4f08f0f529 100644 --- a/tests/migrate/testdata/xmi/plant.golden.sysml +++ b/tests/migrate/testdata/xmi/plant.golden.sysml @@ -33,55 +33,61 @@ part def Plant { action fill { first start then 'open valve'; action 'open valve' { + rep language "JavaScript" /* open = true; */ assign tank.valve.open := true; } first 'open valve' then 'fill tank'; action 'fill tank' { + rep language "JavaScript" /* volume = volume * 2 + (valve.open ? 1 : 0); */ assign tank.volume := tank.volume * 2 + (if tank.valve.open ? 1 else 0); } first 'fill tank' then torn; action torn { - /* body not migrated (the name "volume" resolves to nothing readable: nothing visible from Plant::Fill::torn is called volume; it is in the partitions 'Tank' (this.tank) and 'Pump' (this.pump), which represent different objects, so names resolve through no partition) {JavaScript}: - * volume = 0; - */ + rep language "JavaScript" /* volume = 0; */ } /* guard not migrated: [{JavaScript} on] — the name "on" resolves to nothing readable: nothing visible from Plant::Fill:: is called on; its source 'torn' is in the partitions 'Tank' (this.tank) and 'Pump' (this.pump), which represent different objects, so names resolve through no partition */ first torn then prime; action prime { + rep language "JavaScript" /* on = true; */ assign pump.on := true; } first prime then twice; action twice { + rep language "JavaScript" /* volume = volume + 1; */ assign tank.volume := tank.volume + 1; } first twice then mine; action mine { - /* body not migrated (the name "this.level" resolves to nothing readable: Tank has no feature level) {JavaScript}: - * this.level = 1; - */ + rep language "JavaScript" /* this.level = 1; */ } first mine then 'count run'; action 'count run' { + rep language "JavaScript" /* runs += 1; */ assign runs := runs + 1; } first 'count run' then 'note level'; action 'note level' { + rep language "JavaScript" /* level = tank.volume; */ assign level := tank.volume; } first 'note level' then lost; action lost { + rep language "JavaScript" /* level = level + 1; */ assign level := level + 1; } first lost then free; action free { + rep language "JavaScript" /* level = level + tank.volume; */ assign level := level + tank.volume; } first free then arm; action arm { + rep language "JavaScript" /* this.status = true; */ assign site.control.rack.controller.status := true; } first arm then light; action light { + rep language "JavaScript" /* lit = true; */ assign site.control.rack.controller.led.lit := true; } first light then final; diff --git a/tests/migrate/testdata/xmi/plant_states.golden.sysml b/tests/migrate/testdata/xmi/plant_states.golden.sysml index 41c0041b17..13259188fc 100644 --- a/tests/migrate/testdata/xmi/plant_states.golden.sysml +++ b/tests/migrate/testdata/xmi/plant_states.golden.sysml @@ -20,6 +20,7 @@ part def Plant { state Work { entry; then Prep; exit action tally { + rep language "JavaScript" /* count = count + 1; */ assign context.count := context.count + 1; } state Prep; @@ -58,7 +59,9 @@ part def Plant { transition first Idle accept Resume then Work.Run; transition first Work accept Hold then Pause; transition first Pause accept Resume - do action log { } + do action log { + rep language "JavaScript" /* println("resuming at " + count); */ + } then Work.last; transition first Idle accept Enter then Cell.warmStart; transition first Cell.spent then Idle; diff --git a/tests/migrate/testdata/xmi/probability_nested_def_context.golden.report.txt b/tests/migrate/testdata/xmi/probability_nested_def_context.golden.report.txt index c98fa2a9fb..b78a8ab5c2 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_context.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_nested_def_context.golden.report.txt @@ -1,14 +1,12 @@ # SysML v1 to v2 migration report: probability_nested_def_context.xmi -# migrated 19 element(s): 14 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 19 element(s): 16 mapped, 3 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (3) Activity Mission::Sub::Run _run -> Mission::Sub::Run (acts on its owner Mission::Sub, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 1.0 - context.pr, its share of what the marked branches leave of 1, read when the decision is reached) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) CallBehaviorAction Mission::Sub::Run::call _call -> call (the behavior acts on a Mission::Sub through its parameter context, which is bound to Run::context) -## mapped (14) +## mapped (16) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub ActivityFinalNode Mission::Sub::Run:: _rf -> final @@ -21,5 +19,7 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) Property Mission::pr _pr -> Mission::pr Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/probability_nested_def_context.golden.sysml b/tests/migrate/testdata/xmi/probability_nested_def_context.golden.sysml index 03f373eb95..eb3b9ee8f8 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_context.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_nested_def_context.golden.sysml @@ -9,13 +9,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = context.pr; } } first 'decide' then b { @Stochastic::Probability { p = 1.0 - context.pr; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_nested_def_default.golden.report.txt b/tests/migrate/testdata/xmi/probability_nested_def_default.golden.report.txt index 3b2d6453c3..676665ab31 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_default.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_nested_def_default.golden.report.txt @@ -1,14 +1,12 @@ # SysML v1 to v2 migration report: probability_nested_def_default.xmi -# migrated 19 element(s): 14 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 19 element(s): 16 mapped, 3 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (3) Activity Mission::Sub::Run _run -> Mission::Sub::Run (acts on its owner Mission::Sub, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 0.75, its share of what the marked branches leave of 1) «Probability» ControlFlow Mission::Sub::Run::Inner:: _ea (the probability is written as 0.25, the default of property Mission::pr, since the action acts on a Mission::Sub, which neither holds Mission::pr nor has one part that does; a run no longer reads the property, so an object whose value differs is still weighted by the default) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) -## mapped (14) +## mapped (16) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub ActivityFinalNode Mission::Sub::Run:: _rf -> final @@ -20,6 +18,8 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) CallBehaviorAction Mission::Sub::Run::call _call -> call Property Mission::pr _pr -> Mission::pr Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/probability_nested_def_default.golden.sysml b/tests/migrate/testdata/xmi/probability_nested_def_default.golden.sysml index 1967cf1486..a141634cc1 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_default.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_nested_def_default.golden.sysml @@ -8,13 +8,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = 0.25; } } first 'decide' then b { @Stochastic::Probability { p = 0.75; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part.golden.report.txt b/tests/migrate/testdata/xmi/probability_nested_def_part.golden.report.txt index 5641704a5c..ba41796cd4 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_nested_def_part.golden.report.txt @@ -1,14 +1,12 @@ # SysML v1 to v2 migration report: probability_nested_def_part.xmi -# migrated 20 element(s): 15 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 20 element(s): 17 mapped, 3 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (3) Activity Mission::Sub::Run _run -> Mission::Sub::Run (acts on its owner Mission::Sub, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 1.0 - context.mission.pr, its share of what the marked branches leave of 1, read when the decision is reached) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) CallBehaviorAction Mission::Sub::Run::call _call -> call (the behavior acts on a Mission::Sub through its parameter context, which is bound to Run::context) -## mapped (15) +## mapped (17) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub ActivityFinalNode Mission::Sub::Run:: _rf -> final @@ -21,6 +19,8 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) Property Mission::Sub::mission _mpart -> Mission::Sub::mission Property Mission::pr _pr -> Mission::pr Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part.golden.sysml b/tests/migrate/testdata/xmi/probability_nested_def_part.golden.sysml index 8be72f0056..0d40caaff7 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_nested_def_part.golden.sysml @@ -10,13 +10,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = context.mission.pr; } } first 'decide' then b { @Stochastic::Probability { p = 1.0 - context.mission.pr; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.report.txt b/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.report.txt index b8c724428f..bd00ed5ad6 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.report.txt @@ -1,14 +1,12 @@ # SysML v1 to v2 migration report: probability_nested_def_part_optional.xmi -# migrated 20 element(s): 15 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 20 element(s): 17 mapped, 3 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (3) Activity Mission::Sub::Run _run -> Mission::Sub::Run (acts on its owner Mission::Sub, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 0.75, its share of what the marked branches leave of 1) «Probability» ControlFlow Mission::Sub::Run::Inner:: _ea (the probability is written as 0.25, the default of property Mission::pr, since the action acts on a Mission::Sub, whose one part that holds Mission::pr, Mission::Sub::mission, holds 0..1 objects, so a read through it may find no number; a run no longer reads the property, so an object whose value differs is still weighted by the default) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) -## mapped (15) +## mapped (17) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub ActivityFinalNode Mission::Sub::Run:: _rf -> final @@ -20,6 +18,8 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) CallBehaviorAction Mission::Sub::Run::call _call -> call Property Mission::Sub::mission _mpart -> Mission::Sub::mission Property Mission::pr _pr -> Mission::pr diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.sysml b/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.sysml index 7995435b48..b627469a16 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_nested_def_part_optional.golden.sysml @@ -9,13 +9,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = 0.25; } } first 'decide' then b { @Stochastic::Probability { p = 0.75; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.report.txt b/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.report.txt index 894ae3e6ee..ab3bfb3bd7 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.report.txt @@ -1,14 +1,12 @@ # SysML v1 to v2 migration report: probability_nested_def_part_plural.xmi -# migrated 20 element(s): 15 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 20 element(s): 17 mapped, 3 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (3) Activity Mission::Sub::Run _run -> Mission::Sub::Run (acts on its owner Mission::Sub, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 0.75, its share of what the marked branches leave of 1) «Probability» ControlFlow Mission::Sub::Run::Inner:: _ea (the probability is written as 0.25, the default of property Mission::pr, since the action acts on a Mission::Sub, whose one part that holds Mission::pr, Mission::Sub::mission, holds 2 objects, so a read through it is a collection, not one number; a run no longer reads the property, so an object whose value differs is still weighted by the default) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) -## mapped (15) +## mapped (17) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub ActivityFinalNode Mission::Sub::Run:: _rf -> final @@ -20,6 +18,8 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) CallBehaviorAction Mission::Sub::Run::call _call -> call Property Mission::Sub::mission _mpart -> Mission::Sub::mission Property Mission::pr _pr -> Mission::pr diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.sysml b/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.sysml index 3bba8533f0..b9ccb5129e 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_nested_def_part_plural.golden.sysml @@ -9,13 +9,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = 0.25; } } first 'decide' then b { @Stochastic::Probability { p = 0.75; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.report.txt b/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.report.txt index 6a2ae9e011..5d1be4fc42 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.report.txt @@ -1,13 +1,11 @@ # SysML v1 to v2 migration report: probability_nested_def_part_uncalled.xmi -# migrated 18 element(s): 14 mapped, 4 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 18 element(s): 16 mapped, 2 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (4) +## approximated (2) Activity Mission::Sub::Run _run -> Mission::Sub::Run (the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 1.0 - context.mission.pr, its share of what the marked branches leave of 1, read when the decision is reached) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) -## mapped (14) +## mapped (16) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub ActivityFinalNode Mission::Sub::Run:: _rf -> final @@ -19,6 +17,8 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) Property Mission::Sub::mission _mpart -> Mission::Sub::mission Property Mission::pr _pr -> Mission::pr Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.sysml b/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.sysml index 4a032de323..5fe8cab636 100644 --- a/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_nested_def_part_uncalled.golden.sysml @@ -9,13 +9,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = context.mission.pr; } } first 'decide' then b { @Stochastic::Probability { p = 1.0 - context.mission.pr; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_part_private.golden.report.txt b/tests/migrate/testdata/xmi/probability_part_private.golden.report.txt index 6345b69845..b63e3fbb9a 100644 --- a/tests/migrate/testdata/xmi/probability_part_private.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_part_private.golden.report.txt @@ -1,14 +1,12 @@ # SysML v1 to v2 migration report: probability_part_private.xmi -# migrated 21 element(s): 16 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 21 element(s): 18 mapped, 3 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (3) Activity Mission::Sub::Run _run -> Mission::Sub::Run (acts on its owner Mission::Sub, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Mission::Sub as its usage run) ControlFlow Mission::Sub::Run::Inner:: _eb (the edge carries no «Probability»: it is weighted 0.75, its share of what the marked branches leave of 1) «Probability» ControlFlow Mission::Sub::Run::Inner:: _ea (the probability is written as 0.25, the default of property Mission::pr, since the action acts on a Mission::Sub, whose one part that holds Mission::pr, Mission::Base::mission, is a private feature of Mission::Base, which v2 does not inherit, so it cannot be named on the object; a run no longer reads the property, so an object whose value differs is still weighted by the default) -OpaqueAction Mission::Sub::Run::Inner::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::Inner::b _b -> b (the body is kept as a comment: the body is empty) -## mapped (16) +## mapped (18) «Block» Class Mission _mission -> Mission «Block» Class Mission::Base _base -> Mission::Base Property Mission::Base::mission _mpart -> Mission::Base::mission @@ -22,6 +20,8 @@ ControlFlow Mission::Sub::Run::Inner:: _fa ControlFlow Mission::Sub::Run::Inner:: _fb DecisionNode Mission::Sub::Run::Inner:: _decide -> 'decide' InitialNode Mission::Sub::Run::Inner:: _ii -> start +OpaqueAction Mission::Sub::Run::Inner::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::Inner::b _b -> b (its body is empty, so it is written as an action that does nothing) CallBehaviorAction Mission::Sub::Run::call _call -> call Property Mission::pr _pr -> Mission::pr Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/probability_part_private.golden.sysml b/tests/migrate/testdata/xmi/probability_part_private.golden.sysml index 8b6183d16b..2c08917325 100644 --- a/tests/migrate/testdata/xmi/probability_part_private.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_part_private.golden.sysml @@ -11,13 +11,9 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = 0.25; } } first 'decide' then b { @Stochastic::Probability { p = 0.75; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then 'merge'; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/probability_part_shadowed.golden.report.txt b/tests/migrate/testdata/xmi/probability_part_shadowed.golden.report.txt index c120397ad0..0fc321dabd 100644 --- a/tests/migrate/testdata/xmi/probability_part_shadowed.golden.report.txt +++ b/tests/migrate/testdata/xmi/probability_part_shadowed.golden.report.txt @@ -1,13 +1,10 @@ # SysML v1 to v2 migration report: probability_part_shadowed.xmi -# migrated 17 element(s): 13 mapped, 4 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 17 element(s): 16 mapped, 1 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (4) +## approximated (1) ControlFlow Mission::Sub::Run:: _eb (the edge carries no «Probability»: it is weighted 1.0 - Sub::mission.pr, its share of what the marked branches leave of 1, read when the decision is reached) -OpaqueAction Mission::Sub::Run::a _a -> a (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::b _b -> b (the body is kept as a comment: the body is empty) -OpaqueAction Mission::Sub::Run::mission _shadow -> mission (the body is kept as a comment: the body is empty) -## mapped (13) +## mapped (16) «Block» Class Mission _mission -> Mission «Block» Class Mission::Sub _sub -> Mission::Sub Activity Mission::Sub::Run _run -> Mission::Sub::run (written as an action usage every object of Mission::Sub performs from creation, so its body runs on the object and reaches its features) @@ -18,6 +15,9 @@ ControlFlow Mission::Sub::Run:: _fb ControlFlow Mission::Sub::Run:: _fs DecisionNode Mission::Sub::Run:: _decide -> 'decide' InitialNode Mission::Sub::Run:: _ri -> start +OpaqueAction Mission::Sub::Run::a _a -> a (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::b _b -> b (its body is empty, so it is written as an action that does nothing) +OpaqueAction Mission::Sub::Run::mission _shadow -> mission (its body is empty, so it is written as an action that does nothing) Property Mission::Sub::mission _mpart -> Mission::Sub::mission Property Mission::pr _pr -> Mission::pr Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/probability_part_shadowed.golden.sysml b/tests/migrate/testdata/xmi/probability_part_shadowed.golden.sysml index f1df0ed25c..a2da6fa48d 100644 --- a/tests/migrate/testdata/xmi/probability_part_shadowed.golden.sysml +++ b/tests/migrate/testdata/xmi/probability_part_shadowed.golden.sysml @@ -7,17 +7,11 @@ part def Mission { decide 'decide'; first 'decide' then a { @Stochastic::Probability { p = Sub::mission.pr; } } first 'decide' then b { @Stochastic::Probability { p = 1.0 - Sub::mission.pr; } } - action a { - /* body not migrated (the body is empty) */ - } + action a; first a then mission; - action b { - /* body not migrated (the body is empty) */ - } + action b; first b then 'merge'; - action mission { - /* body not migrated (the body is empty) */ - } + action mission; first mission then 'merge'; merge 'merge'; first 'merge' then final; diff --git a/tests/migrate/testdata/xmi/reactor.golden.report.txt b/tests/migrate/testdata/xmi/reactor.golden.report.txt index a5e66e2c3e..35afcf88d0 100644 --- a/tests/migrate/testdata/xmi/reactor.golden.report.txt +++ b/tests/migrate/testdata/xmi/reactor.golden.report.txt @@ -5,25 +5,25 @@ Parameter Ceil::whole _pcr -> Ceil::whole (the return parameter is written as an out parameter) Parameter Energy::result _pr -> Energy::result (the return parameter is written as an out parameter) Parameter Floor::whole _pfr -> Floor::whole (the return parameter is written as an out parameter) -OpaqueAction Reactor::Cycle::alloc _alloc -> alloc (the body is kept as a comment: the construct "new" is outside the translated subset) -OpaqueAction Reactor::Cycle::bare _bare -> bare (the body is kept as a comment: the name "raw.level" resolves to nothing readable: Reactor::Cycle::bare::raw has no type, so no feature level) -OpaqueAction Reactor::Cycle::loop _loop -> 'loop' (the body is kept as a comment: the construct "for" is outside the translated subset) -OpaqueAction Reactor::Cycle::mixed _mixed -> mixed (the body is kept as a comment: the construct "+" is outside the translated subset: string concatenation has no v2 form in the subset) -OpaqueAction Reactor::Cycle::partial _partial -> partial (the body is kept as a comment: the call "label.trim" is not in the translated function table) -OpaqueAction Reactor::Cycle::pinned _pinned -> pinned (the body is kept as a comment: the construct "var x" is outside the translated subset: x is already a feature here, which a declaration would shadow) +OpaqueAction Reactor::Cycle::alloc _alloc -> alloc (the body is kept as a textual representation, which is not executed: the construct "new" is outside the translated subset) +OpaqueAction Reactor::Cycle::bare _bare -> bare (the body is kept as a textual representation, which is not executed: the name "raw.level" resolves to nothing readable: Reactor::Cycle::bare::raw has no type, so no feature level) +OpaqueAction Reactor::Cycle::loop _loop -> 'loop' (the body is kept as a textual representation, which is not executed: the construct "for" is outside the translated subset) +OpaqueAction Reactor::Cycle::mixed _mixed -> mixed (the body is kept as a textual representation, which is not executed: the construct "+" is outside the translated subset: string concatenation has no v2 form in the subset) +OpaqueAction Reactor::Cycle::partial _partial -> partial (the body is kept as a textual representation, which is not executed: the call "label.trim" is not in the translated function table) +OpaqueAction Reactor::Cycle::pinned _pinned -> pinned (the body is kept as a textual representation, which is not executed: the construct "var x" is outside the translated subset: x is already a feature here, which a declaration would shadow) OpaqueAction Reactor::Cycle::python _python -> python (the Python body is written as v2 assignments) -OpaqueAction Reactor::Cycle::relay _relay -> relay (the body is kept as a comment: the construct "x" is outside the translated subset: an input pin is not assigned) -OpaqueAction Reactor::Cycle::self _self -> self2 (the body is kept as a comment: the name "this.owner.power" resolves to nothing readable: Reactor has no feature owner) -OpaqueAction Reactor::Cycle::shadow _shadow -> shadow (the body is kept as a comment: the construct "var power" is outside the translated subset: power is already a feature here, which a declaration would shadow) -OpaqueAction Reactor::Cycle::stray _stray -> stray (the body is kept as a comment: the name "nowhere" resolves to nothing readable: nothing visible from Reactor::Cycle::stray is called nowhere) -OpaqueAction Reactor::Cycle::trim _trim -> trim (the body is kept as a comment: the call "label.trim" is not in the translated function table) -FunctionBehavior Root _root -> Root (the body is kept as a comment: as the result expression, the types at "Math.sqrt(x)" disagree: the expression is a Real, not the Boolean wanted; as statements, the call "Math.sqrt" is not in the translated function table: a call is not a statement of the subset) +OpaqueAction Reactor::Cycle::relay _relay -> relay (the body is kept as a textual representation, which is not executed: the construct "x" is outside the translated subset: an input pin is not assigned) +OpaqueAction Reactor::Cycle::self _self -> self2 (the body is kept as a textual representation, which is not executed: the name "this.owner.power" resolves to nothing readable: Reactor has no feature owner) +OpaqueAction Reactor::Cycle::shadow _shadow -> shadow (the body is kept as a textual representation, which is not executed: the construct "var power" is outside the translated subset: power is already a feature here, which a declaration would shadow) +OpaqueAction Reactor::Cycle::stray _stray -> stray (the body is kept as a textual representation, which is not executed: the name "nowhere" resolves to nothing readable: nothing visible from Reactor::Cycle::stray is called nowhere) +OpaqueAction Reactor::Cycle::trim _trim -> trim (the body is kept as a textual representation, which is not executed: the call "label.trim" is not in the translated function table) +FunctionBehavior Root _root -> Root (the body is kept as a textual representation, which is not executed: as the result expression, the types at "Math.sqrt(x)" disagree: the expression is a Real, not the Boolean wanted; as statements, the call "Math.sqrt" is not in the translated function table: a call is not a statement of the subset) Parameter Root::ok _prok -> Root::ok (the return parameter is written as an out parameter) Parameter Round::whole _prdr -> Round::whole (the return parameter is written as an out parameter) «SimulationConfig» Class Run _config -> Run (the configuration names no execution target, so it runs no behavior) -OpaqueBehavior Scale _scale -> Scale (the body is kept as a comment: the construct "x" is outside the translated subset: an in parameter is not assigned) -OpaqueBehavior Shift _shift -> Shift (the body is kept as a comment: the construct "a" is outside the translated subset: an in parameter is not assigned) -FunctionBehavior Split _split -> Split (the body is kept as a comment: as the result expression, the behavior has 2 output parameters; one result expression can stand for none of them; as statements, the construct "x" is outside the translated subset: an expression that assigns nothing is not a statement of the subset) +OpaqueBehavior Scale _scale -> Scale (the body is kept as a textual representation, which is not executed: the construct "x" is outside the translated subset: an in parameter is not assigned) +OpaqueBehavior Shift _shift -> Shift (the body is kept as a textual representation, which is not executed: the construct "a" is outside the translated subset: an in parameter is not assigned) +FunctionBehavior Split _split -> Split (the body is kept as a textual representation, which is not executed: as the result expression, the behavior has 2 output parameters; one result expression can stand for none of them; as statements, the construct "x" is outside the translated subset: an expression that assigns nothing is not a statement of the subset) Parameter Split::hi _pshi -> Split::hi (the return parameter is written as an out parameter) ## mapped (56) diff --git a/tests/migrate/testdata/xmi/reactor.golden.sysml b/tests/migrate/testdata/xmi/reactor.golden.sysml index 9435c01cc4..124d3902f2 100644 --- a/tests/migrate/testdata/xmi/reactor.golden.sysml +++ b/tests/migrate/testdata/xmi/reactor.golden.sysml @@ -8,24 +8,22 @@ calc def Energy { in P : ScalarValues::Real[1]; in dt : ScalarValues::Real[1]; out result : ScalarValues::Real[1]; + rep language "JavaScript" /* Math.max(E_0 - P * dt, 0) */ RealFunctions::max(E_0 - P * dt, 0) } action def Scale { in x : ScalarValues::Real[1]; out y : ScalarValues::Real[1]; - /* body not migrated (the construct "x" is outside the translated subset: an in parameter is not assigned) {JavaScript}: - * x = x * 2; y = x; - */ + rep language "JavaScript" /* x = x * 2; y = x; */ } action def Shift { in a : ScalarValues::Real[1]; out b : ScalarValues::Real[1]; - /* body not migrated (the construct "a" is outside the translated subset: an in parameter is not assigned) {JavaScript}: - * const step = 1; a = a + step; b = a; - */ + rep language "JavaScript" /* const step = 1; a = a + step; b = a; */ } action def Double { inout n : ScalarValues::Real[1]; + rep language "JavaScript" /* const k = 2; n = n * k; */ first start then body; action body { attribute k : ScalarValues::Integer; @@ -37,6 +35,7 @@ action def Double { action def Delay { in t_sim : ScalarValues::Real[1]; out d : ScalarValues::Real[1]; + rep language "JavaScript" /* d = t_sim * 2; */ first start then body; action body { assign d := t_sim * 2; @@ -46,32 +45,31 @@ action def Delay { calc def Floor { in x : ScalarValues::Real[1]; out whole : ScalarValues::Integer[1]; + rep language "JavaScript" /* Math.floor(x) */ RealFunctions::floor(x) } calc def Ceil { in x : ScalarValues::Real[1]; out whole : ScalarValues::Integer[1]; + rep language "JavaScript" /* Math.ceil(x) */ OpenSysMLMathFunctions::ceiling(x) } calc def Round { in x : ScalarValues::Real[1]; out whole : ScalarValues::Real[1]; + rep language "JavaScript" /* Math.round(x) */ RealFunctions::floor(x + 0.5) } action def Root { in x : ScalarValues::Real[1]; out ok : ScalarValues::Boolean[1]; - /* body not migrated (as the result expression, the types at "Math.sqrt(x)" disagree: the expression is a Real, not the Boolean wanted; as statements, the call "Math.sqrt" is not in the translated function table: a call is not a statement of the subset) {JavaScript}: - * Math.sqrt(x) - */ + rep language "JavaScript" /* Math.sqrt(x) */ } action def Split { in x : ScalarValues::Real[1]; out lo : ScalarValues::Real[1]; out hi : ScalarValues::Real[1]; - /* body not migrated (as the result expression, the behavior has 2 output parameters; one result expression can stand for none of them; as statements, the construct "x" is outside the translated subset: an expression that assigns nothing is not a statement of the subset) {JavaScript}: - * x / 2 - */ + rep language "JavaScript" /* x / 2 */ } part def Gauge { attribute t_sim : ScalarValues::Real default = 4.0; @@ -85,87 +83,68 @@ part def Reactor { action cycle { first start then mark; action mark { + rep language "JavaScript" /* started = t_sim; */ assign started := localClock.currentTime; } first mark then 'loop'; action 'loop' { - /* body not migrated (the construct "for" is outside the translated subset) {JavaScript}: - * for (var k = 0; k < 3; k++) { power = power + 1; } - */ + rep language "JavaScript" /* for (var k = 0; k < 3; k++) { power = power + 1; } */ } first 'loop' then alloc; action alloc { - /* body not migrated (the construct "new" is outside the translated subset) {JavaScript}: - * power = new Number(4); - */ + rep language "JavaScript" /* power = new Number(4); */ } first alloc then trim; action trim { - /* body not migrated (the call "label.trim" is not in the translated function table) {JavaScript}: - * label = label.trim(); - */ + rep language "JavaScript" /* label = label.trim(); */ } first trim then self2; action self2 { - /* body not migrated (the name "this.owner.power" resolves to nothing readable: Reactor has no feature owner) {JavaScript}: - * power = this.owner.power; - */ + rep language "JavaScript" /* power = this.owner.power; */ } first self2 then stray; action stray { - /* body not migrated (the name "nowhere" resolves to nothing readable: nothing visible from Reactor::Cycle::stray is called nowhere) {JavaScript}: - * power = nowhere + 1; - */ + rep language "JavaScript" /* power = nowhere + 1; */ } first stray then mixed; action mixed { - /* body not migrated (the construct "+" is outside the translated subset: string concatenation has no v2 form in the subset) {JavaScript}: - * power = label + 1; - */ + rep language "JavaScript" /* power = label + 1; */ } first mixed then python; action python { + rep language "Python" /* power = power + 1 */ assign power := power + 1; } first python then partial; action partial { - /* body not migrated (the call "label.trim" is not in the translated function table) {JavaScript}: - * power = power + 1; label = label.trim(); - */ + rep language "JavaScript" /* power = power + 1; label = label.trim(); */ } first partial then shadow; action shadow { - /* body not migrated (the construct "var power" is outside the translated subset: power is already a feature here, which a declaration would shadow) {JavaScript}: - * var power = 1; power += 1; - */ + rep language "JavaScript" /* var power = 1; power += 1; */ } first shadow then pinned; action pinned { in x : ScalarValues::Real[1] = 1.0; - /* body not migrated (the construct "var x" is outside the translated subset: x is already a feature here, which a declaration would shadow) {JavaScript}: - * var x = 1; x += 1; - */ + rep language "JavaScript" /* var x = 1; x += 1; */ } first pinned then relay; action relay { in x : ScalarValues::Real[1] = 1.0; out y : ScalarValues::Real[1]; - /* body not migrated (the construct "x" is outside the translated subset: an input pin is not assigned) {JavaScript}: - * y = x * 2; x = y; - */ + rep language "JavaScript" /* y = x * 2; x = y; */ } first relay then gain; action gain { in x : ScalarValues::Real[1] = 1.5; out y : ScalarValues::Real[1]; + rep language "JavaScript" /* y = x * power; */ assign y := x * power; } first gain then bare; action bare { in raw[1] = 1.0; - /* body not migrated (the name "raw.level" resolves to nothing readable: Reactor::Cycle::bare::raw has no type, so no feature level) {JavaScript}: - * power = raw.level; - */ + rep language "JavaScript" /* power = raw.level; */ } first bare then final; action final terminate; diff --git a/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml b/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml index 472a896a16..34e8041f8c 100644 --- a/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml +++ b/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml @@ -6,17 +6,25 @@ part def Executive { action idle { out z : ScalarValues::Real[1]; out y : ScalarValues::Real[1]; + rep language "JavaScript" /* tdiff = tdiff * 1; */ assign tdiff := tdiff * 1; } first idle then clamp; action clamp { in w : ScalarValues::Real[0..1]; + rep language "JavaScript" /* tMax = w; + * tdiff = tMax * 2; + */ if w->SequenceFunctions::notEmpty() { assign tMax := w; } assign tdiff := tMax * 2; } first clamp then settle; action settle { in w : ScalarValues::Real[0..1]; + rep language "JavaScript" /* tMax = 1; + * tMax = w; + * tdiff = tMax * 2; + */ assign tMax := 1; if w->SequenceFunctions::notEmpty() { assign tMax := w; } assign tdiff := tMax * 2; @@ -25,6 +33,10 @@ part def Executive { action pick { in w : ScalarValues::Real[0..1]; in v : ScalarValues::Real[0..1]; + rep language "JavaScript" /* tMax = w; + * tMax = v; + * tdiff = tMax * 2; + */ if w->SequenceFunctions::notEmpty() { assign tMax := w; } if v->SequenceFunctions::notEmpty() { assign tMax := v; } assign tdiff := tMax * 2; @@ -33,6 +45,10 @@ part def Executive { action early { in w : ScalarValues::Real[0..1]; in v : ScalarValues::Real[0..1]; + rep language "JavaScript" /* tMax = w; + * tdiff = tMax * 2; + * tMax = v; + */ if w->SequenceFunctions::notEmpty() { assign tMax := w; } assign tdiff := tMax * 2; if v->SequenceFunctions::notEmpty() { assign tMax := v; } diff --git a/tests/migrate/testdata/xmi/script_locals.golden.report.txt b/tests/migrate/testdata/xmi/script_locals.golden.report.txt index 7be4e2d3bd..405349f76d 100644 --- a/tests/migrate/testdata/xmi/script_locals.golden.report.txt +++ b/tests/migrate/testdata/xmi/script_locals.golden.report.txt @@ -3,8 +3,8 @@ ## approximated (5) OpaqueBehavior Executive::Executive Behavior::::::diffTime _diffTime -> Executive::'Executive Behavior'::unnamed2::diffTime (the JavaScript body is translated to v2; the clock variable simtime, named by the configuration Timing, reads the local clock; t1 is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it; tdiffOld is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it; the console print print(…) is left out, as it writes to the tool's console and changes nothing of the model; the JavaScript body is written as v2 assignments) -OpaqueBehavior Executive::Executive Behavior::::::javaDiff _javaDiff -> Executive::'Executive Behavior'::unnamed3::javaDiff (the body is kept as a comment: the name "tdiffOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::javaDiff is called tdiffOld) -OpaqueBehavior Executive::Executive Behavior::::::readFirst _readFirst -> Executive::'Executive Behavior'::unnamed4::readFirst (the body is kept as a comment: the name "stepsOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::readFirst is called stepsOld) +OpaqueBehavior Executive::Executive Behavior::::::javaDiff _javaDiff -> Executive::'Executive Behavior'::unnamed3::javaDiff (the body is kept as a textual representation, which is not executed: the name "tdiffOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::javaDiff is called tdiffOld) +OpaqueBehavior Executive::Executive Behavior::::::readFirst _readFirst -> Executive::'Executive Behavior'::unnamed4::readFirst (the body is kept as a textual representation, which is not executed: the name "stepsOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::readFirst is called stepsOld) OpaqueBehavior Executive::Executive Behavior::::::stamp _stamp -> Executive::'Executive Behavior'::unnamed::stamp (the JavaScript body is translated to v2; the clock variable simtime, named by the configuration Timing, reads the local clock; the JavaScript body is written as v2 assignments) «SimulationConfig» Class Timing _config -> Timing (neither Executive nor any general of it has a classifier behavior, so the configuration only holds 'Executive') diff --git a/tests/migrate/testdata/xmi/script_locals.golden.sysml b/tests/migrate/testdata/xmi/script_locals.golden.sysml index 88c1a1a579..6f2d84e230 100644 --- a/tests/migrate/testdata/xmi/script_locals.golden.sysml +++ b/tests/migrate/testdata/xmi/script_locals.golden.sysml @@ -12,6 +12,7 @@ part def Executive { state def 'Executive Behavior' { in ref context : Executive[1]; entry action stamp { + rep language "JavaScript" /* t0 = simtime; */ assign context.t0 := localClock.currentTime; } then Aligning; @@ -21,6 +22,12 @@ part def Executive { state Idle; transition first Aligning accept after 3.0 [SI::s] do action diffTime { + rep language "JavaScript" /* t1 = simtime; + * tdiffOld = tdiff; + * tdiff = (t1 - t0); + * tStep = tdiff - tdiffOld; + * print("step " + tStep); + */ attribute t1 : ScalarValues::Real; assign t1 := localClock.currentTime; attribute tdiffOld : ScalarValues::Real; @@ -31,16 +38,14 @@ part def Executive { then Phasing; transition first Phasing accept after 2.0 [SI::s] do action javaDiff { - /* body not migrated (the name "tdiffOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::javaDiff is called tdiffOld) {Java}: - * tdiffOld = tdiff; + rep language "Java" /* tdiffOld = tdiff; * tdiff = tdiff + 1.0; */ } then Counting; transition first Counting do action readFirst { - /* body not migrated (the name "stepsOld" resolves to nothing readable: nothing visible from Executive::Executive Behavior::::::readFirst is called stepsOld) {JavaScript}: - * steps = steps + stepsOld; + rep language "JavaScript" /* steps = steps + stepsOld; * stepsOld = steps; */ } diff --git a/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml b/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml index 6f724c5bff..90bf7fecb8 100644 --- a/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml +++ b/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml @@ -12,6 +12,7 @@ part def Executive { state def 'Executive Behavior' { in ref context : Executive[1]; entry action stamp { + rep language "JavaScript" /* t0 = simtime; */ assign context.t0 := localClock.currentTime; } then Aligning; @@ -21,6 +22,11 @@ part def Executive { state Idle; transition first Aligning accept after 3.0 [SI::s] do action diffTime { + rep language "JavaScript" /* t1 = simtime; + * tdiffOld = tdiff; + * tdiff = t1 - t0; + * tStep = tdiff - tdiffOld; + */ attribute t1 : ScalarValues::Real; assign t1 := localClock.currentTime; attribute tdiffOld : ScalarValues::Real; @@ -31,12 +37,18 @@ part def Executive { then Phasing; transition first Phasing accept after 2.0 [SI::s] do action reset { + rep language "JavaScript" /* tdiff = 0; + * tStep = tdiff + steps; + */ assign context.tdiff := 0; assign context.tStep := context.tdiff + context.steps; } then Counting; transition first Counting do action count { + rep language "JavaScript" /* steps++; + * tdiff += steps; + */ assign context.steps := context.steps + 1; assign context.tdiff := context.tdiff + context.steps; } diff --git a/tests/migrate/testdata/xmi/station_points.golden.sysml b/tests/migrate/testdata/xmi/station_points.golden.sysml index 34ce3cbe8e..9d382ea371 100644 --- a/tests/migrate/testdata/xmi/station_points.golden.sysml +++ b/tests/migrate/testdata/xmi/station_points.golden.sysml @@ -18,10 +18,12 @@ part def Station { state Idle; state Work { entry action { + rep language "JavaScript" /* trace = trace * 100 + 11; */ assign context.trace := context.trace * 100 + 11; } then Prep; exit action { + rep language "JavaScript" /* trace = trace * 100 + 12; */ assign context.trace := context.trace * 100 + 12; } #StateMachines::junction state Start; @@ -29,15 +31,18 @@ part def Station { #StateMachines::junction state Leave; state Prep { entry action { + rep language "JavaScript" /* trace = trace * 100 + 13; */ assign context.trace := context.trace * 100 + 13; } } state Run { entry action { + rep language "JavaScript" /* trace = trace * 100 + 14; */ assign context.trace := context.trace * 100 + 14; } then Slow; exit action { + rep language "JavaScript" /* trace = trace * 100 + 15; */ assign context.trace := context.trace * 100 + 15; } #StateMachines::junction state Deep; @@ -45,16 +50,19 @@ part def Station { state Slow; state Fast { entry action { + rep language "JavaScript" /* trace = trace * 100 + 17; */ assign context.trace := context.trace * 100 + 17; } } transition first Deep do action { + rep language "JavaScript" /* trace = trace * 100 + 21; */ assign context.trace := context.trace * 100 + 21; } then Fast; transition first Fast accept Back do action { + rep language "JavaScript" /* trace = trace * 100 + 22; */ assign context.trace := context.trace * 100 + 22; } then Out; @@ -63,27 +71,32 @@ part def Station { transition first Prep accept Next then Run; transition first Start do action { + rep language "JavaScript" /* trace = trace * 100 + 23; */ assign context.trace := context.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 { + rep language "JavaScript" /* trace = trace * 100 + 24; */ assign context.trace := context.trace * 100 + 24; } then Leave; transition first Run.Out do action { + rep language "JavaScript" /* trace = trace * 100 + 25; */ assign context.trace := context.trace * 100 + 25; } then Prep; } state Sync { entry action { + rep language "JavaScript" /* trace = trace * 100 + 31; */ assign context.trace := context.trace * 100 + 31; } then regions; exit action { + rep language "JavaScript" /* trace = trace * 100 + 32; */ assign context.trace := context.trace * 100 + 32; } fork Both; @@ -94,9 +107,11 @@ part def Station { state A0; state A1 { entry action { + rep language "JavaScript" /* trace = trace * 100 + 33; */ assign context.trace := context.trace * 100 + 33; } exit action { + rep language "JavaScript" /* trace = trace * 100 + 34; */ assign context.trace := context.trace * 100 + 34; } } @@ -107,9 +122,11 @@ part def Station { state B0; state B1 { entry action { + rep language "JavaScript" /* trace = trace * 100 + 35; */ assign context.trace := context.trace * 100 + 35; } exit action { + rep language "JavaScript" /* trace = trace * 100 + 36; */ assign context.trace := context.trace * 100 + 36; } } @@ -120,12 +137,14 @@ part def Station { transition first Both then regions.a.A1; transition first regions.a.A1 do action { + rep language "JavaScript" /* trace = trace * 100 + 37; */ assign context.trace := context.trace * 100 + 37; } then Gather; transition first Both then regions.b.B1; transition first regions.b.B1 do action { + rep language "JavaScript" /* trace = trace * 100 + 38; */ assign context.trace := context.trace * 100 + 38; } then Gather; @@ -133,6 +152,7 @@ part def Station { } transition first Idle accept Go do action { + rep language "JavaScript" /* trace = trace * 100 + 41; */ assign context.trace := context.trace * 100 + 41; } then Work.Start; @@ -140,6 +160,7 @@ part def Station { transition first Idle accept Enter then Work; transition first Idle accept Dive do action { + rep language "JavaScript" /* trace = trace * 100 + 42; */ assign context.trace := context.trace * 100 + 42; } then Work.Run.Deep; @@ -147,11 +168,13 @@ part def Station { /* 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 { + rep language "JavaScript" /* trace = trace * 100 + 43; */ assign context.trace := context.trace * 100 + 43; } then Idle; transition first Work accept Stop do action { + rep language "JavaScript" /* trace = trace * 100 + 44; */ assign context.trace := context.trace * 100 + 44; } then Idle; @@ -159,6 +182,7 @@ part def Station { transition first Idle accept Split then Sync.Both; transition first Sync.Gather do action { + rep language "JavaScript" /* trace = trace * 100 + 46; */ assign context.trace := context.trace * 100 + 46; } then Idle; diff --git a/tests/migrate/testdata/xmi/submachine_params.golden.sysml b/tests/migrate/testdata/xmi/submachine_params.golden.sysml index ba3230ee75..0d7a18d012 100644 --- a/tests/migrate/testdata/xmi/submachine_params.golden.sysml +++ b/tests/migrate/testdata/xmi/submachine_params.golden.sysml @@ -9,6 +9,7 @@ part def Plant { in x[1]; in ref :>> context = Line::context; entry action tally { + rep language "JavaScript" /* count = count + 1; */ assign context.count := context.count + 1; } } diff --git a/tests/migrate/testdata/xmi/user_library.golden.report.txt b/tests/migrate/testdata/xmi/user_library.golden.report.txt index 892b0e77cc..33cda81564 100644 --- a/tests/migrate/testdata/xmi/user_library.golden.report.txt +++ b/tests/migrate/testdata/xmi/user_library.golden.report.txt @@ -1,16 +1,15 @@ # SysML v1 to v2 migration report: user_library.xmi # exported by MagicDraw UML -# migrated 20 element(s): 16 mapped, 3 approximated, 1 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 20 element(s): 17 mapped, 2 approximated, 1 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## unmapped (1) CallBehaviorAction Recorder::Tally::get _get -> get (the behavior fUML_Library::PrimitiveBehaviors::ListFunctions::ListGet it calls has no v2 declaration) -## approximated (3) +## approximated (2) OutputPin Recorder::Tally::get::result _getOut -> Recorder::Tally::get.result (it is declared admitting no value: it belongs to 'get', which is not migrated and produces no value) -FunctionBehavior fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize _userListSize -> fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize (the body is kept as a comment: the body is empty) Parameter fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize::result _userListSizeResult -> fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize::result (the return parameter is written as an out parameter) -## mapped (16) +## mapped (17) Model Model _m (the root model's members are written at the top level) «Block» Class Recorder _recorder -> Recorder Activity Recorder::Tally _tally -> Recorder::Tally @@ -26,4 +25,5 @@ OutputPin Recorder::Tally::size::result _sizeOut -> Recorder::Tally::size.result Package fUML_Library _userLibrary -> fUML_Library Package fUML_Library::PrimitiveBehaviors _userPrimitives -> fUML_Library::PrimitiveBehaviors Package fUML_Library::PrimitiveBehaviors::ListFunctions _userLists -> fUML_Library::PrimitiveBehaviors::ListFunctions +FunctionBehavior fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize _userListSize -> fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize Parameter fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize::list _userListSizeList -> fUML_Library::PrimitiveBehaviors::ListFunctions::ListSize::list diff --git a/tests/migrate/testdata/xmi/user_library.golden.sysml b/tests/migrate/testdata/xmi/user_library.golden.sysml index 1cad9d655b..d5ed507b32 100644 --- a/tests/migrate/testdata/xmi/user_library.golden.sysml +++ b/tests/migrate/testdata/xmi/user_library.golden.sysml @@ -4,7 +4,6 @@ package fUML_Library { action def ListSize { in list : ScalarValues::String[0..*] ordered nonunique; out result : ScalarValues::Integer[1]; - /* body not migrated (the body is empty) */ } } } diff --git a/tests/migrate/testdata/xmi/weighted_decision.golden.report.txt b/tests/migrate/testdata/xmi/weighted_decision.golden.report.txt index 6e8e8abef7..66da5f1766 100644 --- a/tests/migrate/testdata/xmi/weighted_decision.golden.report.txt +++ b/tests/migrate/testdata/xmi/weighted_decision.golden.report.txt @@ -1,13 +1,10 @@ # SysML v1 to v2 migration report: weighted_decision.xmi -# migrated 16 element(s): 12 mapped, 4 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 16 element(s): 15 mapped, 1 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (4) +## approximated (1) «Probability» ControlFlow Sorter::Route:: _eReject («Probability» from http://example.com/routing/profile is not the SysML profile's; its probability is not read; the edge carries no «Probability»: it is weighted 1.0 - (bias + 0.3), its share of what the marked branches leave of 1, read when the decision is reached) -OpaqueAction Sorter::Route::left _left -> left (the body is kept as a comment: the body is empty) -OpaqueAction Sorter::Route::reject _reject -> reject (the body is kept as a comment: the body is empty) -OpaqueAction Sorter::Route::right _right -> right (the body is kept as a comment: the body is empty) -## mapped (12) +## mapped (15) Model Sorter _m (the root model's members are written at the top level) «Block» Class Sorter _sorter -> Sorter Activity Sorter::Route _act -> Sorter::route (written as an action usage every object of Sorter performs from creation, so its body runs on the object and reaches its features) @@ -19,4 +16,7 @@ ControlFlow Sorter::Route:: _fRight ControlFlow Sorter::Route:: _fReject DecisionNode Sorter::Route:: _decide -> 'decide' InitialNode Sorter::Route:: _init -> start +OpaqueAction Sorter::Route::left _left -> left (its body is empty, so it is written as an action that does nothing) +OpaqueAction Sorter::Route::reject _reject -> reject (its body is empty, so it is written as an action that does nothing) +OpaqueAction Sorter::Route::right _right -> right (its body is empty, so it is written as an action that does nothing) Property Sorter::bias _bias -> Sorter::bias diff --git a/tests/migrate/testdata/xmi/weighted_decision.golden.sysml b/tests/migrate/testdata/xmi/weighted_decision.golden.sysml index 6436a3b36e..b8020be9a5 100644 --- a/tests/migrate/testdata/xmi/weighted_decision.golden.sysml +++ b/tests/migrate/testdata/xmi/weighted_decision.golden.sysml @@ -6,17 +6,11 @@ part def Sorter { first 'decide' then left { @Stochastic::Probability { p = bias; } } first 'decide' then right { @Stochastic::Probability { p = 0.3; } } first 'decide' then reject { @Stochastic::Probability { p = 1.0 - (bias + 0.3); } } - action left { - /* body not migrated (the body is empty) */ - } + action left; first left then 'merge'; - action right { - /* body not migrated (the body is empty) */ - } + action right; first right then 'merge'; - action reject { - /* body not migrated (the body is empty) */ - } + action reject; first reject then 'merge'; merge 'merge'; first 'merge' then final; From e9bb778b0d6a037a55f140c611f6fe7186f4ce3e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:29:01 +0000 Subject: [PATCH 02/19] feat(migrate): map terminate pseudostates to actions Co-Authored-By: jason.han --- internal/translate/migrate/states.go | 22 +-- tests/migrate/behavior_more_test.go | 13 +- tests/migrate/migrate_test.go | 1 + tests/migrate/terminate_test.go | 128 ++++++++++++++++++ .../terminate_orthogonal.golden.report.txt | 27 ++++ .../xmi/terminate_orthogonal.golden.sysml | 25 ++++ .../testdata/xmi/terminate_orthogonal.xmi | 34 +++++ 7 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 tests/migrate/terminate_test.go create mode 100644 tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt create mode 100644 tests/migrate/testdata/xmi/terminate_orthogonal.golden.sysml create mode 100644 tests/migrate/testdata/xmi/terminate_orthogonal.xmi diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index 3d3c4a5675..86469fbaf3 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -601,6 +601,8 @@ func vertexBase(v *sysmlv1.Element) string { return k case "shallowHistory", "deepHistory": return "history" + case "terminate": + return "terminated" } } return "" @@ -1141,7 +1143,10 @@ func (s *stateRegion) vertex(v *sysmlv1.Element) { s.m.madeUp(v, name) s.m.add(v, Mapped, name, "written as a `#StateMachines::deepHistory state` deep history, which re-enters the innermost states active when its state was last left") 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") + name := writeName(s.name(v)) + s.m.w.line(actionKw + name + " terminate;") + s.m.madeUp(v, name) + s.m.add(v, Mapped, name, "written as a terminate action, which ends the state machine's performance in every region") case "entryPoint", "exitPoint": if pointOwner(v).Type == "State" { // Written in the body of the state it belongs to. @@ -1837,10 +1842,9 @@ func (s *stateRegion) acceptsSignal(t, sig *sysmlv1.Element) bool { // transitionWritten reports whether transition t is one the output writes, so // it accepts its trigger or completes its source: its ends resolve as -// transitionEnds requires, and its target is one target names — a state of the -// machine, a final or terminate state of the transition's own region, a -// pseudostate with a v2 form, or a connection point reference into a -// submachine state. It decides as target does, noting nothing. +// transitionEnds requires, and its target is one target names — a state, final +// or terminate action, pseudostate with a v2 form, or connection point reference +// into a submachine state. It decides as target does. func (s *stateRegion) transitionWritten(t *sysmlv1.Element) bool { src, tgt := s.m.model.Ref(t, "source"), s.m.model.Ref(t, "target") internal := t.Attrs["kind"] == "internal" @@ -1865,7 +1869,7 @@ func (s *stateRegion) transitionWritten(t *sysmlv1.Element) bool { case "Pseudostate": switch pseudoKind(tgt) { case "terminate": - return true + return named(tgt) case "exitPoint": return s.m.points[tgt].why == "" && (named(tgt) || pointOwner(tgt).Type != "State") case "entryPoint": @@ -2355,8 +2359,8 @@ func (s *stateRegion) endpoint(t, v *sysmlv1.Element, role string) (string, bool } // target names what a transition leads to: a state or pseudostate of the machine -// by its path, done for a final or terminate state of the region, or the entry -// state of a submachine state a connection point reference enters. +// by its path, done for a final state of the region, or the entry state of a +// submachine state a connection point reference enters. func (s *stateRegion) target(t, v *sysmlv1.Element) (string, bool) { if v == nil { return "", false @@ -2373,7 +2377,7 @@ func (s *stateRegion) target(t, v *sysmlv1.Element) (string, bool) { case "Pseudostate": switch pseudoKind(v) { case "terminate": - return "done", true + return s.endpoint(t, v, "target") case "exitPoint": if s.m.points[v].why != "" { return "", false diff --git a/tests/migrate/behavior_more_test.go b/tests/migrate/behavior_more_test.go index b28907cbdd..7f86296081 100644 --- a/tests/migrate/behavior_more_test.go +++ b/tests/migrate/behavior_more_test.go @@ -2378,12 +2378,10 @@ func TestDeferralNotesTransitionsItCannotOutrank(t *testing.T) { } } -// A transition the writer names by a route other than a vertex's own name — -// into a terminate pseudostate, written to done, or into a submachine state -// through a connection point reference, written to the entry point's state — -// counts as written for the deferral analysis as any other: an unguarded -// completion transition of either kind drops the deferral, since the accept -// loop would never let it fire, and a triggered one takes the signal. +// A transition to a terminate pseudostate, written as a terminate action, or +// through a submachine state by a connection point reference counts as written +// for deferral analysis: an unguarded completion transition drops the deferral, +// since its accept loop would never let it fire, and a triggered one takes it. func TestDeferralCountsTransitionsToUnnamedTargets(t *testing.T) { const terminating = ` @@ -2402,7 +2400,8 @@ func TestDeferralCountsTransitionsToUnnamedTargets(t *testing.T) { ` r := migrateDocumentOptions(t, terminating, ``, migrate.Options{}) - wantLine(t, r.Notation, "transition first Off then done;") + wantLine(t, r.Notation, "action terminated terminate;") + wantLine(t, r.Notation, "transition first Off then terminated;") wantNoLine(t, r.Notation, "item deferred : Door[*] ordered;") wantNote(t, r, "_dDoor", migrate.Unmapped, "the completion transition (_tEnd) leaves the state once its do action ends, which the accept loop that would keep Door never lets it, so the deferral is dropped") diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go index 582304a367..6b791b2b3f 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -390,6 +390,7 @@ var constructFixtures = []string{ "decision_else", "calc_context", "interaction_context", + "terminate_orthogonal", } // migrateFixtureFile migrates testdata/xmi/.xmi. diff --git a/tests/migrate/terminate_test.go b/tests/migrate/terminate_test.go new file mode 100644 index 0000000000..b9bef1a006 --- /dev/null +++ b/tests/migrate/terminate_test.go @@ -0,0 +1,128 @@ +package migrate_test + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/translate/migrate" +) + +func TestTerminatePseudostateEndsOrthogonalMachine(t *testing.T) { + r := migrateFixtureFile(t, "terminate_orthogonal") + for _, line := range []string{ + "state def Run {", + "entry; then regions;", + "state regions parallel {", + "state R1 {", + "state R2 {", + "action terminated terminate;", + "transition first A accept Stop then terminated;", + "transition first B accept Ping then C;", + } { + wantLine(t, r.Notation, line) + } + wantNote(t, r, "_end", migrate.Mapped, "written as a terminate action, which ends the state machine's performance in every region") + wantClean(t, "terminateOrthogonal", r) + + s := session(t, r) + meta(t, s, "%instantiate Machine") + meta(t, s, "%state Machine::Run #1") + if out := meta(t, s, "%send Stop"); !strings.Contains(out, "transition A -> terminated fires on it") { + t.Fatalf("%%send Stop: %s", out) + } + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Execution state: Terminated") { + t.Fatalf("Stop did not end the machine:\n%s", out) + } + if out := meta(t, s, "%send Ping"); strings.Contains(out, "Accepted by") || strings.Contains(out, "transition B -> C fires") { + t.Fatalf("the terminated machine still accepted Ping:\n%s", out) + } + if out := meta(t, s, "%current"); !strings.Contains(out, "Execution state: Terminated") { + t.Fatalf("the sibling region remained active after Stop:\n%s", out) + } + + prior := &migrate.Result{Notation: []byte(` +item def Stop; +item def Ping; +part def Machine { + state def Run { + entry; then regions; + state regions parallel { + state R1 { + entry; then A; + state A; + transition first A accept Stop then done; + } + state R2 { + entry; then B; + state B; + state C; + transition first B accept Ping then C; + } + } + transition first regions then done; + } + exhibit state run : Run; +}`)} + s = session(t, prior) + meta(t, s, "%instantiate Machine") + meta(t, s, "%state Machine::Run #1") + stop := meta(t, s, "%send Stop") + step := meta(t, s, "%step") + ping := meta(t, s, "%send Ping") + if !strings.Contains(ping, "transition B -> C fires on it") { + t.Fatalf("the prior done-target form did not preserve sibling behavior:\nsend Stop:\n%s\nstep:\n%s\nsend Ping:\n%s", stop, step, ping) + } + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "C") { + t.Fatalf("the prior done-target form did not advance the sibling region:\n%s", out) + } +} + +func TestTerminatePseudostateInsideCompositeState(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + + + + + + + + + + + + + + + `, + ``) + wantLine(t, r.Notation, "action terminated terminate;") + wantLine(t, r.Notation, "transition first A accept Stop then terminated;") + wantNote(t, r, "_end", migrate.Mapped, "written as a terminate action") + wantClean(t, "terminateInsideComposite", r) +} + +func TestUntargetedTerminatePseudostateIsWrittenCleanly(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + + + + `, + ``) + wantLine(t, r.Notation, "action terminated terminate;") + wantNote(t, r, "_end", migrate.Mapped, "written as a terminate action") + wantClean(t, "untargetedTerminate", r) +} diff --git a/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt b/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt new file mode 100644 index 0000000000..2092a56663 --- /dev/null +++ b/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt @@ -0,0 +1,27 @@ +# SysML v1 to v2 migration report: terminate_orthogonal.xmi +# migrated 21 element(s): 20 mapped, 1 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) + +## approximated (1) +StateMachine Machine::Run _run -> Machine::Run (the classifier behavior is run by every object of Machine as its usage run) + +## mapped (20) +SignalEvent _stopEvent (written where a trigger refers to it, as accept Stop) +SignalEvent _pingEvent (written where a trigger refers to it, as accept Ping) +«Block» Class Machine _machine -> Machine +Region Machine::Run::R1 _r1 -> R1 (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 Machine::Run::R1:: _r1Initial (written as the entry of the region) +Pseudostate Machine::Run::R1:: _end -> terminated (written as a terminate action, which ends the state machine's performance in every region) +Transition Machine::Run::R1:: _r1Start +Transition Machine::Run::R1:: _stopTransition +Trigger Machine::Run::R1:::: _stopTrigger +State Machine::Run::R1::A _a -> A +Region Machine::Run::R2 _r2 -> R2 (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 Machine::Run::R2:: _r2Initial (written as the entry of the region) +Transition Machine::Run::R2:: _r2Start +Transition Machine::Run::R2:: _pingTransition +Trigger Machine::Run::R2:::: _pingTrigger +State Machine::Run::R2::B _b -> B +State Machine::Run::R2::C _c -> C +Model Model _m (the root model's members are written at the top level) +Signal Ping _ping -> Ping +Signal Stop _stop -> Stop diff --git a/tests/migrate/testdata/xmi/terminate_orthogonal.golden.sysml b/tests/migrate/testdata/xmi/terminate_orthogonal.golden.sysml new file mode 100644 index 0000000000..eb17fcc615 --- /dev/null +++ b/tests/migrate/testdata/xmi/terminate_orthogonal.golden.sysml @@ -0,0 +1,25 @@ +item def Stop; +item def Ping; +part def Machine { + state def Run { + entry; then regions; + state regions parallel { + state R1 { + entry; then A; + state A; + action terminated terminate; + transition first A accept Stop then terminated; + metadata MigrationMetadata::SynthesizedName about terminated; + } + state R2 { + entry; then B; + state B; + state C; + transition first B accept Ping then C; + } + } + transition first regions then done; + metadata MigrationMetadata::SynthesizedName about regions; + } + exhibit state run : Run; +} diff --git a/tests/migrate/testdata/xmi/terminate_orthogonal.xmi b/tests/migrate/testdata/xmi/terminate_orthogonal.xmi new file mode 100644 index 0000000000..6d56047521 --- /dev/null +++ b/tests/migrate/testdata/xmi/terminate_orthogonal.xmi @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8f78697c28ee68a8f0a9e8fa875f19111c74861c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 04:15:42 +0000 Subject: [PATCH 03/19] fix(lower): accept metadata annotations in parallel bodies Co-Authored-By: jason.han --- .../robustness_parallel_wrapper_test.go | 59 +++++++++++++++++++ ...tate_parallel_deferred_event.expected.json | 9 +++ .../state_parallel_deferred_event.sysml | 35 +++++++++++ internal/ir/lower/state_graph.go | 5 +- internal/ir/lower/state_metadata_test.go | 43 ++++++++++++++ 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 internal/exec/runtime/robustness_parallel_wrapper_test.go create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.sysml diff --git a/internal/exec/runtime/robustness_parallel_wrapper_test.go b/internal/exec/runtime/robustness_parallel_wrapper_test.go new file mode 100644 index 0000000000..ec75183355 --- /dev/null +++ b/internal/exec/runtime/robustness_parallel_wrapper_test.go @@ -0,0 +1,59 @@ +package runtime + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/check/passes" + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" +) + +func TestRuntimeRobustnessParallelWrapper(t *testing.T) { + err := stateRunErrorForSource(t, "Machine", ` + package test { + state def Machine { + entry; then S; + state S parallel { + state R { + state A; + } + } + } + } + `) + if err == nil { + t.Fatal("a wrapped region without an entry completed") + } + message := strings.ToLower(err.Error()) + if !strings.Contains(message, "initial") && !strings.Contains(message, "deadlock") && !strings.Contains(message, "stuck") { + t.Fatalf("error = %v; want a typed missing-entry error or a stuck/deadlock report", err) + } +} + +func TestRuntimeRobustnessParallelWrapperRejectsUnresolvedMetadata(t *testing.T) { + src := ` + package test { + state def Machine { + entry; then S; + state S parallel { + @NoSuchMetadata; + state R { + entry; then A; + state A; + } + } + } + } + ` + root := parseAndBuild(t, src) + idx := libs.NewModelIndex() + idx.AddDocument("", root) + idx.ExpandWildcardImports() + diagnostics := passes.Analyze("", root, nil, idx) + for _, diagnostic := range diagnostics { + if strings.Contains(diagnostic.Message, "NoSuchMetadata") { + return + } + } + t.Fatalf("unresolved parallel-state annotation produced no diagnostic: %v", diagnostics) +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.expected.json new file mode 100644 index 0000000000..2f95d8f984 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.expected.json @@ -0,0 +1,9 @@ +{ + "type": "state", + "libraries": true, + "events": [ + {"signal": "Ping"}, + {"signal": "Leave"} + ], + "finalState": "done" +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.sysml b/internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.sysml new file mode 100644 index 0000000000..30f559feff --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_deferred_event.sysml @@ -0,0 +1,35 @@ +// A parallel state owns a DeferredEvent annotation and the migrator's standard +// deferred-signal encoding. Ping is kept while Busy is active and replayed to +// Ready after Leave exits Busy. +package ParallelDeferredEvent { + item def Ping; + item def Leave; + + state def Machine { + entry; then start; + state start; + state Busy parallel { + @MigrationMetadata::DeferredEvent { ref :>> signal : Ping; } + item deferred : Ping[*] ordered; + do action buffer { + first start then receive; + action receive accept kept : Ping; + then action keep { assign deferred := SequenceFunctions::including(deferred, receive.kept); } + then receive; + } + exit action flush { + for kept in deferred { send kept to self; } + then action clear { assign deferred := (); } + } + state R { + entry; then active; + state active; + } + } + state Ready; + + succession first start then Busy; + transition first Busy accept Leave then Ready; + transition first Ready accept Ping then done; + } +} diff --git a/internal/ir/lower/state_graph.go b/internal/ir/lower/state_graph.go index 843dd5a7f1..8c9a49a38a 100644 --- a/internal/ir/lower/state_graph.go +++ b/internal/ir/lower/state_graph.go @@ -1100,11 +1100,12 @@ func isParallelRegionMember(member ast.Node) bool { // parallelOwnedMember reports whether a parallel state may own a member itself // rather than contribute it to a region: its behaviors, the // pseudostates its regions branch through, the edges between them, a metadata -// usage annotating it, and a definition written in its body, which declares a -// type rather than a region. +// usage or prefix annotation annotating it, and a definition written in its +// body, which declares a type rather than a region. func parallelOwnedMember(member ast.Node) bool { switch n := member.(type) { case *ast.Comment, *ast.Documentation, *ast.TextualRepresentation, + *ast.PrefixMetadata, *ast.EntryMember, *ast.DoMember, *ast.ExitMember, *ast.PseudostateNode, *ast.SuccessionEdge, *ast.TransitionEdge, *ast.TransitionMember, diff --git a/internal/ir/lower/state_metadata_test.go b/internal/ir/lower/state_metadata_test.go index c8e3895ee7..d11214df98 100644 --- a/internal/ir/lower/state_metadata_test.go +++ b/internal/ir/lower/state_metadata_test.go @@ -180,3 +180,46 @@ func TestMetadataLocalAliasSpelling(t *testing.T) { t.Fatalf("pseudostates are %v, want [choice:pick]", got) } } + +func TestDeferredSignalsOnParallelState(t *testing.T) { + src := `package M { + item def Ping; + state m parallel { + @MigrationMetadata::DeferredEvent { ref :>> signal : Ping; } + state r { + entry; then active; + state active; + } + } + }` + p := parser.New(source.New("m.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) > 0 { + t.Fatalf("parse errors: %v", p.Diagnostics) + } + idx := libs.NewModelIndex() + idx.AddDocument("m.sysml", root) + idx.ExpandWildcardImports() + pkg, ok := idx.DocumentRoot("m.sysml").LookupLocal("M") + if !ok { + t.Fatal("package M not indexed") + } + sym, ok := pkg.Scope.LookupLocal("m") + if !ok { + t.Fatal("state m not indexed") + } + usage, ok := sym.Decl.(*ast.Usage) + if !ok { + t.Fatalf("state m declaration is %T, want *ast.Usage", sym.Decl) + } + deferred := DeferredSignals(resolve.New(idx), sym.Scope, usage) + if len(deferred) != 1 { + t.Fatalf("DeferredSignals = %v, want one signal", deferred) + } + if got := deferred[0].Type.Text(); got != "Ping" { + t.Fatalf("deferred signal type = %q, want Ping", got) + } + if deferred[0].Scope == nil { + t.Fatal("deferred signal has no state body scope") + } +} From 114f877b963c9f62587e7ac0a3f382e723f9be31 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:10:01 +0000 Subject: [PATCH 04/19] feat(runtime): keep unstarted composite regions inactive Co-Authored-By: jason.han --- .../design/precise-semantics-alignment.md | 60 +++----- docs/project/pssm-referee-baseline.json | 6 +- docs/project/pssm-referee.md | 58 ++++---- docs/project/spec-compliance.md | 3 +- .../robustness_inactive_region_test.go | 28 ++++ .../robustness_parallel_wrapper_test.go | 10 +- internal/exec/runtime/robustness_test.go | 101 ------------- internal/exec/runtime/state_region_entry.go | 9 +- ...ion_entered_by_default.check.expected.json | 22 +++ ...n_entered_by_default.declared.trace.golden | 79 ++++++++++ ...ly_region_entered_by_default.expected.json | 67 +++++++++ ...ion_entered_by_default.seed-1.trace.golden | 79 ++++++++++ ..._fork_only_region_entered_by_default.sysml | 43 ++++++ ...nly_region_entered_by_default.trace.golden | 79 ++++++++++ ...res_inactive_parallel_region.expected.json | 14 ++ ...ry_restores_inactive_parallel_region.sysml | 34 +++++ ...ores_inactive_parallel_region.trace.golden | 36 +++++ ...starts_outer_region_inactive.expected.json | 8 + ...ed_fork_starts_outer_region_inactive.sysml | 23 +++ ..._starts_outer_region_inactive.trace.golden | 7 + ...without_entry_inactive.check.expected.json | 12 ++ ...thout_entry_inactive.declared.trace.golden | 49 +++++++ ...egion_without_entry_inactive.expected.json | 27 ++++ ...without_entry_inactive.seed-1.trace.golden | 49 +++++++ ...rallel_region_without_entry_inactive.sysml | 29 ++++ ...region_without_entry_inactive.trace.golden | 49 +++++++ ...teless_region_with_behaviors.expected.json | 2 +- ...llel_stateless_region_with_behaviors.sysml | 5 +- ...ateless_region_with_behaviors.trace.golden | 8 - internal/ir/lower/fork_plan.go | 138 +----------------- internal/ir/lower/fork_plan_test.go | 116 ++++++++------- internal/ir/lower/state_graph.go | 46 +++--- tools/referee/pssm/classify.go | 113 +------------- tools/referee/pssm/classify_test.go | 9 +- 34 files changed, 892 insertions(+), 526 deletions(-) create mode 100644 internal/exec/runtime/robustness_inactive_region_test.go create mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden diff --git a/docs/internals/design/precise-semantics-alignment.md b/docs/internals/design/precise-semantics-alignment.md index 9382f62f53..6d102ec849 100644 --- a/docs/internals/design/precise-semantics-alignment.md +++ b/docs/internals/design/precise-semantics-alignment.md @@ -1669,7 +1669,7 @@ which supersede the hand count this section was first written with — the moves | The UML `StateMachine` as the class under test (a standalone machine with attributes, operations, a constructor) | `part def` with `attribute`s, `action def`s and `exhibit state`, as an owned machine's: the reader (`reader.go`) reads the machine as the `Target` whose `Machine` is itself, with its attributes, operations and their methods, and its constructor | standard | | A guard whose behavior acts on the model (calls `trace(...)` before returning its value) | none: a v2 guard is a Boolean expression (§7.18.3, `validateTransitionFeatureMembershipGuardExpression`; `bool guard[*]` in `TransitionPerformances.kerml`, the effect a separate `step`), and an expression has no spelling for an action. UML 2.5.1 §14.5.11 `Transition::guard` itself calls such a guard ill formed. Recording the runtime's guard reads as the referee's observable instead is refused too: the reads the suite traces are a junction's on the target's default entry, read at the incoming transition's selection, where the library orders every transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | no translation | | A guard whose behavior is an opaque behavior, not an activity | none: the reader follows an activity's nodes to tell whether the behavior acts, and does not read an opaque body, so the guard is refused rather than carried as its Boolean text alone. A `FunctionBehavior` is the exception — it accesses no object by UML's contract (§13.2.3.3) — and is translated as the expression it spells | no translation | -| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a region a fork enters needs no `entry; then` (finding 6 below, fixed) | extension | +| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a branch enters its target without an `entry; then`, while a region omitted by default entry remains inactive if it has no entry transition (finding 6 below, fixed) | extension | The classification is by construct, in the order of the table: a test whose model uses any construct with no spelling or no translation is counted as not expressible whatever else it @@ -1736,18 +1736,17 @@ is where a later translation moves them back. | *Event 019-E* | standard | *translated since the emitter binds entry parameters and returns outputs:* `T2` accepts `'or'(left, right)` and stores both in `trigger_v_or_left`, `trigger_v_or_right`; the two regions' substate entries declare `in left = trigger_v_or_left; in right = trigger_v_or_right;` and assign the operation's two outputs (`out result`, `out 'return'`), each region's entry writing them in the order the regions are entered, so the caller receives the second region's values and the two admitted traces are both reached and nothing else | | *Deferred 007* | extension | *translated since the emitter spells a returning effect with inputs:* the deferred `op(p1)` call fires `T4` once the second state is active; `T4`'s effect is `action def T4_effect` with `in p` bound to the accept's `p1` and `out 'return'`, its `return` an assignment of `not p`; the one admitted trace, the first state's exit, `T3(effect)`, `T4(effect)[in=true][out=false]` and the tester's `[out=false]`, is reached | | *Standalone 003* | standard | *the standalone machine is read as the target since the reader does so, and translated since the emitter binds entry parameters and returns outputs:* the same shape as *Event 019-E*, the machine itself the class under test with `or` its operation; both admitted traces are reached | -| *Fork 002* | extension | *translated since finding 6 was fixed:* the fork enters the two regions of a nested composite state, which have no initial pseudostate; the lowerer used to refuse a `parallel` region with no `entry; then` — this project's gap, not v2's | -| *Join 001* | extension | *translated since finding 6 was fixed:* the fork enters the two regions of the top-level composite state, which have no initial pseudostate; the same lowerer refusal | +| *Fork 002* | extension | *translated:* the fork enters two regions without initial pseudostates; a no-entry region stays inactive on default entry and is activated by the branch | +| *Join 001* | extension | *translated:* the fork enters two regions without initial pseudostates; omitted no-entry regions stay inactive and block owner completion | | *Choice 005* | extension | *refused, settled:* the guards of the junction's and the choice's four outgoing transitions each call `trace("T1.n(guard)")` and the admitted trace records the calls, to show when each guard is read; a v2 guard is an expression with no room for an action, so the translation keeps only the guard's value and cannot reach the trace, and is refused rather than run short. Making the runtime's guard reads the referee's observable would not reach the trace either: the junction sits on the composite's default entry and the suite reads its guards before `T2(effect)` and the composite's entry, where the library reads a transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | The last two were kept apart from the other seven and from the 29 with no spelling: UML allows -a fork to target states inside orthogonal regions that have no initial pseudostate, SysML v2 -`parallel` regions can spell the shape, and only the lowerer's check stood in the way. The -lowerer now accepts a region a fork enters (finding 6), so the two run and the referee reports -them in its expressible buckets; a region with neither an entry transition nor a fork branch -into it is still refused, and the classifier names that *lowerer refuses an orthogonal region -with neither an entry transition nor a fork branch into it* (*Entry 002 E*, which is not -expressible on other grounds too). +a fork to target states inside orthogonal regions that have no initial pseudostate, and SysML +v2 `parallel` regions can spell the shape. A composite-state region without an entry remains +inactive on default entry, whether or not a fork can enter it; the owner cannot complete until +all regions reach final states. Explicit branches and transitions can enter the region, and +the PSSM classifier no longer records the former lowerer refusal. Entry 002 E remains +not-expressible on its entry-point construct. #### Behavior parameters, operation results, tester traces and standalone machines @@ -2328,35 +2327,18 @@ sites of the runtime's fixed, the pool's order and the do step drawn on the entr of them has to change (second open decision). `state_choice_pseudostate` does not reach it. *Fixed* with the second open decision: the code now matches the note (SM30's *Decided* sentence), and Track E records the finding as landed. -6. **The lowerer refuses a fork into orthogonal regions that have no initial pseudostate.** - UML lets a fork's outgoing transitions enter states inside a composite state's orthogonal - regions directly, with no initial pseudostate in those regions (PSSM *Fork 002* and *Join - 001* are built this way); SysML v2 `parallel` regions can spell the shape, and this - project's `fork` extension can spell the fork. `lower.ToStateGraph` refuses it — "region - `` has no initial state; write `entry; then ;` inside the region" — because it - required every region to name its own start even when a fork was the only way in. A gap of - ours, which the PSSM referee's classifier recorded as *lowerer refuses fork into a region - without an entry transition*. *Fixed:* `lower/fork_plan.go:planForks` reads every fork's - branches into a `ForkPlan` — one target state per orthogonal region of one composite state, - no guard, at least two branches — and `ToStateGraph` accepts a region with no entry - transition when `ForkStarted` says a fork enters it, still refusing one with neither - (`robustness_test.go:fork_leaves_a_region_without_a_way_in`). Such a region has no default - start, so `checkForkOnlyRegion` also refuses a machine where any other way into the composite - — a transition to the composite itself, to a state in another of its regions or to its - history, its own self-transition, or the machine's entry naming it, directly or through a - junction, choice or join — would start the region by default, naming that way in - (`robustness_test.go:fork_only_region_entered_by_default`). The runtime consumes the plan - (`state_executor.go:fireForkTransition` → `state_region_entry.go:enterForkBranches`): the - source configuration is left down to the least common ancestor of the source and the - composite, as for a move to a single state (`leaveForFork`), so an active ancestor is - neither exited nor entered again; then each branch runs its effect, enters what is left of - the way down to the composite and its target directly (PSSM §8.5.7), a region no branch - names taking its own initial. Pinned by `state_fork_enters_regions_without_initial`, - `state_fork_in_composite_enters_parallel_substate`, `state_fork_within_active_ancestor`, - `state_fork_within_active_region` (all with trace goldens) and `lower/fork_plan_test.go`. - *Fork 002* and *Join 001* translate and run; the branches are still entered in the regions' - declaration order, so the interleavings PSSM admits beyond that one are SM22's open decision, - and `docs/project/pssm-referee.md` records where each landed. +6. **A no-entry region of a composite state remains inactive until explicitly entered.** + UML permits a region without an initial pseudostate, and SysML v2 `parallel` can represent + it. `recordRegionInitials` records no default start for such composite-state regions; + `regionStart` returns no state and `enterRegion` leaves the region absent from the active + configuration. This applies whether or not a fork elsewhere can enter the region. A fork + branch or later transition naming a vertex activates it normally. `regionComplete` treats a + missing region state as incomplete, so the owner cannot complete until every region reaches + a final state. The rule is covered by `TestRuntimeRobustnessInactiveRegion`, + `state_parallel_region_without_entry_inactive`, `state_fork_only_region_entered_by_default`, + `state_nested_fork_starts_outer_region_inactive` and + `state_history_restores_inactive_parallel_region`. The PSSM classifier no longer reports + the old lowerer refusal; Entry 002 E remains not-expressible on its entry-point construct. 7. **A transition from a composite state into its own history pseudostate reads the record before the state is left.** The configuration a history restores is written when its owner is exited (`state_executor.go:exitState` → `recordChildHistory`, `recordRegionHistory`), but diff --git a/docs/project/pssm-referee-baseline.json b/docs/project/pssm-referee-baseline.json index 34b96e43ef..7480d20685 100644 --- a/docs/project/pssm-referee-baseline.json +++ b/docs/project/pssm-referee-baseline.json @@ -6,8 +6,8 @@ "url": "https://www.omg.org/spec/PSSM/20181101/PSSM_TestSuite.xmi", "suiteDigest": "c355b249c356774377a46b60345019d827af1ce417bde88e533aa5f39206ae07", "tests": 103, - "recorded": "2026-10-01", - "develop": "deca2b0389f796b02fa1c30a387617d0613a034c" + "recorded": "2026-10-09", + "develop": "88af0fea3f4aac4f59af762701a79d51002ce39b" }, "buckets": { "differs-by-design": 5, @@ -810,7 +810,7 @@ "class": "not-expressible", "bucket": "not-expressible", "reasons": [ - "entry point EntryPoint1; lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it S1/Region1; lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it S1/Region2; local transition T1.1; local transition T2.1" + "entry point EntryPoint1; local transition T1.1; local transition T2.1" ] }, { diff --git a/docs/project/pssm-referee.md b/docs/project/pssm-referee.md index bf2d0cde37..38f071915f 100644 --- a/docs/project/pssm-referee.md +++ b/docs/project/pssm-referee.md @@ -133,8 +133,9 @@ under [Behavior parameters, operation results, tester traces and standalone mach [candidate table](../internals/design/precise-semantics-alignment.md#a-guard-whose-behavior-acts-on-the-model) records the three candidates: the settled refusal of a construct v2 cannot spell faithfully. - **A fork into orthogonal regions that have no initial pseudostate**: *Fork 002*, *Join 001* - — kept apart from the rest while the lowerer refused the shape, and translated since it - accepts it, see [Findings about our own conformance](#findings-about-our-own-conformance). + — translated with the general inactive-region rule: regions without an entry remain + inactive on default entry, and fork branches activate the regions they name; see + [Findings about our own conformance](#findings-about-our-own-conformance). ## Translating @@ -669,9 +670,10 @@ transition when a fork's branch enters it ([finding 6](#findings-about-our-own-c so the two tests the classifier filed under *lowerer refuses fork into a region without an entry transition* translate and run. Both moved `not-expressible` → `fail`; nothing else moved, and the eighteen other failures' reasons are byte-identical to the previous baseline's. Three -`not-expressible` tests that also carried the reason stay where they are on their other grounds: -*Transition 023* and *Standalone 002* drop it (their forks now enter their regions), *Entry 002 E* -keeps the renamed reason (its regions have neither an entry transition nor a fork branch). +`not-expressible` tests that also carried the reason stayed there on other grounds: +*Transition 023* and *Standalone 002* have their regions entered by forks, while *Entry 002 E* +also contains an entry point. The general inactive-region rule below supersedes that former +lowerer refusal. | Test | Row | Movement | Adjudication | |---|---|---|---| @@ -832,10 +834,13 @@ By reason, as the classifier names them: exit left, only by completion transitions or by paths accepting different events). - A standalone state machine is read as the target class, and a tester's `trace(...)` after a call is driven, so neither is a reason any longer; Event 019 A runs and passes. -- **lowerer refuses an orthogonal region with neither an entry transition nor a fork branch - into it** (ours): Entry 002 E, which is not expressible on other grounds too. Fork 002 and - Join 001, filed here while the lowerer refused every region without an entry transition, - translate since finding 6 was fixed. +- A composite-state region without an entry is valid: default entry leaves it inactive until + a transition or fork branch names one of its states, and its owner cannot complete while + that region is inactive. Entry 002 E remains not-expressible on its entry-point construct; + the no-entry refusal is no longer a classifier reason. Coverage includes + `TestRuntimeRobustnessInactiveRegion`, `state_parallel_region_without_entry_inactive`, + `state_fork_only_region_entered_by_default` and + `state_history_restores_inactive_parallel_region`. - **guard side effect** (no translation, settled): Choice 005. Making the runtime's guard reads the referee's observable instead was tried and refused: the suite reads the junction's guards at the incoming transition's selection, before its effect and the target's entry, where @@ -857,30 +862,17 @@ three tests as a translation limit and on two as the suite's defect, and its las a do step on the entry front, *Terminate 002*'s — is fixed. No test now cites an open site of the runtime; a `fail` cites a tool choice, a translation limit or the suite's defect: -- **The lowerer refused a fork into orthogonal regions that have no initial pseudostate** - (*Fork 002*, *Join 001*; alignment finding 6). UML lets a fork's outgoing transitions enter - states inside a composite state's orthogonal regions directly, with no initial pseudostate in - those regions; SysML v2 `parallel` regions can spell the shape and this project's `fork` - extension can spell the fork. `lower.ToStateGraph` refused it ("region `` has no initial - state; write `entry; then ;` inside the region") because it required every region to - name its own start even when a fork was the only way in. The two tests were filed - `not-expressible` under the distinct reason *lowerer refuses fork into a region without an - entry transition* so they were never confused with the constructs v2 has no spelling for. - Fixed: `lower/fork_plan.go` reads each fork's branches into a `ForkPlan` — one target state - per orthogonal region of one composite state — and the lowerer accepts a region a fork enters - without an entry transition of its own, still refusing one with neither (the classifier's - reason is now *lowerer refuses an orthogonal region with neither an entry transition nor a - fork branch into it*, which only *Entry 002 E* still carries, and it reads a branch's region - the way the lowerer does — the region of the fork's composite the target lies in, however - deep); the runtime leaves the source configuration — every region of the composite when - the fork is reached from inside them — runs the first branch's effect, enters the rest of the - way down to the composite, then enters every region in declaration order, each branch's - effect before its target (`state_executor.go:leaveForFork`, - `state_region_entry.go:enterForkBranches`; `state_fork_enters_regions_without_initial`, - `state_fork_in_composite_enters_parallel_substate`, - `state_fork_omitted_region_declared_first` and `state_fork_from_within_owner_regions` with - their trace goldens). The two tests - translate and run; where each landed is in the movements table above. +- **A composite-state region has no entry transition** (alignment finding 6). UML permits + such a region and SysML v2 can spell it as a parallel state's sub-state. The lowerer records + no default start; runtime default entry leaves the region out of the active configuration, + whether or not a fork can enter it. A later explicit transition or fork branch can activate + it, and the owner remains incomplete until every region is final. `regionStart`, + `enterRegion`, `regionComplete` and `stateComplete` implement the rule. The lowerer tests + `TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, + `TestToStateGraph_ForkOmissionLeavesRegionInactive` and + `TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`; runtime fixtures cover default + entry, later transition/fork entry, history restoration and completion blocking. Entry 002 E + remains not-expressible for its entry-point construct, not for an absent region start. - **A transition from a composite state into its own history pseudostate read the record before the state was left** (*History 001-A*, *History 002-D*; alignment finding 7). The configuration a history restores is recorded when its owner is exited diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 59fdfad4fd..8a28f7ef36 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -847,6 +847,7 @@ nothing above. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| +| A composite-state region with no entry transition stays inactive on default entry; its owner cannot complete until every region is final, and a later transition or fork branch may activate it | `lower/state_graph.go` `recordRegionInitials`; `state_region_entry.go` `regionStart`, `enterRegion`, `enterLazily`, `forkEntry`, `enterForkBranches`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `TestToStateGraph_ForkOmissionLeavesRegionInactive`, `TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`; `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens), `state_transition_cross_region_nested_target.sysml` | ✅ Faithful | | A named succession or transition endpoint that resolves to a non-vertex or non-action node is reported during analysis; positional, implied, unresolved and flow ends remain unchanged | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go`; `passes/action_endpoint.go` `ActionEndpointPass`; `lower/action_nodes.go` `ActionNodes` and `ActionEndpointAccepted`, including lazy inherited action-node collection | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestActionEndpointPassReportsResolvedNonNodes`, `:TestActionEndpointPassAcceptsLoweredNodes`, `:TestActionEndpointPassDiagnosticAgreesWithLowering`, `:TestActionEndpointPassInheritedNodes`, `:TestActionEndpointPassIgnoresFeatureChainsThroughNonNodes`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`; `lower/action_inherited_test.go`; `runtime/testdata/conformance/action_inherited_endpoint.sysml` | ⚠️ Approximate (a feature chain rooted in a part usage is legal notation and stays silent during validation, but execution still reports the existing lowering error because invoking an action through that chain is not implemented) | | Initial state identification | `lower/state_graph.go:ToStateGraph`; `state_executor.go:686` initialize | `state_simple.sysml` | ✅ Faithful | | A succession out of a state body's own entry subaction names the state it starts in (`entry; then off;`), which is how a machine designates where it starts | `lower/entry_transition.go` `addEntryTransition`, `UnconditionalStart`; `lower/state_graph.go` collectTransitions SuccessionEdge case + `isEntrySubaction` | `lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState`, `lower/transition_source_test.go:TestToStateGraph_UnguardedEntryTransitionIsInitial`, `state_entry_succession_initial.sysml` conformance, `tests/parser/testdata/parse/behavior_exhibit_state_body.golden` | ✅ Faithful | @@ -914,7 +915,7 @@ nothing above. | A succession (`succession first s then a;`) names its endpoints the way a transition does, so it reaches a nested, region-local or qualified vertex, and a pseudostate as well as a state | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go` route succession ends; `passes/state_transition.go`; `lower/state_graph.go` `collectTransitions` (UsageSuccession and SuccessionEdge cases) → `(*StateGraph).endpointVertex`/`endpointState`, over the same `lower.EndpointResolver` a transition's endpoints resolve through | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`, `lower/state_graph_nested_test.go:TestSuccessionReachesAPseudostate`, `:TestSuccessionQualifiedTargetNamesTheVertexItQualifies`, `:TestToStateGraph_EntrySuccessionNamesInitialState` | ✅ Faithful (previously matched the endpoint's last name segment against a flat state list, which reached no pseudostate and could bind a same-named vertex of another state) | | Two regions may declare same-named pseudostates, and each is a vertex of its own | `lower/state_graph.go` `StateGraph.Pseudostates` (declaration-ordered slice, not keyed by name), `addPseudostate` | `lower/state_graph_nested_test.go:TestSameNamedPseudostatesInSiblingRegionsAreBothCollected` | ✅ Faithful | | Junction pseudostates: the route through a junction is resolved statically before the incoming effect; every outgoing guard is read against the current data, several enabled branches are a recorded transition choice, and an unguarded branch is the default when none of the guards holds. The settled route continues through further junctions but stays open at the first choice; a history default is resolved the same way. If a route has no way through, its compound transition is not enabled. The occurrence may go to another enabled transition, be deferred or be discarded as unmatched; a completion with no route is dropped. For a join, selection checks the way out using only the candidate and prior arrivals. Signal/change dispatches count same-occurrence peers only when their regions chose them; firing checks completion against those peers, and a dead route fires none. Timer expiries are separate occurrences even when due together; a dead completing segment is disabled, leaving its source's timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). A history default route disables the transition only when its source is outside the history owner. Only `errNoWayThrough` disables: cycles, unevaluable guards and binding failures still fail as the transition fires. Dead branches whose continuation has no way through are filtered from a junction draw; one surviving branch is followed without a draw. Choice remains dynamic, and a dead junction reached beyond a choice remains a run error. UML 2.5.1 §14.2.3.8.1 (compound transition) and §14.2.3.7 (junction) are the extension's references. KerML `ControlPerformances::DecisionPerformance` declares `feature outgoingHBLink: HappensBefore[1]`; it does not specify no-way-through or unmatched-event behavior | `state_route.go` `resolveRoute`, `routeAvailable`, `followOut`, `enabledBranches`, `junctionDraw`, `travel`, `travelResolving`, `settleDraws`, `pickBranch`, `errNoWayThrough`; `state_executor.go` transition enablement and history-default routing | `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`, `state_junction_no_way_through_unmatched.sysml`, `state_junction_no_way_through_other_transition_fires.sysml`, `state_junction_no_way_through_deferred.sysml`, `state_junction_dead_branch_not_drawn.sysml`, `state_completion_no_way_through_dropped.sysml`, `state_history_default_no_way_through.sysml`, `state_history_self_transition_default_no_way_through_restores.sysml`, `state_join_no_way_out_disables_last_segment.sysml`; `explore_test.go:TestExploreStaticJunctionBranches`, `:TestExploreJunctionDrawnAsTransitionFires`, `:TestExploreHistoryDefaultThroughJunction`; `replay_test.go:TestReplayRefusedChoiceUndoesTheJunctionDrawBeforeIt`, `:TestReplayRefusedChoiceUndoesTheHistoryDefaultsJunctionDraw`; `robustness_junction_exit_route_test.go:TestRuntimeRobustnessJunctionExitRoute`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead junction beyond choice, cycle, unevaluable guard, join exit failure); `robustness_test.go:region_pseudostate_without_satisfied_guard` | ✅ Faithful | -| 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 | +| Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork starts at its default only when it has one; otherwise it remains inactive. Default entry into a composite likewise leaves any no-entry region inactive, regardless of whether another fork can enter it. A composite completes only after every region reaches a final state; an inactive region blocks completion. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `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`, `regionStart`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`; `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | | Join pseudostates: each occurrence fires every not-yet-arrived segment it enables and whose region's dispatch chose it, exiting each source, running its effect and recording each in `joinArrived` only after the firing succeeds; when multiple segments fire, they are drawn in `join ` order. At selection, including for each time expiry, probes count only the candidate and prior arrivals, checking the way out only if they complete the join. In signal/change dispatches, same-occurrence peers count only when their regions chose them; firing checks completion against those peers, and if the route is dead, none fire. Timer expiries are separate occurrences even at the same instant; a dead completing segment is disabled, leaving its source's other timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). The join completes when prior arrivals plus segments enabled by the current occurrence and chosen by their regions in that dispatch cover every incoming segment and the outgoing route is available. The current segments are drawn as `join `, arrivals clear and the owner is left after the last segment effect. Completion segments fire only on their own completion; waiting regions do not complete their owner; arrivals clear on region re-entry or when another transition leaves the owner. Snapshots, held images and canonical check state preserve arrivals. PSSM leaves the owner before the final incoming effect; leaving after that effect is the extension's reading recorded in SM34. `TransitionPerformances.kerml` says `private succession [1] transitionLinkSource then [*] effect;`; `StatePerformances.kerml` says `and hence happening during the state performance.` Neither supplies a cross-segment arrival rule; that is the UML/PSSM extension reference | `state_route.go` `joinFiringNow`; `state_executor.go` `fireJoinTransition`, `fireJoinSegment`, `fireJoinIncoming`, `leaveJoinOwner`, `regionWaitingAtJoin`, `setRegionState`, `clearJoinArrivalsOwnedBy`; `snapshot.go`, `held_image_behavior.go`, `check_state.go` preserve `joinArrived` | Conformance fixtures: `state_fork_join_pseudostate`, `state_join_arrival_dropped_when_owner_exited`, `state_join_change_segment_rises_alone`, `state_join_change_segment_waits_before_junction`, `state_join_change_segment_waits_before_junction_metadata`, `state_join_change_segments_rise_together`, `state_join_completion_is_not_a_timers_expiry`, `state_join_completion_segment_waits_for_do_behavior`, `state_join_dead_timer_join_keeps_group_alternative`, `state_join_exit_from_regions_before_owner_exit`, `state_join_from_composite_sources`, `state_join_from_nested_states`, `state_join_no_way_out_disables_last_segment`, `state_join_of_machine_regions_from_nested_source`, `state_join_runs_every_incoming_effect`, `state_join_segment_fires_on_own_signal`, `state_join_segments_arrive_together_on_one_occurrence`, `state_join_segment_not_chosen_does_not_arrive`, `state_join_peer_not_chosen_does_not_block_arrival`, `state_join_segment_reads_its_payload`, `state_join_segment_trigger_unmatched`, `state_join_time_segment_needs_same_occurrence`, `state_join_time_segment_not_taken_by_signal`, `state_join_time_segment_unsynchronized_reads_no_route`, `state_join_time_segment_unsynchronized_reads_no_route_metadata`, `state_join_time_segments_expire_apart`, `state_join_time_segments_expire_together`, `state_join_waits_for_every_segment_enabled`; tests: `pseudostate_test.go:TestForkAndJoinPseudostates`, `:TestJoinWaitsForEveryBranch`, `:TestJoinSegmentFiresOnOwnOccurrence`, `:TestJoinDisarmedSegmentCanArriveAlone`, `:TestJoinFromActiveCompositeSourcesFires`, `:TestJoinCompletionSegmentWaitsForItsSourcesDoBehavior`, `:TestJoinCompletionSegmentWaitsForItsSourcesBody`; `fork_join_test.go:TestForkJoinVisitOrderIsDeterministic`; `choice_test.go:TestCompletionTransitionArrivesAtUnreadyJoin`; `state_change_trigger_test.go:TestChangeTriggerDisarmedSegmentCanWaitForItsPeer`; `join_snapshot_test.go:TestSnapshotRestoresJoinArrivals`, `:TestCloneNilJoinArrivalsIsWritable`; `check_test.go:TestCheckJoinWaitsForSlowestBranch`; `replay_test.go:TestReplayRefusedJoinDrawChangesNothing`, `:TestReplayRefusedJoinDrawUndoesTheSegmentsChoice`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead signal and timer join exits, exit failure) | ✅ 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 ;`) and `lower/state_metadata.go` (`#shallowHistory state`/`#deepHistory state`); `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 | diff --git a/internal/exec/runtime/robustness_inactive_region_test.go b/internal/exec/runtime/robustness_inactive_region_test.go new file mode 100644 index 0000000000..6351bd16fe --- /dev/null +++ b/internal/exec/runtime/robustness_inactive_region_test.go @@ -0,0 +1,28 @@ +package runtime + +import "testing" + +func TestRuntimeRobustnessInactiveRegion(t *testing.T) { + _, visits, err := executeStateSource(t, "Machine", `package test { + state Machine { + entry; then working; + state working parallel { + state left { state idle; } + state right { state waiting; } + } + state finished; + transition first working then finished; + } + }`) + if err != nil { + t.Fatalf("execute state machine: %v", err) + } + for _, visit := range visits { + switch visit { + case "idle", "waiting": + t.Errorf("no-entry region state %s was activated", visit) + case "finished": + t.Fatal("working completed while both regions were inactive") + } + } +} diff --git a/internal/exec/runtime/robustness_parallel_wrapper_test.go b/internal/exec/runtime/robustness_parallel_wrapper_test.go index ec75183355..d272094924 100644 --- a/internal/exec/runtime/robustness_parallel_wrapper_test.go +++ b/internal/exec/runtime/robustness_parallel_wrapper_test.go @@ -8,7 +8,7 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" ) -func TestRuntimeRobustnessParallelWrapper(t *testing.T) { +func TestRuntimeRobustnessParallelWrapperLeavesRegionInactive(t *testing.T) { err := stateRunErrorForSource(t, "Machine", ` package test { state def Machine { @@ -21,12 +21,8 @@ func TestRuntimeRobustnessParallelWrapper(t *testing.T) { } } `) - if err == nil { - t.Fatal("a wrapped region without an entry completed") - } - message := strings.ToLower(err.Error()) - if !strings.Contains(message, "initial") && !strings.Contains(message, "deadlock") && !strings.Contains(message, "stuck") { - t.Fatalf("error = %v; want a typed missing-entry error or a stuck/deadlock report", err) + if err != nil { + t.Fatalf("a wrapped region without an entry should remain inactive, not fail: %v", err) } } diff --git a/internal/exec/runtime/robustness_test.go b/internal/exec/runtime/robustness_test.go index a94927cbf9..53701d080a 100644 --- a/internal/exec/runtime/robustness_test.go +++ b/internal/exec/runtime/robustness_test.go @@ -289,10 +289,7 @@ func TestRuntimeRobustness(t *testing.T) { t.Run("action_body_unresolved_feature", testActionBodyUnresolvedFeature) t.Run("state_body_unresolved_unit", testStateBodyUnresolvedUnit) t.Run("fork_branches_share_region", testForkBranchesShareRegion) - t.Run("fork_leaves_a_region_without_a_way_in", testForkLeavesARegionWithoutAWayIn) - t.Run("fork_only_region_entered_by_default", testForkOnlyRegionEnteredByDefault) t.Run("fork_branch_with_a_trigger", testForkBranchWithATrigger) - t.Run("nested_fork_starts_an_outer_region_by_default", testNestedForkStartsAnOuterRegionByDefault) t.Run("join_with_one_incoming_branch", testJoinWithOneIncomingBranch) t.Run("join_incoming_effect_that_fails", testJoinIncomingEffectThatFails) t.Run("join_with_two_segments_from_one_source", testJoinWithTwoSegmentsFromOneSource) @@ -6138,66 +6135,6 @@ func testForkBranchesShareRegion(t *testing.T) { } } -// testForkLeavesARegionWithoutAWayIn: a region a fork enters needs no entry -// transition, but a sibling region neither enters still has no way in. -func testForkLeavesARegionWithoutAWayIn(t *testing.T) { - _, _, err := executeStateSource(t, "Machine", `package test { - state Machine { - entry; then init; - state init; - state working parallel { - state left { state a; } - state right { state b; } - state third { state c; } - } - fork split; - - transition first init then split; - transition first split then a; - transition first split then b; - } - }`) - if err == nil { - t.Fatal("expected an error for the region no fork enters") - } - if !strings.Contains(err.Error(), "region third has no initial state") { - t.Errorf("expected the third region's missing initial, got: %v", err) - } - if strings.Contains(err.Error(), "region left") || strings.Contains(err.Error(), "region right") { - t.Errorf("the fork-entered regions were refused too: %v", err) - } -} - -// testForkOnlyRegionEnteredByDefault: a region only a fork enters has no -// default start, so another transition into its composite state is refused -// before the machine runs rather than failing when that transition fires. -func testForkOnlyRegionEnteredByDefault(t *testing.T) { - _, _, err := executeStateSource(t, "Machine", `package test { - attribute def Go; - state Machine { - entry; then init; - state init; - state working parallel { - state left { state a; } - state right { state b; } - } - fork split; - - transition first init accept Go then working; - transition first init then split; - transition first split then a; - transition first split then b; - } - }`) - if err == nil { - t.Fatal("expected an error for the transition entering the fork-only regions by default") - } - if !strings.Contains(err.Error(), "region left in state working has no initial state") || - !strings.Contains(err.Error(), "the transition from init to working") { - t.Errorf("expected the default entry into left to be named, got: %v", err) - } -} - // testForkBranchWithATrigger: a fork's branches fire as one with the fork, so a // branch that waits for an occurrence is refused before the machine runs rather // than firing regardless of it. @@ -6226,44 +6163,6 @@ func testForkBranchWithATrigger(t *testing.T) { } } -// testNestedForkStartsAnOuterRegionByDefault: a fork into a nested composite -// state enters the outer one on the way, starting its other region by default, -// so an outer region only another fork enters is refused before the machine runs. -func testNestedForkStartsAnOuterRegionByDefault(t *testing.T) { - _, _, err := executeStateSource(t, "Machine", `package test { - state Machine { - entry; then init; - state init; - state outer parallel { - state o1 { - entry; then hold; - state hold; - state inner parallel { - state left { state a; } - state right { state b; } - } - fork split; - transition first split then a; - transition first split then b; - } - state o2 { state c; } - } - fork split2; - - transition first init then split; - transition first split2 then hold; - transition first split2 then c; - } - }`) - if err == nil { - t.Fatal("expected an error for the nested fork starting o2 by default") - } - if !strings.Contains(err.Error(), "region o2 in state outer has no initial state") || - !strings.Contains(err.Error(), "the transition from init to split") { - t.Errorf("expected the nested fork's route to be named, got: %v", err) - } -} - // testJoinWithOneIncomingBranch: a join synchronizes branches, so a single // incoming transition is a modeling error rather than a pass-through. func testJoinWithOneIncomingBranch(t *testing.T) { diff --git a/internal/exec/runtime/state_region_entry.go b/internal/exec/runtime/state_region_entry.go index db92959c7a..e93b72f2af 100644 --- a/internal/exec/runtime/state_region_entry.go +++ b/internal/exec/runtime/state_region_entry.go @@ -137,6 +137,9 @@ func (e *StateExecutor) enterRegion(w *regionEntry) error { if err != nil { return err } + if entry == nil { + return nil + } e.setRegionState(w.region, entry) _, deepest, err := e.enterToward(w.container, entry, w.branches) if err != nil { @@ -172,15 +175,15 @@ func (e *StateExecutor) regionStart(w *regionEntry) (*ast.StateNode, error) { return w.target, nil } if owner := e.graph.RegionState[w.region]; owner != nil { + if w.container != nil && len(e.graph.StartOf(w.region)) == 0 { + return nil, nil + } return owner, nil } entry, err := e.startIn(w.region) if err != nil { return nil, err } - if entry == nil { - return nil, fmt.Errorf("region %s has no initial state", w.region.Name) - } return entry, nil } diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json new file mode 100644 index 0000000000..f913a417eb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json @@ -0,0 +1,22 @@ +{ + "verdict": "divergent", + "divergent": { + "log": [ + "\"a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) \"", + "\"a(entry) b(entry) a(entry) c(entry) b(entry) finished(entry) \"", + "\"a(entry) b(entry) b(entry) a(entry) c(entry) finished(entry) \"", + "\"a(entry) b(entry) b(entry) c(entry) a(entry) finished(entry) \"", + "\"a(entry) b(entry) c(entry) a(entry) b(entry) finished(entry) \"", + "\"a(entry) b(entry) c(entry) b(entry) a(entry) finished(entry) \"", + "\"b(entry) a(entry) a(entry) b(entry) c(entry) finished(entry) \"", + "\"b(entry) a(entry) a(entry) c(entry) b(entry) finished(entry) \"", + "\"b(entry) a(entry) b(entry) a(entry) c(entry) finished(entry) \"", + "\"b(entry) a(entry) b(entry) c(entry) a(entry) finished(entry) \"", + "\"b(entry) a(entry) c(entry) a(entry) b(entry) finished(entry) \"", + "\"b(entry) a(entry) c(entry) b(entry) a(entry) finished(entry) \"" + ] + }, + "agreed": { + "finalState": "finished" + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden new file mode 100644 index 0000000000..9e4bf2ea81 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden @@ -0,0 +1,79 @@ +exit: init +enter: working +transition: init -> working (event: accept Default) +exit: working +enter: init +transition: working -> init (event: accept Back) +exit: init +choice fork split: next split->a(effect), split->b(effect) (unordered; took split->a(effect) first) +enter: working +enter: a (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "a(entry) " -> "a(entry) " + eval operator + -> "a(entry) " +enter: b (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) " + eval literal "b(entry) " -> "b(entry) " + eval operator + -> "a(entry) b(entry) " +transition: init -> split +exit: a +enter: done +transition: a -> done (event: accept FinishA) +exit: b +enter: done +transition: b -> done (event: accept FinishB) +choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) +exit: done +exit: done +exit: working +enter: init +transition: working -> init (event: accept Back) +exit: init +choice fork all: next all->a(effect), all->b(effect), all->c(effect) (unordered; took all->a(effect) first) +enter: working +enter: a (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) " + eval literal "a(entry) " -> "a(entry) " + eval operator + -> "a(entry) b(entry) a(entry) " +choice fork all: next all->b(effect), all->c(effect) (unordered; took all->b(effect) first) +enter: b (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) a(entry) " + eval literal "b(entry) " -> "b(entry) " + eval operator + -> "a(entry) b(entry) a(entry) b(entry) " +enter: c (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) a(entry) b(entry) " + eval literal "c(entry) " -> "c(entry) " + eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) " +transition: init -> all +exit: a +enter: done +transition: a -> done (event: accept FinishA) +exit: b +enter: done +transition: b -> done (event: accept FinishB) +exit: c +enter: done +transition: c -> done (event: accept FinishC) +choice exiting working: next left.done(exit), right.done(exit), third.done(exit) (unordered; took left.done(exit) first) +exit: done +choice exiting working: next right.done(exit), third.done(exit) (unordered; took right.done(exit) first) +exit: done +exit: done +exit: working +enter: finished (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) a(entry) b(entry) c(entry) " + eval literal "finished(entry) " -> "finished(entry) " + eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) " +transition: working -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json new file mode 100644 index 0000000000..e935776bf6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json @@ -0,0 +1,67 @@ +{ + "type": "state", + "events": [ + {"signal": "Default"}, + {"signal": "Back"}, + {"signal": "Fork"}, + {"signal": "FinishA"}, + {"signal": "FinishB"}, + {"signal": "Back"}, + {"signal": "All"}, + {"signal": "FinishA"}, + {"signal": "FinishB"}, + {"signal": "FinishC"} + ], + "outcomes": [ + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) a(entry) c(entry) b(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) b(entry) a(entry) c(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) b(entry) c(entry) a(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) c(entry) a(entry) b(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) c(entry) b(entry) a(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) a(entry) b(entry) c(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) a(entry) c(entry) b(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) b(entry) a(entry) c(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) b(entry) c(entry) a(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) c(entry) a(entry) b(entry) finished(entry) "}} + }, + { + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) c(entry) b(entry) a(entry) finished(entry) "}} + } + ], + "admissible": "Regions of a parallel state entered on one occurrence: each is entered, in which order is open", + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden new file mode 100644 index 0000000000..4ce90069fd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden @@ -0,0 +1,79 @@ +exit: init +enter: working +transition: init -> working (event: accept Default) +exit: working +enter: init +transition: working -> init (event: accept Back) +exit: init +choice fork split: next split->a(effect), split->b(effect) (unordered; took split->b(effect) first) +enter: working +enter: b (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "b(entry) " -> "b(entry) " + eval operator + -> "b(entry) " +enter: a (entry action) +stmt action body + stmt assign log + eval feature log -> "b(entry) " + eval literal "a(entry) " -> "a(entry) " + eval operator + -> "b(entry) a(entry) " +transition: init -> split +exit: a +enter: done +transition: a -> done (event: accept FinishA) +exit: b +enter: done +transition: b -> done (event: accept FinishB) +choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) +exit: done +exit: done +exit: working +enter: init +transition: working -> init (event: accept Back) +exit: init +choice fork all: next all->a(effect), all->b(effect), all->c(effect) (unordered; took all->c(effect) first) +enter: working +enter: c (entry action) +stmt action body + stmt assign log + eval feature log -> "b(entry) a(entry) " + eval literal "c(entry) " -> "c(entry) " + eval operator + -> "b(entry) a(entry) c(entry) " +choice fork all: next all->a(effect), all->b(effect) (unordered; took all->a(effect) first) +enter: a (entry action) +stmt action body + stmt assign log + eval feature log -> "b(entry) a(entry) c(entry) " + eval literal "a(entry) " -> "a(entry) " + eval operator + -> "b(entry) a(entry) c(entry) a(entry) " +enter: b (entry action) +stmt action body + stmt assign log + eval feature log -> "b(entry) a(entry) c(entry) a(entry) " + eval literal "b(entry) " -> "b(entry) " + eval operator + -> "b(entry) a(entry) c(entry) a(entry) b(entry) " +transition: init -> all +exit: a +enter: done +transition: a -> done (event: accept FinishA) +exit: b +enter: done +transition: b -> done (event: accept FinishB) +exit: c +enter: done +transition: c -> done (event: accept FinishC) +choice exiting working: next left.done(exit), right.done(exit), third.done(exit) (unordered; took third.done(exit) first) +exit: done +choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) +exit: done +exit: done +exit: working +enter: finished (entry action) +stmt action body + stmt assign log + eval feature log -> "b(entry) a(entry) c(entry) a(entry) b(entry) " + eval literal "finished(entry) " -> "finished(entry) " + eval operator + -> "b(entry) a(entry) c(entry) a(entry) b(entry) finished(entry) " +transition: working -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.sysml b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.sysml new file mode 100644 index 0000000000..fd53f15614 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.sysml @@ -0,0 +1,43 @@ +package ForkOnlyRegion { + item def Default; + item def Back; + item def Fork; + item def All; + item def FinishA; + item def FinishB; + item def FinishC; + + state Machine { + attribute log : String = ""; + entry; then init; + state init; + state working parallel { + state left { + state a { entry { assign log := log + "a(entry) "; } } + transition first a accept FinishA then done; + } + state right { + state b { entry { assign log := log + "b(entry) "; } } + transition first b accept FinishB then done; + } + state third { + state c { entry { assign log := log + "c(entry) "; } } + transition first c accept FinishC then done; + } + } + state finished { entry { assign log := log + "finished(entry) "; } } + fork split; + fork all; + + transition first init accept Default then working; + transition first init accept Fork then split; + transition first init accept All then all; + transition first working accept Back then init; + transition first working then finished; + transition first split then a; + transition first split then b; + transition first all then a; + transition first all then b; + transition first all then c; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden new file mode 100644 index 0000000000..9e4bf2ea81 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden @@ -0,0 +1,79 @@ +exit: init +enter: working +transition: init -> working (event: accept Default) +exit: working +enter: init +transition: working -> init (event: accept Back) +exit: init +choice fork split: next split->a(effect), split->b(effect) (unordered; took split->a(effect) first) +enter: working +enter: a (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "a(entry) " -> "a(entry) " + eval operator + -> "a(entry) " +enter: b (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) " + eval literal "b(entry) " -> "b(entry) " + eval operator + -> "a(entry) b(entry) " +transition: init -> split +exit: a +enter: done +transition: a -> done (event: accept FinishA) +exit: b +enter: done +transition: b -> done (event: accept FinishB) +choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) +exit: done +exit: done +exit: working +enter: init +transition: working -> init (event: accept Back) +exit: init +choice fork all: next all->a(effect), all->b(effect), all->c(effect) (unordered; took all->a(effect) first) +enter: working +enter: a (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) " + eval literal "a(entry) " -> "a(entry) " + eval operator + -> "a(entry) b(entry) a(entry) " +choice fork all: next all->b(effect), all->c(effect) (unordered; took all->b(effect) first) +enter: b (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) a(entry) " + eval literal "b(entry) " -> "b(entry) " + eval operator + -> "a(entry) b(entry) a(entry) b(entry) " +enter: c (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) a(entry) b(entry) " + eval literal "c(entry) " -> "c(entry) " + eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) " +transition: init -> all +exit: a +enter: done +transition: a -> done (event: accept FinishA) +exit: b +enter: done +transition: b -> done (event: accept FinishB) +exit: c +enter: done +transition: c -> done (event: accept FinishC) +choice exiting working: next left.done(exit), right.done(exit), third.done(exit) (unordered; took left.done(exit) first) +exit: done +choice exiting working: next right.done(exit), third.done(exit) (unordered; took right.done(exit) first) +exit: done +exit: done +exit: working +enter: finished (entry action) +stmt action body + stmt assign log + eval feature log -> "a(entry) b(entry) a(entry) b(entry) c(entry) " + eval literal "finished(entry) " -> "finished(entry) " + eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) " +transition: working -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json new file mode 100644 index 0000000000..88976b8e78 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json @@ -0,0 +1,14 @@ +{ + "type": "state", + "events": [ + {"signal": "Leave"}, + {"signal": "ReturnShallow"}, + {"signal": "Leave"}, + {"signal": "ReturnDeep"} + ], + "finalState": "active", + "outputs": { + "log": {"type": "String", "value": "A(entry) A(exit) A(entry) A(exit) A(entry) "} + }, + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.sysml b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.sysml new file mode 100644 index 0000000000..1bcd579e60 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.sysml @@ -0,0 +1,34 @@ +package HistoryInactiveRegion { + item def Leave; + item def ReturnShallow; + item def ReturnDeep; + + state Machine { + attribute log : String = ""; + entry; then working; + state working parallel { + state a { + entry; then active; + state active { + entry { assign log := log + "A(entry) "; } + exit { assign log := log + "A(exit) "; } + } + } + state b { + state inactive { + entry { assign log := log + "B(entry) "; } + exit { assign log := log + "B(exit) "; } + } + } + history shallow; + deep history deep; + } + state outside; + state completed; + + transition first active when Leave then outside; + transition first outside when ReturnShallow then shallow; + transition first outside when ReturnDeep then deep; + transition first working then completed; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.trace.golden b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.trace.golden new file mode 100644 index 0000000000..438bf3192b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.trace.golden @@ -0,0 +1,36 @@ +exit: active (exit action) +stmt action body + stmt assign log + eval feature log -> "A(entry) " + eval literal "A(exit) " -> "A(exit) " + eval operator + -> "A(entry) A(exit) " +exit: working +enter: outside +transition: active -> outside (event: accept Leave) +exit: outside +enter: working +enter: active (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(exit) " + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) A(exit) A(entry) " +transition: outside -> working (event: accept ReturnShallow) +exit: active (exit action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(exit) A(entry) " + eval literal "A(exit) " -> "A(exit) " + eval operator + -> "A(entry) A(exit) A(entry) A(exit) " +exit: working +enter: outside +transition: active -> outside (event: accept Leave) +exit: outside +enter: working +enter: active (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(exit) A(entry) A(exit) " + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) A(exit) A(entry) A(exit) A(entry) " +transition: outside -> working (event: accept ReturnDeep) diff --git a/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json new file mode 100644 index 0000000000..933eeb8a8f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json @@ -0,0 +1,8 @@ +{ + "type": "state", + "finalState": "a+inner+b", + "outputs": { + "log": {"type": "String", "value": ""} + }, + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.sysml b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.sysml new file mode 100644 index 0000000000..68de3fb5aa --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.sysml @@ -0,0 +1,23 @@ +package NestedForkInactiveRegion { + state Machine { + attribute log : String = ""; + entry; then init; + state init; + state outer parallel { + state o1 { + entry; then hold; + state hold; + state inner parallel { + state left { state a; } + state right { state b; } + } + fork split; + transition first split then a; + transition first split then b; + } + state o2 { state c { entry { assign log := log + "c(entry) "; } } } + } + + transition first init then split; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.trace.golden b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.trace.golden new file mode 100644 index 0000000000..2294e7f48f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.trace.golden @@ -0,0 +1,7 @@ +exit: init +enter: outer +choice fork split: next split->a(effect), split->b(effect) (unordered; took split->a(effect) first) +enter: inner +enter: a +enter: b +transition: init -> split diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json new file mode 100644 index 0000000000..d224a888b3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json @@ -0,0 +1,12 @@ +{ + "verdict": "divergent", + "divergent": { + "log": [ + "\"A(entry) A(entry) B(entry) outside(entry) \"", + "\"A(entry) B(entry) A(entry) outside(entry) \"" + ] + }, + "agreed": { + "finalState": "outside" + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden new file mode 100644 index 0000000000..8bdad6063b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden @@ -0,0 +1,49 @@ +exit: idle +enter: working +enter: A (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) " +transition: idle -> working (event: accept Go) +exit: A +enter: done +transition: A -> done (event: accept FinishA) +exit: done +exit: working +enter: idle +transition: working -> idle (event: accept Back) +exit: idle +enter: working +choice entering working: next A(entry), B(entry) (unordered; took A(entry) first) +enter: A (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) " + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) A(entry) " +enter: B (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(entry) " + eval literal "B(entry) " -> "B(entry) " + eval operator + -> "A(entry) A(entry) B(entry) " +transition: idle -> B (event: accept Direct) +exit: A +enter: done +transition: A -> done (event: accept FinishA) +exit: B +enter: done +transition: B -> done (event: accept FinishB) +choice exiting working: next a.done(exit), b.done(exit) (unordered; took a.done(exit) first) +exit: done +exit: done +exit: working +enter: outside (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(entry) B(entry) " + eval literal "outside(entry) " -> "outside(entry) " + eval operator + -> "A(entry) A(entry) B(entry) outside(entry) " +transition: working -> outside diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json new file mode 100644 index 0000000000..fd6e640037 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json @@ -0,0 +1,27 @@ +{ + "type": "state", + "events": [ + {"signal": "Go"}, + {"signal": "FinishA"}, + {"signal": "Back"}, + {"signal": "Direct"}, + {"signal": "FinishA"}, + {"signal": "FinishB"} + ], + "outcomes": [ + { + "finalState": "outside", + "outputs": { + "log": {"type": "String", "value": "A(entry) A(entry) B(entry) outside(entry) "} + } + }, + { + "finalState": "outside", + "outputs": { + "log": {"type": "String", "value": "A(entry) B(entry) A(entry) outside(entry) "} + } + } + ], + "admissible": "Regions of a parallel state entered on one occurrence: each is entered, in which order is open", + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden new file mode 100644 index 0000000000..b0ca3c499f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden @@ -0,0 +1,49 @@ +exit: idle +enter: working +enter: A (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) " +transition: idle -> working (event: accept Go) +exit: A +enter: done +transition: A -> done (event: accept FinishA) +exit: done +exit: working +enter: idle +transition: working -> idle (event: accept Back) +exit: idle +enter: working +choice entering working: next A(entry), B(entry) (unordered; took B(entry) first) +enter: B (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) " + eval literal "B(entry) " -> "B(entry) " + eval operator + -> "A(entry) B(entry) " +enter: A (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) B(entry) " + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) B(entry) A(entry) " +transition: idle -> B (event: accept Direct) +exit: A +enter: done +transition: A -> done (event: accept FinishA) +exit: B +enter: done +transition: B -> done (event: accept FinishB) +choice exiting working: next a.done(exit), b.done(exit) (unordered; took a.done(exit) first) +exit: done +exit: done +exit: working +enter: outside (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) B(entry) A(entry) " + eval literal "outside(entry) " -> "outside(entry) " + eval operator + -> "A(entry) B(entry) A(entry) outside(entry) " +transition: working -> outside diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.sysml b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.sysml new file mode 100644 index 0000000000..9cb20b94d1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.sysml @@ -0,0 +1,29 @@ +package InactiveParallelRegion { + item def Go; + item def Back; + item def Direct; + item def FinishA; + item def FinishB; + + state Machine { + attribute log : String = ""; + entry; then idle; + state idle; + state working parallel { + state a { + entry; then A; + state A { entry { assign log := log + "A(entry) "; } } + transition first A accept FinishA then done; + } + state b { + state B { entry { assign log := log + "B(entry) "; } } + transition first B accept FinishB then done; + } + } + state outside { entry { assign log := log + "outside(entry) "; } } + transition first idle accept Go then working; + transition first idle accept Direct then B; + transition first working accept Back then idle; + transition first working then outside; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden new file mode 100644 index 0000000000..8bdad6063b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden @@ -0,0 +1,49 @@ +exit: idle +enter: working +enter: A (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) " +transition: idle -> working (event: accept Go) +exit: A +enter: done +transition: A -> done (event: accept FinishA) +exit: done +exit: working +enter: idle +transition: working -> idle (event: accept Back) +exit: idle +enter: working +choice entering working: next A(entry), B(entry) (unordered; took A(entry) first) +enter: A (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) " + eval literal "A(entry) " -> "A(entry) " + eval operator + -> "A(entry) A(entry) " +enter: B (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(entry) " + eval literal "B(entry) " -> "B(entry) " + eval operator + -> "A(entry) A(entry) B(entry) " +transition: idle -> B (event: accept Direct) +exit: A +enter: done +transition: A -> done (event: accept FinishA) +exit: B +enter: done +transition: B -> done (event: accept FinishB) +choice exiting working: next a.done(exit), b.done(exit) (unordered; took a.done(exit) first) +exit: done +exit: done +exit: working +enter: outside (entry action) +stmt action body + stmt assign log + eval feature log -> "A(entry) A(entry) B(entry) " + eval literal "outside(entry) " -> "outside(entry) " + eval operator + -> "A(entry) A(entry) B(entry) outside(entry) " +transition: working -> outside diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json index cdfa51b2e5..83c1b7c621 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json @@ -6,7 +6,7 @@ ], "finalState": "done", "outputs": { - "log": {"type": "Integer", "value": 12} + "log": {"type": "Integer", "value": 0} }, "trace": true } diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml index c1095dfb87..b64fc44c5d 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml @@ -1,6 +1,5 @@ -// A region of a parallel state stood for by a state declaring entry and exit -// behaviors, but no substates, needs no initial: it starts in that state, runs -// its entry, and runs its exit when the parallel state is left. +// A region without an entry transition remains inactive, including when its +// only state declares entry and exit behaviors. package test { attribute def Go; attribute def Stop; diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden index 63f78db798..e434580975 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden @@ -1,15 +1,7 @@ exit: wstart enter: done transition: wstart -> done (event: accept Go) -choice exiting busy: next done(exit), idle(exit) (unordered; took done(exit) first) exit: done -stmt action body - stmt assign log - eval feature log -> 1 - eval literal 10 -> 10 - eval operator * -> 10 - eval literal 2 -> 2 - eval operator + -> 12 exit: busy enter: done transition: busy -> done (event: accept Stop) diff --git a/internal/ir/lower/fork_plan.go b/internal/ir/lower/fork_plan.go index 3bf114477d..062e9da33f 100644 --- a/internal/ir/lower/fork_plan.go +++ b/internal/ir/lower/fork_plan.go @@ -114,147 +114,11 @@ func (g *StateGraph) enclosingRegion(state *ast.StateNode) *ast.StateRegion { return nil } -// ForkStarted reports whether a fork enters region, so it may start there -// without an entry transition of its own. +// ForkStarted reports whether a fork branch enters region. func (g *StateGraph) ForkStarted(region *ast.StateRegion) bool { return len(g.ForkEntered[region]) > 0 } -// checkForkOnlyRegion checks that a region without an entry transition is only -// ever started by a fork's branch: no other way into its composite state may -// leave the region to start by default. -func (g *StateGraph) checkForkOnlyRegion(owner *ast.StateNode, region *ast.StateRegion) error { - entry := g.defaultEntryInto(owner, region) - if entry == "" { - return nil - } - return fmt.Errorf("region %s in state %s has no initial state: only a fork's branch may start it, but %s enters %s without one; write `entry; then ;` inside the region", - region.Name, owner.Name, entry, owner.Name) -} - -// defaultEntryInto describes a way into owner that starts region by default, -// naming no state inside it, or is empty when only forks' branches enter owner. -func (g *StateGraph) defaultEntryInto(owner *ast.StateNode, region *ast.StateRegion) string { - for _, ps := range g.Pseudostates { - if plan := g.ForkPlans[ps]; plan != nil && plan.Owner == owner && plan.Branches[region] == nil { - return "fork " + ps.Name - } - } - for _, body := range g.entryBodies() { - if from := g.bodyState(body); from != nil && g.within(owner, from) { - continue - } - for _, t := range g.EntryTransitions[body] { - if g.entersByDefault(owner, region, nil, t.Target) { - return "the entry transition naming " + t.Target.Name - } - } - } - for _, source := range g.States { - for _, t := range g.Transitions[source] { - if target := g.defaultStart(owner, region, source, t.Target, nil); target != nil { - return fmt.Sprintf("the transition from %s to %s", source.Name, vertexName(target)) - } - } - } - return "" -} - -// defaultStart is the vertex a transition from source to target ends at when it -// enters owner in a way that starts region by default — owner itself or a state -// in another of its regions, reached from outside owner or from owner itself — -// following junctions, choices and joins to the states they lead to, and a fork -// to the composite state its branches enter or the states they end at; nil otherwise. -func (g *StateGraph) defaultStart(owner *ast.StateNode, region *ast.StateRegion, source *ast.StateNode, target ast.Node, seen map[ast.Node]bool) ast.Node { - switch v := target.(type) { - case *ast.StateNode: - if g.entersByDefault(owner, region, source, v) { - return v - } - case *ast.PseudostateNode: - switch v.Kind { - case ast.PseudostateShallowHistory, ast.PseudostateDeepHistory: - if g.entersByDefault(owner, region, source, g.PseudostateOwner[v]) { - return v - } - case ast.PseudostateFork: - if g.forkStartsByDefault(owner, region, source, v) { - return v - } - case ast.PseudostateJunction, ast.PseudostateChoice, ast.PseudostateJoin: - if seen[v] { - return nil - } - if seen == nil { - seen = map[ast.Node]bool{} - } - seen[v] = true - for _, t := range g.Transitions[v] { - if end := g.defaultStart(owner, region, source, t.Target, seen); end != nil { - return end - } - } - } - } - return nil -} - -// forkStartsByDefault reports whether fork's branches enter owner on their way to -// a state below it, or end at owner or in another of its regions, from source. -func (g *StateGraph) forkStartsByDefault(owner *ast.StateNode, region *ast.StateRegion, source *ast.StateNode, fork *ast.PseudostateNode) bool { - plan := g.ForkPlans[fork] - switch { - case plan == nil, plan.Owner == owner: - return false - case g.within(owner, plan.Owner): - return g.entersByDefault(owner, region, source, plan.Owner) - } - for _, branch := range plan.Branches { - if g.entersByDefault(owner, region, source, branch.Target.(*ast.StateNode)) { - return true - } - } - return false -} - -// entryBodies lists the bodies whose entry transitions are on record, in -// declaration order: the machine's own, then each state's and its regions'. -func (g *StateGraph) entryBodies() []ast.Node { - bodies := []ast.Node{nil} - for _, state := range g.States { - bodies = append(bodies, state) - for _, region := range g.CompositeStates[state] { - bodies = append(bodies, region) - } - } - return bodies -} - -// bodyState is the state a body's entry transitions start inside: the state -// itself, a region's owner, nil for the machine's own body. -func (g *StateGraph) bodyState(body ast.Node) *ast.StateNode { - switch b := body.(type) { - case *ast.StateNode: - return b - case *ast.StateRegion: - return g.RegionOwner[b] - } - return nil -} - -// entersByDefault reports whether a move from source to target starts region by -// default: target is owner or lies in another of its regions, and the move comes -// from outside owner or from owner itself, so the whole of owner is entered. -func (g *StateGraph) entersByDefault(owner *ast.StateNode, region *ast.StateRegion, source, target *ast.StateNode) bool { - if target == nil || !g.within(owner, target) { - return false - } - if source != owner && g.within(owner, source) && target != owner { - return false - } - return g.regionUnder(owner, target) != region -} - // within reports whether state is owner or lies below it. func (g *StateGraph) within(owner, state *ast.StateNode) bool { for s := state; s != nil; s = g.ParentState[s] { diff --git a/internal/ir/lower/fork_plan_test.go b/internal/ir/lower/fork_plan_test.go index 61af9c0cce..cb81ef5c7b 100644 --- a/internal/ir/lower/fork_plan_test.go +++ b/internal/ir/lower/fork_plan_test.go @@ -191,9 +191,9 @@ func TestToStateGraph_ForkBranchesNestedInOneRegionFail(t *testing.T) { } } -// A region with neither an entry transition nor a fork branch has no way in. -func TestToStateGraph_RegionWithoutInitialOrForkFails(t *testing.T) { - _, err := ToStateGraph(stateDefinitionIn(t, ` +// A region with neither an entry transition nor a fork branch stays inactive. +func TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive(t *testing.T) { + graph, err := ToStateGraph(stateDefinitionIn(t, ` package test { state def Machine { entry; then idle; @@ -210,61 +210,50 @@ func TestToStateGraph_RegionWithoutInitialOrForkFails(t *testing.T) { } } `), nil) - if err == nil { - t.Fatal("region no fork enters succeeded") - } - if !strings.Contains(err.Error(), "region third has no initial state") { - t.Fatalf("error = %q, want missing initial for third", err) + if err != nil { + t.Fatalf("ToStateGraph: %v", err) } + requireRegionInitial(t, graph, "work", "third", false, false) } -// A region only a fork enters must be entered only by that fork: any other way -// into its composite state would start the region by default, which it cannot. -func TestToStateGraph_ForkOnlyRegionEnteredByDefaultFails(t *testing.T) { - for name, tc := range map[string]struct{ entry, extra, want string }{ +// A fork-only region stays inactive on default entry, whatever names the move. +func TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive(t *testing.T) { + for name, tc := range map[string]struct{ entry, extra string }{ "transition into the composite state": { `entry; then idle;`, `transition first idle accept Go then work;`, - "the transition from idle to work enters work", }, "transition into the other region": { `entry; then idle;`, `transition first idle accept Go then b;`, - "the transition from idle to b enters work", }, "self-transition of the composite state": { `entry; then idle;`, `transition first work accept Go then work;`, - "the transition from work to work enters work", }, "transition into the composite state's history": { `entry; then idle;`, `transition first idle accept Go then back;`, - "the transition from idle to back enters work", }, "entry transition naming the composite state": { `entry; then work;`, ``, - "the entry transition naming work", }, "entry transition naming a state in the other region": { `entry; then b;`, ``, - "the entry transition naming b", }, "guarded entry transition naming a state in the other region": { `entry; if false then idle; then b;`, ``, - "the entry transition naming b", }, "junction outside leading into the other region": { `entry; then idle;`, `junction j; transition first idle accept Go then j; transition first j then b;`, - "the transition from idle to b enters work", }, } { t.Run(name, func(t *testing.T) { - _, err := ToStateGraph(stateDefinitionIn(t, ` + graph, err := ToStateGraph(stateDefinitionIn(t, ` package test { attribute def Go; state def Machine { @@ -283,19 +272,16 @@ func TestToStateGraph_ForkOnlyRegionEnteredByDefaultFails(t *testing.T) { } } `), nil) - if err == nil { - t.Fatal("default entry into a fork-only region succeeded") - } - if !strings.Contains(err.Error(), "region left in state work has no initial state") || !strings.Contains(err.Error(), tc.want) { - t.Fatalf("error = %q, want %q", err, tc.want) + if err != nil { + t.Fatalf("ToStateGraph: %v", err) } + requireRegionInitial(t, graph, "work", "left", false, true) }) } } -// An enclosing state's entry transition naming a state in the other region enters -// the composite state from outside it too; the other region's own is fine. -func TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryFails(t *testing.T) { +// An enclosing entry may enter a sibling region while the fork-only region stays inactive. +func TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive(t *testing.T) { machine := func(outerEntry, rightEntry string) string { return ` package test { @@ -320,18 +306,21 @@ func TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryFails(t *testing.T) } ` } - _, err := ToStateGraph(stateDefinitionIn(t, machine(`entry; then b;`, ``)), nil) - if err == nil || !strings.Contains(err.Error(), "the entry transition naming b") { - t.Fatalf("outer's entry naming b: error = %v, want the entry transition naming b", err) + graph, err := ToStateGraph(stateDefinitionIn(t, machine(`entry; then b;`, ``)), nil) + if err != nil { + t.Fatalf("outer entry naming b: %v", err) } - if _, err := ToStateGraph(stateDefinitionIn(t, machine(`entry; then start;`, `entry; then b;`)), nil); err != nil { + requireRegionInitial(t, graph, "work", "left", false, true) + graph, err = ToStateGraph(stateDefinitionIn(t, machine(`entry; then start;`, `entry; then b;`)), nil) + if err != nil { t.Fatalf("right's own entry naming b: %v", err) } + requireRegionInitial(t, graph, "work", "left", false, true) + requireRegionInitial(t, graph, "work", "right", true, true) } -// A fork that omits a region starts it by default, so a region another fork -// alone enters is refused; with an entry transition the omission is fine. -func TestToStateGraph_ForkOnlyRegionOmittedByAnotherForkFails(t *testing.T) { +// A fork that omits a region leaves it inactive, even when another fork enters it. +func TestToStateGraph_ForkOmissionLeavesRegionInactive(t *testing.T) { machine := func(third string) string { return ` package test { @@ -356,22 +345,21 @@ func TestToStateGraph_ForkOnlyRegionOmittedByAnotherForkFails(t *testing.T) { } ` } - _, err := ToStateGraph(stateDefinitionIn(t, machine(``)), nil) - if err == nil || !strings.Contains(err.Error(), "region left in state work has no initial state") || !strings.Contains(err.Error(), "fork f2 enters work") { - t.Fatalf("f2 omitting left: error = %v, want fork f2 enters work", err) + graph, err := ToStateGraph(stateDefinitionIn(t, machine(``)), nil) + if err != nil { + t.Fatalf("f2 omitting left: %v", err) } - _, err = ToStateGraph(stateDefinitionIn(t, machine(`entry; then c;`)), nil) - if err == nil || !strings.Contains(err.Error(), "region left in state work has no initial state") { - t.Fatalf("third with its own entry: error = %v, want left still refused", err) + requireRegionInitial(t, graph, "work", "left", false, true) + graph, err = ToStateGraph(stateDefinitionIn(t, machine(`entry; then c;`)), nil) + if err != nil { + t.Fatalf("third with its own entry: %v", err) } + requireRegionInitial(t, graph, "work", "left", false, true) + requireRegionInitial(t, graph, "work", "third", true, true) } -// A fork's branches enter every composite state above their targets, so a fork -// into a nested composite state starts an outer sibling region by default, and a -// fork above a composite state whose branch names it starts the regions of that -// state the nearer fork alone enters. A fork reached from inside the outer state -// leaves its other region as it is. -func TestToStateGraph_ForkOnlyRegionEnteredByANestedForkFails(t *testing.T) { +// Nested forks leave every omitted region inactive until a branch or transition enters it. +func TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive(t *testing.T) { machine := func(o2Entry, leftEntry, routes string) string { return ` package test { @@ -401,19 +389,21 @@ func TestToStateGraph_ForkOnlyRegionEnteredByANestedForkFails(t *testing.T) { } ` } - _, err := ToStateGraph(stateDefinitionIn(t, machine(``, ``, + graph, err := ToStateGraph(stateDefinitionIn(t, machine(``, ``, `transition first idle then split; transition first split2 then hold; transition first idle accept Go then split2;`)), nil) - if err == nil || !strings.Contains(err.Error(), "region o2 in state outer has no initial state") || !strings.Contains(err.Error(), "the transition from idle to split enters outer") { - t.Fatalf("nested fork from outside: error = %v, want the transition from idle to split", err) + if err != nil { + t.Fatalf("nested fork from outside: %v", err) } - _, err = ToStateGraph(stateDefinitionIn(t, machine(``, `entry; then a;`, + requireRegionInitial(t, graph, "outer", "o2", false, true) + graph, err = ToStateGraph(stateDefinitionIn(t, machine(``, `entry; then a;`, `transition first idle then split2; transition first split2 then inner;`)), nil) - if err == nil || !strings.Contains(err.Error(), "region right in state inner has no initial state") || !strings.Contains(err.Error(), "the transition from idle to split2 enters inner") { - t.Fatalf("outer fork naming inner: error = %v, want the transition from idle to split2", err) + if err != nil { + t.Fatalf("outer fork naming inner: %v", err) } + requireRegionInitial(t, graph, "inner", "right", false, true) if _, err := ToStateGraph(stateDefinitionIn(t, machine(``, ``, `transition first idle then split2; transition first split2 then hold;`)), nil); err != nil { @@ -421,6 +411,24 @@ func TestToStateGraph_ForkOnlyRegionEnteredByANestedForkFails(t *testing.T) { } } +func requireRegionInitial(t *testing.T, graph *StateGraph, ownerName, regionName string, wantStart, wantFork bool) { + t.Helper() + for region, start := range graph.RegionInitials { + owner := graph.RegionOwner[region] + if owner == nil || owner.Name != ownerName || region.Name != regionName { + continue + } + if (start != nil) != wantStart { + t.Fatalf("region %s.%s start = %v, want initial present %t", ownerName, regionName, start, wantStart) + } + if graph.ForkStarted(region) != wantFork { + t.Fatalf("region %s.%s fork-started = %t, want %t", ownerName, regionName, graph.ForkStarted(region), wantFork) + } + return + } + t.Fatalf("region %s.%s not recorded", ownerName, regionName) +} + // A transition that names a state inside the fork-only region, or one that stays // inside the composite state — directly or through a junction declared there — // starts no region by default and is accepted. diff --git a/internal/ir/lower/state_graph.go b/internal/ir/lower/state_graph.go index 8c9a49a38a..0dd40d3d69 100644 --- a/internal/ir/lower/state_graph.go +++ b/internal/ir/lower/state_graph.go @@ -125,7 +125,7 @@ type StateGraph struct { // selection of an active composite owner. CompositeStateOrder []*ast.StateNode - // RegionInitials: region → initial state + // RegionInitials: region → initial state, nil when default entry leaves it inactive. RegionInitials map[*ast.StateRegion]*ast.StateNode // ParentState: child → parent @@ -158,8 +158,7 @@ type StateGraph struct { // ForkPlans: fork → where its branches lead, checked when the graph is built. ForkPlans map[*ast.PseudostateNode]*ForkPlan - // ForkEntered: region → the forks whose branches enter it, in declaration - // order. A region only forks enter needs no entry transition of its own. + // ForkEntered: region → the forks whose branches enter it, in declaration order. ForkEntered map[*ast.StateRegion][]*ast.PseudostateNode // JoinPlans: join → where its segments come from, checked when the graph is built. @@ -393,9 +392,8 @@ func (g *StateGraph) collectRegions(body []inheritedMember) { } } -// recordRegionInitials records where every region starts and refuses one that -// nothing starts: a top-level region needs an entry transition, a composite -// state's region an entry transition or a fork branch into it. +// recordRegionInitials records where every region starts, refusing only a +// top-level region without an entry transition. func (g *StateGraph) recordRegionInitials() error { for _, region := range g.TopRegions { g.RegionInitials[region] = g.UnconditionalStart(region) @@ -406,14 +404,10 @@ func (g *StateGraph) recordRegionInitials() error { for _, state := range g.CompositeStateOrder { for _, region := range g.CompositeStates[state] { g.RegionInitials[region] = g.UnconditionalStart(region) - if len(g.EntryTransitions[region]) > 0 || g.stateless(region) { - continue - } - if !g.ForkStarted(region) { - return g.noInitialState(region, fmt.Sprintf("region %s in state %s", region.Name, state.Name)) - } - if err := g.checkForkOnlyRegion(state, region); err != nil { - return err + if len(g.EntryTransitions[region]) == 0 && !g.stateless(region) { + if wrapper := g.RegionState[region]; wrapper != nil && g.parallelState[wrapper] { + return g.noInitialState(region, "") + } } } } @@ -963,6 +957,18 @@ func collectStateContents(graph *StateGraph, state *ast.StateNode, scope *symbol return nil } +// recordCompositeState registers a state's regions while retaining their +// declaration order for deterministic consumers. +func (g *StateGraph) recordCompositeState(state *ast.StateNode) { + if _, exists := g.CompositeStates[state]; !exists { + g.CompositeStateOrder = append(g.CompositeStateOrder, state) + } + g.CompositeStates[state] = state.Regions + for _, region := range state.Regions { + g.RegionOwner[region] = state + } +} + // stateless reports whether region is stood for by a state declaring no substates // (behaviors and transitions are not states): such a region // starts in, and stays in, that state, so it needs no initial. @@ -987,18 +993,6 @@ func (g *StateGraph) stateless(region *ast.StateRegion) bool { return true } -// recordCompositeState registers a state's regions while retaining their -// declaration order for deterministic consumers. -func (g *StateGraph) recordCompositeState(state *ast.StateNode) { - if _, exists := g.CompositeStates[state]; !exists { - g.CompositeStateOrder = append(g.CompositeStateOrder, state) - } - g.CompositeStates[state] = state.Regions - for _, region := range state.Regions { - g.RegionOwner[region] = state - } -} - // stateScope returns the scope state's body was declared in, given the scope of // the body that declares it. A state lowering synthesized from a usage or a bare // substate declaration is keyed in the scope tree by that declaration. diff --git a/tools/referee/pssm/classify.go b/tools/referee/pssm/classify.go index aa31360799..6ddb208a73 100644 --- a/tools/referee/pssm/classify.go +++ b/tools/referee/pssm/classify.go @@ -73,9 +73,6 @@ const ( ConstructTesterTrace Construct = "tester trace" ConstructGuardSideEffect Construct = "guard side effect" ConstructGuardBehaviorUnread Construct = "guard behavior not read" - // This project's lowerer refusing a shape UML allows and v2 can spell: - // a candidate gap of ours, recorded apart from v2's missing spellings. - ConstructRegionNoEntry Construct = "lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it" // Recorded but not deciding: a pseudostate filed as a connection point that // is neither an entry nor an exit point and that no transition reaches. ConstructStrayConnectionPoint Construct = "stray connection point" @@ -108,7 +105,6 @@ var constructClass = map[Construct]Expressibility{ ConstructTesterTrace: NotExpressible, ConstructGuardSideEffect: NotExpressible, ConstructGuardBehaviorUnread: NotExpressible, - ConstructRegionNoEntry: NotExpressible, ConstructFork: Extension, ConstructJoin: Extension, ConstructJunction: Extension, @@ -125,10 +121,6 @@ type Use struct { Where string } -// Ours reports whether the construct is unspellable because of this project's -// lowerer rather than because SysML v2 has no notation for it. -func (c Construct) Ours() bool { return c == ConstructRegionNoEntry } - func (u Use) String() string { if u.Where == "" { return string(u.Construct) @@ -170,10 +162,9 @@ func Classify(t *Test) Classification { add(ConstructRedefinedMachine, t.Machine.Name) } w := &walker{ - add: add, - reached: reachedVertices(t.Machine.Regions), - forkEntered: forkEnteredRegions(t.Machine.Regions), - unbound: unboundBehaviors(BindBehaviors(t.Machine)), + add: add, + reached: reachedVertices(t.Machine.Regions), + unbound: unboundBehaviors(BindBehaviors(t.Machine)), } w.connectionPoints(t.Machine.ConnectionPoints) w.regions(t.Machine.Regions) @@ -225,88 +216,9 @@ func reachedVertices(regions []*Region) map[*Vertex]bool { return reached } -// forkEnteredRegions collects every region a fork's branches start, as the -// lowerer plans them: the regions of the fork's owner — the innermost orthogonal -// state below every target — each target lies in, however deep. -func forkEnteredRegions(regions []*Region) map[*Region]bool { - targets := map[*Vertex][]*Vertex{} - var visit func([]*Region) - visit = func(regions []*Region) { - for _, r := range regions { - for _, tr := range r.Transitions { - if tr.Source != nil && tr.Source.Kind == VertexFork && tr.Target != nil { - targets[tr.Source] = append(targets[tr.Source], tr.Target) - } - } - for _, v := range r.Vertices { - visit(v.Regions) - } - } - } - visit(regions) - entered := map[*Region]bool{} - for _, branches := range targets { - owner := forkOwner(branches) - for _, target := range branches { - if region := regionUnder(owner, target); region != nil { - entered[region] = true - } - } - } - return entered -} - -// parentState is the state whose region declares v, nil at the machine's. -func parentState(v *Vertex) *Vertex { - if v.Region == nil { - return nil - } - return v.Region.owner -} - -// forkOwner is the innermost orthogonal state every target lies below, nil when -// only the machine encloses them all. -func forkOwner(targets []*Vertex) *Vertex { - owner := parentState(targets[0]) - for _, target := range targets[1:] { - for owner != nil && !within(owner, target) { - owner = parentState(owner) - } - } - for owner != nil && len(owner.Regions) < 2 { - owner = parentState(owner) - } - return owner -} - -// within reports whether v is owner or lies below it. -func within(owner, v *Vertex) bool { - for s := v; s != nil; s = parentState(s) { - if s == owner { - return true - } - } - return false -} - -// regionUnder is the region of owner that v lies in, nil when owner is the -// machine or v is not below it. -func regionUnder(owner, v *Vertex) *Region { - if owner == nil { - return nil - } - for s := v; s != nil && s != owner; s = parentState(s) { - if s.Region != nil && s.Region.owner == owner { - return s.Region - } - } - return nil -} - type walker struct { - add func(Construct, string) - reached map[*Vertex]bool - forkEntered map[*Region]bool + add func(Construct, string) + reached map[*Vertex]bool // unbound are the behaviors with parameters no event binds. unbound map[*Behavior]bool } @@ -339,7 +251,6 @@ func (w *walker) regions(regions []*Region) { if r.ExtendedRegion != "" { w.add(ConstructExtendedRegion, r.Name) } - w.initial(r) for _, v := range r.Vertices { w.vertex(v) } @@ -359,20 +270,6 @@ func (w *walker) regions(regions []*Region) { } } -// initial records an orthogonal region with neither an initial pseudostate nor -// a fork branch into it: UML allows it, the lowerer refuses it. -func (w *walker) initial(r *Region) { - if r.owner == nil || len(r.owner.Regions) < 2 || w.forkEntered[r] { - return - } - for _, v := range r.Vertices { - if v.Kind == VertexInitial { - return - } - } - w.add(ConstructRegionNoEntry, r.owner.Path()+"/"+r.Name) -} - // guard records a guard whose behavior does more than compute its value, or // whose behavior the reader does not follow: a v2 guard is an expression, and // the evaluator admits no side effect in one. diff --git a/tools/referee/pssm/classify_test.go b/tools/referee/pssm/classify_test.go index 7eaffe0fd0..ace2fcdce3 100644 --- a/tools/referee/pssm/classify_test.go +++ b/tools/referee/pssm/classify_test.go @@ -158,6 +158,13 @@ func TestClassifyTerminateIsStandard(t *testing.T) { } } +func TestClassifyInactiveOrthogonalRegionAsStandard(t *testing.T) { + c := classifyFixture(t, "", ``) + if c.Class != Standard { + t.Fatalf("classified %s (%s), want standard", c.Class, c.Reason()) + } +} + func TestClassifyNoSpellingOutranksAll(t *testing.T) { cases := []struct { name, connectionPoints, body, want string @@ -169,8 +176,6 @@ func TestClassifyNoSpellingOutranksAll(t *testing.T) { {"local transition", "", ``, "local transition TL"}, {"internal transition", "", ``, "internal transition TI"}, {"extended region", "", ``, "extended region R"}, - {"orthogonal region nothing enters", "", ``, "lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it O/R2"}, - {"orthogonal region of a nested orthogonal state a fork passes through", "", ``, "lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it O.N/P; lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it O.N/Q"}, {"redefined state", "", ``, "redefined state R"}, {"guard side effect", "", guardWithSideEffect, "guard side effect T3"}, {"redefined transition", "", ``, "redefined transition TR"}, From 31b7af75d7b4bd212e15282b5959bfebee81d6fa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:10:05 +0000 Subject: [PATCH 05/19] feat(migrate): wrap single-region composite states Co-Authored-By: jason.han --- ...ngle_region_parallel_wrapper.expected.json | 10 + ...state_single_region_parallel_wrapper.sysml | 27 +++ ...ingle_region_parallel_wrapper.trace.golden | 15 ++ .../migrate/diagrams_internal_test.go | 4 +- internal/translate/migrate/edges.go | 6 +- internal/translate/migrate/migrate.go | 8 +- internal/translate/migrate/names.go | 28 ++- internal/translate/migrate/states.go | 185 +++++++++++------ tests/migrate/behavior_more_test.go | 13 +- tests/migrate/behavior_test.go | 2 +- tests/migrate/empty_region_test.go | 12 +- tests/migrate/region_wrapper_test.go | 195 ++++++++++++++++++ tests/migrate/states_test.go | 90 ++++---- .../xmi/orthogonal_initials.golden.report.txt | 32 +-- .../xmi/orthogonal_initials.golden.sysml | 40 ++-- .../xmi/plant_states.golden.report.txt | 14 +- .../testdata/xmi/plant_states.golden.sysml | 28 +-- .../xmi/station_points.golden.report.txt | 44 ++-- .../testdata/xmi/station_points.golden.sysml | 122 +++++------ .../terminate_orthogonal.golden.report.txt | 4 +- .../transition_relocation.golden.report.txt | 10 +- .../xmi/transition_relocation.golden.sysml | 20 +- 22 files changed, 632 insertions(+), 277 deletions(-) create mode 100644 internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.trace.golden create mode 100644 tests/migrate/region_wrapper_test.go diff --git a/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.expected.json b/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.expected.json new file mode 100644 index 0000000000..452970fed4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.expected.json @@ -0,0 +1,10 @@ +{ + "type": "state", + "trace": true, + "events": [{"signal": "Step"}], + "finalState": "Other", + "outputs": { + "entries": {"type": "Integer", "value": 1}, + "exits": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.sysml b/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.sysml new file mode 100644 index 0000000000..cefc153ec2 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.sysml @@ -0,0 +1,27 @@ +package StateSingleRegionParallelWrapper { + private import ScalarValues::*; + + attribute def Step; + + state def Machine { + attribute entries : Integer = 0; + attribute exits : Integer = 0; + entry; then S; + + state S parallel { + entry { assign entries := entries + 1; } + exit { assign exits := exits + 1; } + + state R { + entry; then A; + state A; + state B; + transition first A accept Step then B; + transition first B then done; + } + } + + state Other; + transition first S then Other; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.trace.golden b/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.trace.golden new file mode 100644 index 0000000000..e74363080d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_single_region_parallel_wrapper.trace.golden @@ -0,0 +1,15 @@ +exit: A +enter: B +transition: A -> B (event: accept Step) +exit: B +enter: done +transition: B -> done +exit: done +exit: S (exit action) +stmt action body + stmt assign exits + eval feature exits -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +enter: Other +transition: S -> Other diff --git a/internal/translate/migrate/diagrams_internal_test.go b/internal/translate/migrate/diagrams_internal_test.go index bc8cb39820..8adc7102f1 100644 --- a/internal/translate/migrate/diagrams_internal_test.go +++ b/internal/translate/migrate/diagrams_internal_test.go @@ -151,7 +151,7 @@ func TestDiagramViews(t *testing.T) { `, diagram("_d", "Shutting", "_shutting", "SysML Activity Diagram", "_pump"), - []string{"action def Shut {\n view Shutting {\n expose Sys::Pump;\n render Views::asTextualNotation;\n }\n /* body not migrated"}, Mapped, + []string{"action def Shut {\n view Shutting {\n expose Sys::Pump;\n render Views::asTextualNotation;\n }\n rep language \"JavaScript\" /* 1; */"}, Mapped, "its owner OpaqueBehavior Valve::Shutting is written as the body of action def Valve::Shut, whose method it is"}, {"a diagram owned by an action node is written in the body of the node's activity", ` @@ -250,7 +250,7 @@ func TestDiagramViews(t *testing.T) { `, diagram("_d", "Busy", "_busy", "SysML State Machine Diagram", "_read"), - []string{"state def Modes {\n view 'Busy 2' : StandardViewDefinitions::StateTransitionView {\n expose Modes;\n render Views::asInterconnectionDiagram;\n }\n /* the region has no initial pseudostate: nothing enters it */\n state Busy {\n /* the region has no initial pseudostate: nothing enters it */\n state Read;\n }\n}"}, + []string{"state def Modes {\n view 'Busy 2' : StandardViewDefinitions::StateTransitionView {\n expose Modes;\n render Views::asInterconnectionDiagram;\n }\n /* the region has no initial pseudostate: nothing enters it */\n state Busy parallel {\n state region {\n /* the region has no initial pseudostate: nothing enters it */\n state Read;\n }\n metadata MigrationMetadata::SynthesizedName about region;\n }\n}"}, Approximated, "the view exposes state def Modes, whose graph the rendering draws with the 1 shown nodes and edges of it; its owner State Modes::::Busy has no v2 body; written in state def Modes"}, {"a state shown by another diagram is exposed as the member its state def declares", machineMembers, diff --git a/internal/translate/migrate/edges.go b/internal/translate/migrate/edges.go index 01b0514232..fa12f137dc 100644 --- a/internal/translate/migrate/edges.go +++ b/internal/translate/migrate/edges.go @@ -168,10 +168,10 @@ func (m *migration) wroteEdgeEnding(e, owner *sysmlv1.Element, keyword, name str } // edgePlaceIn locates a member declared in owner's body within the actions nest -// names; a lone region's members are its owner's, a method's its operation's. +// names; an inline region's members are its owner's, a method's its operation's. func (m *migration) edgePlaceIn(owner *sysmlv1.Element, nest []string, name string) edgePlace { for owner != nil && owner.Type == "Region" && owner.Role == "region" { - if _, parallel := m.parallel[owner]; parallel { + if m.regionWrittenAsState(owner) { break } owner = owner.Parent @@ -526,7 +526,7 @@ func (m *migration) drawsNode(el *sysmlv1.Element, f viewForm) bool { k := nodeKind(el) return el.Role == "node" && k != nodePin && k != nodeParam case "StateTransitionView": - return el.Role == "subvertex" || el.Type == "Region" && m.parallel[el] != "" + return el.Role == "subvertex" || el.Type == "Region" && m.regionWrittenAsState(el) } return false } diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index c502b9776a..469154c071 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -149,7 +149,7 @@ func FromModelOptions(name string, model *sysmlv1.Model, opts Options) *Result { satisfied: map[*sysmlv1.Element]map[*sysmlv1.Element]bool{}, framed: map[*sysmlv1.Element]bool{}, taken: map[*sysmlv1.Element]map[string]bool{}, - parallel: map[*sysmlv1.Element]string{}, + regionStates: map[*sysmlv1.Element]string{}, exposed: map[*sysmlv1.Element]string{}, methodOf: map[*sysmlv1.Element]*sysmlv1.Element{}, endNames: map[*sysmlv1.Element]string{}, @@ -364,9 +364,9 @@ type migration struct { // opened holds the member names of each synthesized declaration being // written, outermost first; a reference written inside them avoids those names. opened []columnNames - // parallel names the parallel state each region of an orthogonal state is - // written in; a lone region is written inline and has no name of its own. - parallel map[*sysmlv1.Element]string + // regionStates marks regions written as state usages; orthogonal regions + // name their parallel host, while a wrapped single region has no host name. + regionStates map[*sysmlv1.Element]string // exposed notes, for each feature reached from outside its owner (through // a connector path, a slot or a redefinition), what reaches it. exposed map[*sysmlv1.Element]string diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index 01b360a2bd..1acb6ae012 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -115,6 +115,10 @@ func (m *migration) writtenName(e *sysmlv1.Element) string { } func (m *migration) nameTaken(owner *sysmlv1.Element, name string) bool { + return m.nameTakenExcept(owner, name, nil) +} + +func (m *migration) nameTakenExcept(owner *sysmlv1.Element, name string, except *sysmlv1.Element) bool { if m.taken[owner][name] { return true } @@ -122,6 +126,9 @@ func (m *migration) nameTaken(owner *sysmlv1.Element, name string) bool { return m.topLevelNamed(name) } for _, c := range owner.Children { + if c == except { + continue + } if m.nameOf(c) == name { return true } @@ -193,8 +200,7 @@ func upperFirst(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. +// from the top-level declaration down, the root Model not being written. // A connection point is a member of its owner, whichever region a tool listed it in; // a method is the body of its operation. func (m *migration) segments(e *sysmlv1.Element) []string { @@ -230,9 +236,20 @@ func (m *migration) path(e *sysmlv1.Element) []segment { if cur.Parent == nil && cur.Type == "Model" { break } + var wrappedPointRegion *sysmlv1.Element + if owner := pointOwner(cur); owner != nil && owner.Type == "State" { + if regions := m.populatedRegions(owner); len(regions) == 1 && + m.regionWrittenAsState(regions[0]) && m.regionStates[regions[0]] == "" { + wrappedPointRegion = regions[0] + } + } if cur.Type == "Region" && cur.Role == "region" { - if p, ok := m.parallel[cur]; ok { - segs = append([]segment{{name: p}, {name: m.nameFor(cur)}}, segs...) + if m.regionWrittenAsState(cur) { + if p := m.regionStates[cur]; p != "" { + segs = append([]segment{{name: p}, {name: m.nameFor(cur)}}, segs...) + } else { + segs = append([]segment{{name: m.nameFor(cur), feature: true, elem: cur}}, segs...) + } } continue } @@ -243,6 +260,9 @@ func (m *migration) path(e *sysmlv1.Element) []segment { if within, ok := m.nestedIn[cur]; ok { segs = append([]segment{{name: within, feature: true}}, segs...) } + if wrappedPointRegion != nil { + segs = append([]segment{{name: m.nameFor(wrappedPointRegion), feature: true, elem: wrappedPointRegion}}, segs...) + } } return segs } diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index 86469fbaf3..1cc02ecee5 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -99,7 +99,7 @@ func (m *migration) stateMachineBody(sm *sysmlv1.Element) { for _, cp := range sm.Owned("connectionPoint") { m.connectionPoint(cp) } - m.regions(sm, m.populatedRegions(sm), false, func() { /* no extra nesting to write */ }) + m.regions(sm, m.populatedRegions(sm), false, func() {}, nil) m.writeRelocatedTransitions(sm) }) } @@ -172,10 +172,8 @@ func (m *migration) indexTransitions(sm *sysmlv1.Element) { 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 -// used names, nil for a body no v1 element owns. +// nameRegions names a machine or state's regions: a State's one region and +// several regions become state usages; a StateMachine's one stays inline. func (m *migration) nameRegions(regions []*sysmlv1.Element, owner *sysmlv1.Element, used map[string]bool) { if len(regions) > 1 { name := m.freshMember(owner, used, "regions") @@ -186,11 +184,27 @@ func (m *migration) nameRegions(regions []*sysmlv1.Element, owner *sysmlv1.Eleme rname, m.synthesized[r] = "region", true } m.names[r] = freshIn(inner, rname) - m.parallel[r] = name + m.regionStates[r] = name m.nameRegion(r, nil, inheritedStateNamesSet()) } return } + if len(regions) == 1 && owner.Type == "State" { + r := regions[0] + rname := m.nameOf(r) + if rname == "" { + rname, m.synthesized[r] = "region", true + } + m.names[r] = m.freshMemberExcept(owner, r, used, rname) + m.regionStates[r] = "" + inner := inheritedStateNamesSet() + m.namePoints(owner, inner) + m.nameRegion(r, nil, inner) + for _, pr := range pointRegions(owner) { + m.regionUsed[pr] = inner + } + return + } for _, r := range regions { m.nameRegion(r, owner, used) } @@ -342,7 +356,7 @@ func (m *migration) entryPointForm(v, owner *sysmlv1.Element) pointForm { note += "; no initial pseudostate starts the " + pluralRegion(len(withoutInitial)) + " " + strings.Join(withoutInitial, ", ") + ", which v1 too leaves inactive on entering the state" } if len(withoutEntry) > 0 { - note += "; no initial pseudostate of the " + pluralRegion(len(withoutEntry)) + " " + strings.Join(withoutEntry, ", ") + " enters it, so no entry is written for it" + note += "; no initial pseudostate enters the " + pluralRegion(len(withoutEntry)) + " " + strings.Join(withoutEntry, ", ") + ", which v1 too leaves inactive on entering the state" } return pointForm{defaultEntry: true, note: note} } @@ -545,15 +559,26 @@ func (m *migration) vertexWritten(v *sysmlv1.Element) bool { return ok } -// regionWritten reports whether region r is written as a member of its own: a sub-state -// of the parallel state an orthogonal state's regions become, in a machine that is written. +// regionWritten reports whether r is written as a sub-state in a written machine. func (m *migration) regionWritten(r *sysmlv1.Element) bool { sm := machineOf(r) if sm == nil || !m.written(sm) { return false } m.nameMachine(sm) - return m.parallel[r] != "" + return m.regionWrittenAsState(r) +} + +// regionWrittenAsState reports whether r is a sub-state that supplies a +// region of a parallel state rather than an inline region body. +func (m *migration) regionWrittenAsState(r *sysmlv1.Element) bool { + _, ok := m.regionStates[r] + return ok +} + +func (m *migration) wrappedRegion(owner *sysmlv1.Element, regions []*sysmlv1.Element) bool { + return owner.Type == "State" && len(regions) == 1 && + m.regionWrittenAsState(regions[0]) && m.regionStates[regions[0]] == "" } // nameVertex gives a vertex that is written as a member its name in the body @@ -589,6 +614,15 @@ func (m *migration) freshMember(owner *sysmlv1.Element, used map[string]bool, ba return name } +func (m *migration) freshMemberExcept(owner, except *sysmlv1.Element, used map[string]bool, base string) string { + name := base + for i := 2; used[name] || m.nameTakenExcept(owner, name, except); i++ { + name = base + strconv.Itoa(i) + } + used[name] = true + return name +} + // vertexBase is the name a vertex of a kind that is written as a member takes // when anonymous; "" for a kind that is not. func vertexBase(v *sysmlv1.Element) string { @@ -608,11 +642,9 @@ func vertexBase(v *sysmlv1.Element) string { return "" } -// regions writes the regions of a state machine or composite state: one inline, -// several as the sub-states of one parallel state. The entry succession follows -// the body's entry action when entered says one was written; between writes -// the members that come after it and before the states. -func (m *migration) regions(owner *sysmlv1.Element, regions []*sysmlv1.Element, entered bool, between func()) { +// regions writes the regions of a state machine or composite state: a State's +// one region is a parallel sub-state, while one machine region stays inline. +func (m *migration) regions(owner *sysmlv1.Element, regions []*sysmlv1.Element, entered bool, between func(), wrapped func(*stateRegion)) { switch len(regions) { case 0: between() @@ -623,22 +655,21 @@ func (m *migration) regions(owner *sysmlv1.Element, regions []*sysmlv1.Element, } case 1: st := m.region(regions[0]) - st.enter(entered) + if wrapped == nil { + st.enter(entered) + between() + st.write() + return + } between() - st.write() + name := writeName(m.names[regions[0]]) + m.w.block(stateKw+name, func() { wrapped(st) }) + m.madeUp(regions[0], name) + state := writeName(m.nameFor(owner)) + m.add(regions[0], Mapped, name, "the region is written as the sub-state "+name+" of the parallel state "+state+", as a composite state's regions are") default: - name := m.parallel[regions[0]] - withoutInitial, withoutEntry := m.regionsWithoutEntry(regions) - switch { - case len(withoutInitial) == 0 && len(withoutEntry) == 0: - m.w.line(entryThen(entered, "", writeName(name))) - case len(withoutEntry) == 0: - m.w.lines(commentLines("no default entry: the " + pluralRegion(len(withoutInitial)) + " " + strings.Join(withoutInitial, ", ") + - " have no initial pseudostate, so only a fork or a transition naming a nested state enters the regions")) - default: - m.w.lines(commentLines("no default entry: these regions have no written entry: " + strings.Join(slices.Concat(withoutInitial, withoutEntry), ", ") + - "; only a fork or a transition naming a nested state enters them")) - } + name := m.regionStates[regions[0]] + m.w.line(entryThen(entered, "", writeName(name))) between() m.w.block(stateKw+writeName(name)+" parallel", func() { for _, r := range regions { @@ -657,22 +688,6 @@ func (m *migration) regions(owner *sysmlv1.Element, regions []*sysmlv1.Element, } } -// regionsWithoutEntry names the regions nothing enters: those no initial pseudostate -// starts, and apart those whose initial pseudostates enter other regions or nothing. -func (m *migration) regionsWithoutEntry(regions []*sysmlv1.Element) (withoutInitial, withoutEntry []string) { - for _, r := range regions { - e := m.regionEntry(r) - switch { - case e.init != nil: - case len(e.strays) == 0 && len(e.dangling) == 0: - withoutInitial = append(withoutInitial, describe(r)) - default: - withoutEntry = append(withoutEntry, describe(r)) - } - } - return withoutInitial, withoutEntry -} - // regionEntry is what enters a region: the initial pseudostate written as its // entry — one of the region's own whose transition enters it, or one a stray // initial of an orthogonal region donates, its donor — with the region's further @@ -871,10 +886,25 @@ func pluralRegion(n int) string { return "regions" } +// regionsWithoutEntry names regions no initial pseudostate enters. +func (m *migration) regionsWithoutEntry(regions []*sysmlv1.Element) (withoutInitial, withoutEntry []string) { + for _, r := range regions { + e := m.regionEntry(r) + switch { + case e.init != nil: + case len(e.strays) == 0 && len(e.dangling) == 0: + withoutInitial = append(withoutInitial, describe(r)) + default: + withoutEntry = append(withoutEntry, describe(r)) + } + } + return withoutInitial, withoutEntry +} + // region prepares to write region r of the machine that nameMachine named. func (m *migration) region(r *sysmlv1.Element) *stateRegion { s := &stateRegion{m: m, r: r, owner: r.Parent, used: m.regionUsed[r], machine: machineOf(r)} - if _, parallel := m.parallel[r]; parallel { + if m.regionWrittenAsState(r) { s.owner = nil } return s @@ -947,7 +977,9 @@ func (s *stateRegion) write() { } } s.m.writeComments(s.r, false) - s.m.add(s.r, Mapped, "", "the one region is written as the body of its owner") + if !s.m.regionWrittenAsState(s.r) { + s.m.add(s.r, Mapped, "", "the one region is written as the body of its owner") + } } // name returns the v2 name nameMachine gave a vertex. @@ -1225,6 +1257,10 @@ func (s *stateRegion) state(v *sysmlv1.Element) { defers := s.deferrals(v, do, exit) head := s.stateHead(v, name) regions := s.m.populatedRegions(v) + wrapped := s.m.wrappedRegion(v, regions) + if wrapped { + head += " parallel" + } inv := firstOwned(v, "stateInvariant") points := s.m.connectionPoints(v) pointRegs := pointRegions(v) @@ -1254,20 +1290,39 @@ func (s *stateRegion) state(v *sysmlv1.Element) { s.m.invariant(inv) } entered := entry != nil && s.m.inlineBehavior("entry action", entry, v) - between := s.betweenActions(v, do, exit, points, defers) + between := s.betweenActions(v, do, exit, defers) + if !wrapped && len(points) > 0 { + writePoints := between + between = func() { + writePoints() + s.m.statePoints(v) + } + } if len(regions) == 0 { between() } else { - s.m.regions(v, regions, entered, between) + var writeWrapped func(*stateRegion) + if wrapped { + writeWrapped = func(region *stateRegion) { + s.m.statePoints(v) + region.enter(false) + region.write() + s.pointTransitions(regions, pointRegs) + s.m.writeRelocatedTransitions(v) + } + } + s.m.regions(v, regions, entered, between, writeWrapped) + } + if !wrapped { + s.pointTransitions(regions, pointRegs) + s.m.writeRelocatedTransitions(v) } - s.pointTransitions(regions, pointRegs) - s.m.writeRelocatedTransitions(v) }) } // betweenActions is a state's block body after its entry action: its do and -// exit actions and its connection points. -func (s *stateRegion) betweenActions(v, do, exit *sysmlv1.Element, points []*sysmlv1.Element, defers *deferrals) func() { +// exit actions and the encoding of any deferred triggers. +func (s *stateRegion) betweenActions(v, do, exit *sysmlv1.Element, defers *deferrals) func() { return func() { switch { case defers.encoded(): @@ -1281,9 +1336,6 @@ func (s *stateRegion) betweenActions(v, do, exit *sysmlv1.Element, points []*sys s.m.inlineBehavior(exitAction, exit, v) } } - if len(points) > 0 { - s.m.statePoints(v) - } } } @@ -2325,7 +2377,8 @@ func (s *stateRegion) path(v *sysmlv1.Element) (string, bool) { if !ok || machineOf(v) != s.machine { return "", false } - if owner := memberOwner(v); owner == s.machine || owner == s.r && !s.ownerScope { + owner := memberOwner(v) + if owner == s.machine || owner == s.r && !s.ownerScope || s.pointInWrappedRegion(v) { return writeName(name), true } segs := s.m.segments(v) @@ -2336,6 +2389,12 @@ func (s *stateRegion) path(v *sysmlv1.Element) (string, bool) { return qualifiedStatePathWithSeparator(segs[len(s.m.segments(base)):], "."), true } +func (s *stateRegion) pointInWrappedRegion(v *sysmlv1.Element) bool { + return !s.ownerScope && s.r.Parent != nil && s.r.Parent.Type == "State" && + s.m.regionWrittenAsState(s.r) && s.m.regionStates[s.r] == "" && + pointOwner(v) == s.r.Parent +} + // qualifiedStatePathWithSeparator joins state path segments with the requested separator. func qualifiedStatePathWithSeparator(segs []string, separator string) string { parts := make([]string, len(segs)) @@ -2352,7 +2411,7 @@ func (s *stateRegion) endpoint(t, v *sysmlv1.Element, role string) (string, bool if !ok { return "", false } - if owner := memberOwner(v); owner != s.r && owner != s.machine { + if owner := memberOwner(v); owner != s.r && owner != s.machine && !s.pointInWrappedRegion(v) { s.m.add(t, Mapped, "", "the "+role+" "+describe(v)+" lies in another region and is named by its path "+p) } return p, true @@ -2631,9 +2690,13 @@ func (m *migration) writeRelocatedTransitions(host *sysmlv1.Element) { return } s := m.region(regions[0]) - s.owner = host - s.ownerScope = true - if host.Type == "State" { + if m.wrappedRegion(host, regions) { + s.owner = host + } else { + s.owner = host + s.ownerScope = true + } + if host.Type == "State" && !m.wrappedRegion(host, regions) { s.used = m.stateUsed[host] } for _, transition := range transitions { diff --git a/tests/migrate/behavior_more_test.go b/tests/migrate/behavior_more_test.go index 7f86296081..64fe653596 100644 --- a/tests/migrate/behavior_more_test.go +++ b/tests/migrate/behavior_more_test.go @@ -851,7 +851,7 @@ func TestStrictDeferralSurvivesInactiveSubstateTransition(t *testing.T) { r := migrateDocumentOptions(t, machine, ``, migrate.Options{Strict: true}) wantNoLine(t, r.Notation, "defer Door;") for _, line := range []string{ - "state Busy {", + "state Busy parallel {", "item deferred : Door[*] ordered;", "#MigrationMetadata::DeferredKeeper action receive accept kept : Door;", "transition first Heating accept Door then Opened;", @@ -2071,22 +2071,21 @@ const loggingMachine = ` const loggingApplications = ` ` -// A state's entry and exit behaviors and the states of its one region are -// members of one v2 body, so those sharing a name are told apart the way any -// clashing members are; the region's entry follows the state's own entry action. +// A state's entry and exit behaviors stay on the owner, and its one region is a +// nested state whose entry remains inside that region. func TestStateBehaviorsSharingANameAreDistinguished(t *testing.T) { r := migrateDocument(t, loggingMachine, loggingApplications) for _, line := range []string{ + "state On parallel {", "entry action log {", "exit action 'log 2' {", + "state region {", + "entry; then 'log 3';", "then 'log 3';", "state 'log 3';", } { wantLine(t, r.Notation, line) } - if strings.Contains(string(r.Notation), "entry; then 'log 3'") { - t.Errorf("the region's entry is written as a second entry action:\n%s", r.Notation) - } wantNote(t, r, "_onExit", migrate.Approximated, "written as v2 assignments") if es := entriesFor(r, "_onExit"); len(es) == 1 && es[0].Target != "Logger::Logging::On::'log 2'" { t.Errorf("the exit behavior's report target is %q, not the name written", es[0].Target) diff --git a/tests/migrate/behavior_test.go b/tests/migrate/behavior_test.go index 5a444fa659..d58b4e4e13 100644 --- a/tests/migrate/behavior_test.go +++ b/tests/migrate/behavior_test.go @@ -696,7 +696,7 @@ func TestStateMachineMigratesToAnExecutableStateDef(t *testing.T) { for _, line := range []string{ "state def Control {", "entry; then Idle;", - "state Busy {", + "state Busy parallel {", "entry; then Warm;", "transition first Warm accept after 2.0 [SI::s] then Hot;", "state Cool : Cooling { in ref :>> context = Control::context; }", diff --git a/tests/migrate/empty_region_test.go b/tests/migrate/empty_region_test.go index 6b45987797..271418705f 100644 --- a/tests/migrate/empty_region_test.go +++ b/tests/migrate/empty_region_test.go @@ -36,29 +36,29 @@ const emptyRegionApplications = ` ` -// A region with no vertex is not written: a state for it would be a region -// nothing enters, which the runtime refuses. The populated sibling region is -// then the state's only region and is written inline, and the machine runs. +// A region with no vertex is not written. The populated sibling region is +// wrapped under the composite state, and the machine runs. func TestEmptyRegionsAreNotWritten(t *testing.T) { r := migrateDocument(t, emptyRegionMachine, emptyRegionApplications) for _, line := range []string{ "state def Life {", "entry; then On;", - "state On {", + "state On parallel {", + "state work {", "entry; then Run;", "state def Idle {", "/* the StateMachine's regions hold no vertex: nothing enters them */", } { wantLine(t, r.Notation, line) } - for _, line := range []string{"parallel", "state spare"} { + for _, line := range []string{"state spare"} { if strings.Contains(string(r.Notation), line) { t.Errorf("notation writes %q for an empty region:\n%s", line, r.Notation) } } wantNote(t, r, "_r2", migrate.Skipped, "the region holds no vertex, so nothing enters it and no state is written for it") wantNote(t, r, "_r3", migrate.Skipped, "the region holds no vertex, so nothing enters it and no state is written for it") - wantNote(t, r, "_r1", migrate.Mapped, "the one region is written as the body of its owner") + wantNote(t, r, "_r1", migrate.Mapped, "the region is written as the sub-state work of the parallel state On, as a composite state's regions are") s := session(t, r) meta(t, s, "%instantiate Device") diff --git a/tests/migrate/region_wrapper_test.go b/tests/migrate/region_wrapper_test.go new file mode 100644 index 0000000000..e3f560d0cd --- /dev/null +++ b/tests/migrate/region_wrapper_test.go @@ -0,0 +1,195 @@ +package migrate_test + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/translate/migrate" +) + +const singleRegionWrapperMachine = ` + + + + + + + + + + + + + + + + + JavaScript + visits = visits + 1; + + + JavaScript + visits = visits + 10; + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const singleRegionWrapperApplications = ` + ` + +func TestSingleRegionCompositeStateIsWrapped(t *testing.T) { + r := migrateDocument(t, singleRegionWrapperMachine, singleRegionWrapperApplications) + for _, line := range []string{ + "state S parallel {", + "state R {", + "entry; then A;", + "#StateMachines::shallowHistory state history;", + "transition first Outside accept Open then S.R.A;", + "transition first A accept Step then B;", + "transition first S.R.B accept Leave then Outside;", + "transition first B accept Finish then done;", + "entry action remember", + "exit action record", + } { + wantLine(t, r.Notation, line) + } + wantNote(t, r, "_inner", migrate.Mapped, "the region is written as the sub-state R of the parallel state S, as a composite state's regions are") + wantClean(t, "single-region composite state wrapper", r) + + s := session(t, r) + meta(t, s, "%instantiate Rig") + meta(t, s, "%state Rig::Run #1") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Outside") { + t.Fatalf("initial state: %s", out) + } + meta(t, s, "%send Open") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: A") { + t.Fatalf("Open did not enter S.R.A:\n%s", out) + } + meta(t, s, "%send Step") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: B") { + t.Fatalf("Step did not enter S.R.B:\n%s", out) + } + meta(t, s, "%send Leave") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Outside") { + t.Fatalf("Leave did not exit S:\n%s", out) + } + meta(t, s, "%send Open") + meta(t, s, "%step") + meta(t, s, "%send Step") + meta(t, s, "%step") + meta(t, s, "%send Finish") + meta(t, s, "%step") + meta(t, s, "%step") + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: Other") { + t.Fatalf("R's final state did not complete S and fire its completion transition:\n%s", out) + } +} + +func TestSingleRegionCompositeStateConnectionPointsStayInRegion(t *testing.T) { + xmi := ` + + + + + + + + + + + + + + + + + + + + + + + + + + ` + r := migrateDocument(t, xmi, singleRegionWrapperApplications) + for _, line := range []string{"state S parallel {", "state R {", "#StateMachines::junction state 'in';", "#StateMachines::junction state 'out';", "transition first 'in' then Active;", "transition first Active then 'out';", "transition first Idle accept Go then S.R.'in';", "transition first S.R.'out' then Idle;"} { + wantLine(t, r.Notation, line) + } + wantClean(t, "wrapped state connection points", r) +} + +func TestStateMachineSingleRegionRemainsFlattened(t *testing.T) { + xmi := ` + + + + + + + + + ` + r := migrateDocument(t, xmi, singleRegionWrapperApplications) + wantLine(t, r.Notation, "entry; then Active;") + wantLine(t, r.Notation, "state Active;") + if strings.Contains(string(r.Notation), "parallel") || strings.Contains(string(r.Notation), "state main {") { + t.Errorf("the StateMachine's single region was wrapped:\n%s", r.Notation) + } + wantClean(t, "single-region state machine", r) +} + +func TestUnnamedCompositeRegionGetsFreshStateName(t *testing.T) { + xmi := ` + + + + + + ` + integerHref + ` + + + + + + + + + + ` + r := migrateDocument(t, xmi, singleRegionWrapperApplications) + for _, line := range []string{"state S parallel {", "state region2 {", "entry; then Active;"} { + wantLine(t, r.Notation, line) + } + wantNote(t, r, "_inner", migrate.Mapped, "the region is written as the sub-state region2 of the parallel state S") + wantClean(t, "unnamed composite region", r) +} diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index 249f88c4eb..1ebcdb1d45 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -16,7 +16,7 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { "state def Line {", "attribute dawn : Time::TimeInstantValue = 21600.0 [SI::s];", "entry; then Idle;", - "state Work {", + "state Work parallel {", "entry; then Prep;", "#StateMachines::shallowHistory state last;", "#StateMachines::deepHistory state deepest;", @@ -27,12 +27,12 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { "join gather;", "state Both {", "state regions parallel {", - "transition first Work.Run accept Stop then Idle;", - "transition first Idle accept Resume then Work.Run;", + "transition first Work.steps.Run accept Stop then Idle;", + "transition first Idle accept Resume then Work.steps.Run;", "transition first Pause accept Resume", "do action log {", `rep language "JavaScript" /* println("resuming at " + count); */`, - "then Work.last;", + "then Work.steps.last;", "transition first Idle accept Enter then Cell.warmStart;", "transition first Cell.spent then Idle;", "transition first route if context.count < 2 then Work;", @@ -43,7 +43,7 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { "transition first Both.regions.b.B2 then gather;", "transition first gather then Idle;", "transition first Idle accept Ping then Idle;", - "transition first Work accept Bump then Work.Prep;", + "transition first Work accept Bump then Work.steps.Prep;", "transition first Pause accept at dawn then Idle;", "state def CellMachine {", "state warmStart;", @@ -56,8 +56,8 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { if strings.Contains(string(r.Notation), "accept at never") || strings.Contains(string(r.Notation), "accept Unused") { t.Errorf("an unresolvable instant or an unreferenced event was written:\n%s", r.Notation) } - wantNote(t, r, "_tStop", migrate.Mapped, "the source 'Run' lies in another region and is named by its path Work.Run") - wantNote(t, r, "_tDirect", migrate.Mapped, "the target 'Run' lies in another region and is named by its path Work.Run") + wantNote(t, r, "_tStop", migrate.Mapped, "the source 'Run' lies in another region and is named by its path Work.steps.Run") + wantNote(t, r, "_tDirect", migrate.Mapped, "the target 'Run' lies in another region and is named by its path Work.steps.Run") wantNote(t, r, "_junc", migrate.Mapped, "written as a #StateMachines::junction state pseudostate") wantNote(t, r, "_gSpent", migrate.Mapped, "an else guard is written as the unguarded transition out of the junction") wantNote(t, r, "_fork", migrate.Mapped, "written as a fork pseudostate") @@ -184,10 +184,10 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { func TestNestedTransitionRelocatesToCommonAncestor(t *testing.T) { for _, opts := range []migrate.Options{{}, {Strict: true}} { r := migrateFixtureFileOptions(t, "transition_relocation", opts) - wantLine(t, r.Notation, "transition first Work.Run accept Stop then Parked.Ready;") + wantLine(t, r.Notation, "transition first Work.work.Run accept Stop then Parked.parked.Ready;") wantNote(t, r, "_region4Init", migrate.Approximated, "coincides with region3's own entry into 'wait3'") wantNote(t, r, "_crossRegionInit", migrate.Approximated, "coincides with region3's own entry into 'wait3'") - wantLine(t, r.Notation, "no default entry: these regions have no written entry: 'region4'") + wantLine(t, r.Notation, "entry; then regions;") wantLine(t, r.Notation, "every initial pseudostate of the region enters another region: nothing enters this one") if strings.Count(string(r.Notation), "then wait3;") != 1 { t.Errorf("region3's entry into wait3 is not written exactly once:\n%s", r.Notation) @@ -257,13 +257,18 @@ func TestInitialIntoOrthogonalRegionEntersThatRegion(t *testing.T) { } // (d) two initials both entering the region: the first is written, the second refused. - wantLine(t, r.Notation, "state Duplicate {\n /* not migrated: Pseudostate (_uaInit2) — a region has one initial pseudostate; (_uaInit1) is written as it */\n entry; then ua1;") + wantLine(t, r.Notation, "state Duplicate parallel {") + wantLine(t, r.Notation, "state ua {") + wantLine(t, r.Notation, "/* not migrated: Pseudostate (_uaInit2) — a region has one initial pseudostate; (_uaInit1) is written as it */") + wantLine(t, r.Notation, "entry; then ua1;") wantNote(t, r, "_uaInit1", migrate.Mapped, "written as the entry of the region") - // (e) the target lies in a nested composite state's one region, written as that state's body. - wantLine(t, r.Notation, "state Inner {\n entry; then inner1;") - wantNote(t, r, "_naStray", migrate.Approximated, "written as the entry of the body of 'Inner', which owns its target 'inner1', not of na") - wantNote(t, r, "_naStrayT", migrate.Approximated, "so it is written as the entry of the body of 'Inner'") + // (e) the target lies in a nested composite state's wrapped region. + wantLine(t, r.Notation, "state Inner parallel {") + wantLine(t, r.Notation, "state ni {") + wantLine(t, r.Notation, "entry; then inner1;") + wantNote(t, r, "_naStray", migrate.Approximated, "written as the entry of ni, which owns its target 'inner1', not of na") + wantNote(t, r, "_naStrayT", migrate.Approximated, "so it is written as the entry of ni") // (f) the target region's own initial has no transition, so it is no entry: the stray one is donated. wantLine(t, r.Notation, "state gb {\n /* not migrated: Pseudostate (_gbInit) — no transition leaves the initial pseudostate */\n entry; then gb1;") @@ -271,7 +276,9 @@ func TestInitialIntoOrthogonalRegionEntersThatRegion(t *testing.T) { wantNote(t, r, "_gaStray", migrate.Approximated, "written as the entry of gb, which owns its target 'gb1', not of ga") // (g) a target in a region of another, non-orthogonal state is refused, not donated. - wantLine(t, r.Notation, "state Work {\n /* not migrated: Pseudostate (_wInit) — the initial transition's target has no v2 form here */") + wantLine(t, r.Notation, "state Work parallel {") + wantLine(t, r.Notation, "state wr {") + wantLine(t, r.Notation, "/* not migrated: Pseudostate (_wInit) — the initial transition's target has no v2 form here */") wantNote(t, r, "_wInitT", migrate.Unmapped, "its target 'idle1' lies in 'ir', a region of 'Idle' and no orthogonal region of the pseudostate's; an initial transition enters its own region") if strings.Count(s, "then idle1;") != 1 { t.Errorf("Idle's own entry into idle1 is not written exactly once:\n%s", s) @@ -339,7 +346,7 @@ func TestSubmachineStateBlockRedeclaresParametersForEntry(t *testing.T) { func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { r := migrateFixtureFile(t, "station_points") for _, line := range []string{ - "state Work {", + "state Work parallel {", "#StateMachines::junction state Start;", "#StateMachines::junction state Leave;", "#StateMachines::junction state Deep;", @@ -351,17 +358,17 @@ func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { "transition first Start", "transition first Run accept Finish", "then Leave;", - "transition first Run.Out", + "transition first steps.Run.pace.Out", "fork Both;", "join Gather;", "transition first Both then regions.a.A1;", "transition first Both then regions.b.B1;", "then Gather;", - "then Work.Start;", + "then Work.steps.Start;", "transition first Idle accept Enter then Work;", - "then Work.Run.Deep;", - "transition first Idle accept Resume then Work.H;", - "transition first Work.Leave", + "then Work.steps.Run.pace.Deep;", + "transition first Idle accept Resume then Work.steps.H;", + "transition first Work.steps.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 */", @@ -379,8 +386,8 @@ func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { 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, "_tGo", migrate.Mapped, "named by its path Work.steps.Start") + wantNote(t, r, "_tDive", migrate.Mapped, "named by its path Work.steps.Run.pace.Deep") wantNote(t, r, "_tBothA", migrate.Mapped, "named by its path Both") wantNote(t, r, "_tAg", migrate.Mapped, "named by its path Gather") wantNote(t, r, "_hist", migrate.Mapped, "written as a `#StateMachines::shallowHistory state` shallow history") @@ -427,7 +434,7 @@ func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { // then the transition out of it. one := station() drive("Go", "Idle -> Start") - current("Slow") + current("Run | Slow") trace(one, "41112314") drive("Finish", "Run -> Leave") current("Idle") @@ -440,7 +447,7 @@ func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { // then the transition out of it into Prep, still inside Work. two := station() drive("Dive", "Idle -> Deep") - current("Fast") + current("Run | Fast") trace(two, "4211142117") drive("Back", "Fast -> Out") current("Prep") @@ -458,7 +465,7 @@ func TestCompositeStateConnectionPointsKeepTheUMLOrder(t *testing.T) { drive("Stop", "Work -> Idle") trace(three, "111314151244") drive("Resume", "Idle -> H") - current("Slow") + current("Run | Slow") trace(three, "1113141512441114") meta(t, s, "%stop") @@ -521,18 +528,18 @@ const gateMachine = ` const gateApplications = ` ` -// An empty region is skipped when the machine's vertices are named as when they are -// written, so the remaining region's states are inline and a transition across nesting -// levels names its far end by a path that exists. +// An empty region is skipped while the populated region remains nested under its +// composite owner; transitions name vertices through the generated region state. func TestEmptyRegionLeavesNoPhantomPath(t *testing.T) { r := migrateDocument(t, gateMachine, gateApplications) for _, line := range []string{ "state def Latch {", "entry; then Idle;", - "state Busy {", + "state Busy parallel {", + "state region {", "entry; then Inner;", - "transition first Idle accept Open then Busy.Inner;", - "transition first Busy.Inner accept Shut then Idle;", + "transition first Idle accept Open then Busy.region.Inner;", + "transition first Busy.region.Inner accept Shut then Idle;", } { wantLine(t, r.Notation, line) } @@ -540,7 +547,7 @@ func TestEmptyRegionLeavesNoPhantomPath(t *testing.T) { t.Errorf("a parallel state was named for a machine with one populated region:\n%s", r.Notation) } wantNote(t, r, "_gUnused", migrate.Skipped, "the region holds no vertex, so nothing enters it and no state is written for it") - wantNote(t, r, "_gtIn", migrate.Mapped, "the target 'Inner' lies in another region and is named by its path Busy.Inner") + wantNote(t, r, "_gtIn", migrate.Mapped, "the target 'Inner' lies in another region and is named by its path Busy.region.Inner") s := session(t, r) meta(t, s, "%instantiate Gate") @@ -1046,7 +1053,7 @@ const guardedEntryApplications = ` // 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{"#StateMachines::junction state arm;", "transition first arm if context.armed then W2;", "transition first Idle accept Go then Work.arm;"} { + for _, line := range []string{"#StateMachines::junction state arm;", "transition first arm if context.armed then W2;", "transition first Idle accept Go then Work.r.arm;"} { if !strings.Contains(string(r.Notation), line) { t.Errorf("missing %q in:\n%s", line, r.Notation) } @@ -1256,24 +1263,25 @@ 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. +// A point-only region is skipped, leaving one populated region wrapped under the +// composite state; its connection points are named through that region. func TestPointOnlyRegionIsNotAParallelBranch(t *testing.T) { r := migrateDocument(t, pointOnlyRegion, pointOnlyRegionApplications) for _, line := range []string{ + "state Sync parallel {", + "state a {", "#StateMachines::junction state Both;", "#StateMachines::junction state Gather;", - "transition first Idle accept Go then Sync.Both;", + "transition first Idle accept Go then Sync.a.Both;", "transition first Both then A2;", "transition first A2 accept Stop then Gather;", - "transition first Sync.Gather then Idle;", + "transition first Sync.a.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 "} { + for _, bad := range []string{"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) } @@ -1539,7 +1547,7 @@ func TestExitPointReachedFromOutsideIsRefused(t *testing.T) { if n := strings.Count(string(r.Notation), "#StateMachines::junction state 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;"} { + for _, line := range []string{"transition first Self accept Go then Self.r.leave;", "transition first Self.r.leave then Idle;"} { if !strings.Contains(string(r.Notation), line) { t.Errorf("missing %q in:\n%s", line, r.Notation) } diff --git a/tests/migrate/testdata/xmi/orthogonal_initials.golden.report.txt b/tests/migrate/testdata/xmi/orthogonal_initials.golden.report.txt index fb253b1342..5d280de76a 100644 --- a/tests/migrate/testdata/xmi/orthogonal_initials.golden.report.txt +++ b/tests/migrate/testdata/xmi/orthogonal_initials.golden.report.txt @@ -16,8 +16,8 @@ Pseudostate Plant::Life::main::Dangle::ga:: _gaStray (written as th Transition Plant::Life::main::Dangle::ga:: _gaStrayT (its target 'gb1' lies in gb, an orthogonal region of the pseudostate's, so it is written as the entry of gb) Pseudostate Plant::Life::main::Donate::da:: _daStray (written as the entry of db, which owns its target 'db1', not of da, the orthogonal region that owns the pseudostate) Transition Plant::Life::main::Donate::da:: _daStrayT (its target 'db1' lies in db, an orthogonal region of the pseudostate's, so it is written as the entry of db) -Pseudostate Plant::Life::main::Nested::na:: _naStray (written as the entry of the body of 'Inner', which owns its target 'inner1', not of na, the orthogonal region that owns the pseudostate) -Transition Plant::Life::main::Nested::na:: _naStrayT (its target 'inner1' lies in the body of 'Inner', an orthogonal region of the pseudostate's, so it is written as the entry of the body of 'Inner') +Pseudostate Plant::Life::main::Nested::na:: _naStray (written as the entry of ni, which owns its target 'inner1', not of na, the orthogonal region that owns the pseudostate) +Transition Plant::Life::main::Nested::na:: _naStrayT (its target 'inner1' lies in ni, an orthogonal region of the pseudostate's, so it is written as the entry of ni) ## mapped (81) Model Model _m (the root model's members are written at the top level) @@ -38,54 +38,54 @@ Trigger Plant::Life::main:::: _toDuplicateTrigger Trigger Plant::Life::main:::: _toNestedTrigger Trigger Plant::Life::main:::: _toDangleTrigger State Plant::Life::main::Coincide _coincide -> Coincide -Region Plant::Life::main::Coincide::ca _ca -> ca (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) +Region Plant::Life::main::Coincide::ca _ca -> ca (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Coincide::ca:: _caOwn (written as the entry of the region) Transition Plant::Life::main::Coincide::ca:: _caOwnT State Plant::Life::main::Coincide::ca::ca1 _ca1 -> ca1 -Region Plant::Life::main::Coincide::cb _cb -> cb (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) +Region Plant::Life::main::Coincide::cb _cb -> cb (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Coincide::cb:: _cbInit (written as the entry of the region) Transition Plant::Life::main::Coincide::cb:: _cbInitT State Plant::Life::main::Coincide::cb::cb1 _cb1 -> cb1 State Plant::Life::main::Conflict _conflict -> Conflict -Region Plant::Life::main::Conflict::xa _xa -> xa (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) +Region Plant::Life::main::Conflict::xa _xa -> xa (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Conflict::xa:: _xaOwn (written as the entry of the region) Transition Plant::Life::main::Conflict::xa:: _xaOwnT State Plant::Life::main::Conflict::xa::xa1 _xa1 -> xa1 -Region Plant::Life::main::Conflict::xb _xb -> xb (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) +Region Plant::Life::main::Conflict::xb _xb -> xb (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Conflict::xb:: _xbInit (written as the entry of the region) Transition Plant::Life::main::Conflict::xb:: _xbInitT State Plant::Life::main::Conflict::xb::xb1 _xb1 -> xb1 State Plant::Life::main::Conflict::xb::xb2 _xb2 -> xb2 State Plant::Life::main::Dangle _dangle -> Dangle -Region Plant::Life::main::Dangle::ga _ga -> ga (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) +Region Plant::Life::main::Dangle::ga _ga -> ga (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Dangle::ga:: _gaOwn (written as the entry of the region) Transition Plant::Life::main::Dangle::ga:: _gaOwnT State Plant::Life::main::Dangle::ga::ga1 _ga1 -> ga1 -Region Plant::Life::main::Dangle::gb _gb -> gb (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) +Region Plant::Life::main::Dangle::gb _gb -> gb (an orthogonal region is written as a sub-state of the parallel state regions) State Plant::Life::main::Dangle::gb::gb1 _gb1 -> gb1 State Plant::Life::main::Donate _donate -> Donate -Region Plant::Life::main::Donate::da _da -> da (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) +Region Plant::Life::main::Donate::da _da -> da (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Donate::da:: _daOwn (written as the entry of the region) Transition Plant::Life::main::Donate::da:: _daOwnT State Plant::Life::main::Donate::da::da1 _da1 -> da1 -Region Plant::Life::main::Donate::db _db -> db (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) +Region Plant::Life::main::Donate::db _db -> db (an orthogonal region is written as a sub-state of the parallel state regions) State Plant::Life::main::Donate::db::db1 _db1 -> db1 State Plant::Life::main::Duplicate _duplicate -> Duplicate -Region Plant::Life::main::Duplicate::ua _ua (the one region is written as the body of its owner) +Region Plant::Life::main::Duplicate::ua _ua -> ua (the region is written as the sub-state ua of the parallel state Duplicate, as a composite state's regions are) Pseudostate Plant::Life::main::Duplicate::ua:: _uaInit1 (written as the entry of the region) Transition Plant::Life::main::Duplicate::ua:: _uaInit1T State Plant::Life::main::Duplicate::ua::ua1 _ua1 -> ua1 State Plant::Life::main::Duplicate::ua::ua2 _ua2 -> ua2 State Plant::Life::main::Nested _nested -> Nested -Region Plant::Life::main::Nested::na _na -> na (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) +Region Plant::Life::main::Nested::na _na -> na (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Nested::na:: _naOwn (written as the entry of the region) Transition Plant::Life::main::Nested::na:: _naOwnT State Plant::Life::main::Nested::na::na1 _na1 -> na1 -Region Plant::Life::main::Nested::nb _nb -> nb (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) +Region Plant::Life::main::Nested::nb _nb -> nb (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Nested::nb:: _nbInit (written as the entry of the region) Transition Plant::Life::main::Nested::nb:: _nbInitT State Plant::Life::main::Nested::nb::Inner _inner -> Inner -Region Plant::Life::main::Nested::nb::Inner::ni _ni (the one region is written as the body of its owner) +Region Plant::Life::main::Nested::nb::Inner::ni _ni -> ni (the region is written as the sub-state ni of the parallel state Inner, as a composite state's regions are) State Plant::Life::main::Nested::nb::Inner::ni::inner1 _inner1 -> inner1 «Block» Class Site _site -> Site StateMachine Site::Cross _cross -> Site::Cross @@ -93,11 +93,11 @@ Region Site::Cross::main _crossMain (the one region is written as the body of it Pseudostate Site::Cross::main:: _crossInit (written as the entry of the region) Transition Site::Cross::main:: _crossInitT State Site::Cross::main::Idle _idle -> Idle -Region Site::Cross::main::Idle::ir _ir (the one region is written as the body of its owner) +Region Site::Cross::main::Idle::ir _ir -> ir (the region is written as the sub-state ir of the parallel state Idle, as a composite state's regions are) Pseudostate Site::Cross::main::Idle::ir:: _iInit (written as the entry of the region) Transition Site::Cross::main::Idle::ir:: _iInitT State Site::Cross::main::Idle::ir::idle1 _idle1 -> idle1 State Site::Cross::main::Work _work -> Work -Region Site::Cross::main::Work::wr _wr (the one region is written as the body of its owner) +Region Site::Cross::main::Work::wr _wr -> wr (the region is written as the sub-state wr of the parallel state Work, as a composite state's regions are) State Site::Cross::main::Work::wr::work1 _work1 -> work1 SignalEvent nextEvent _nextEvent (written where a trigger refers to it, as accept Next) diff --git a/tests/migrate/testdata/xmi/orthogonal_initials.golden.sysml b/tests/migrate/testdata/xmi/orthogonal_initials.golden.sysml index 515259361c..0f3eda52c5 100644 --- a/tests/migrate/testdata/xmi/orthogonal_initials.golden.sysml +++ b/tests/migrate/testdata/xmi/orthogonal_initials.golden.sysml @@ -50,11 +50,13 @@ part def Plant { transition first regions then done; metadata MigrationMetadata::SynthesizedName about regions; } - state Duplicate { - /* not migrated: Pseudostate (_uaInit2) — a region has one initial pseudostate; (_uaInit1) is written as it */ - entry; then ua1; - state ua1; - state ua2; + state Duplicate parallel { + state ua { + /* not migrated: Pseudostate (_uaInit2) — a region has one initial pseudostate; (_uaInit1) is written as it */ + entry; then ua1; + state ua1; + state ua2; + } } state Nested { entry; then regions; @@ -65,9 +67,11 @@ part def Plant { } state nb { entry; then Inner; - state Inner { - entry; then inner1; - state inner1; + state Inner parallel { + state ni { + entry; then inner1; + state inner1; + } } } } @@ -100,15 +104,19 @@ part def Plant { part def Site { state def Cross { entry; then Idle; - state Work { - /* not migrated: Pseudostate (_wInit) — the initial transition's target has no v2 form here */ - /* not migrated: Transition (_wInitT) — its target 'idle1' lies in 'ir', a region of 'Idle' and no orthogonal region of the pseudostate's; an initial transition enters its own region */ - /* every initial pseudostate of the region enters another region: nothing enters this one */ - state work1; + state Work parallel { + state wr { + /* not migrated: Pseudostate (_wInit) — the initial transition's target has no v2 form here */ + /* not migrated: Transition (_wInitT) — its target 'idle1' lies in 'ir', a region of 'Idle' and no orthogonal region of the pseudostate's; an initial transition enters its own region */ + /* every initial pseudostate of the region enters another region: nothing enters this one */ + state work1; + } } - state Idle { - entry; then idle1; - state idle1; + state Idle parallel { + state ir { + entry; then idle1; + state idle1; + } } } } diff --git a/tests/migrate/testdata/xmi/plant_states.golden.report.txt b/tests/migrate/testdata/xmi/plant_states.golden.report.txt index 525768f41d..7e7a6e46f7 100644 --- a/tests/migrate/testdata/xmi/plant_states.golden.report.txt +++ b/tests/migrate/testdata/xmi/plant_states.golden.report.txt @@ -8,7 +8,7 @@ TimeEvent never _never (the time event's time is not written: the instant "next ## approximated (6) StateMachine Plant::Line _line -> Plant::Line (acts on its owner Plant, which it takes as its parameter context, since its this is the def's own occurrence; the classifier behavior is run by every object of Plant as its usage line) -Transition Plant::Line::main:: _tBump (the target 'Prep' lies in another region and is named by its path Work.Prep; a local transition is written external: the composite state Work exits and re-enters where v1 stayed in it, running its exit and entry behaviors) +Transition Plant::Line::main:: _tBump (the target 'Prep' lies in another region and is named by its path Work.steps.Prep; a local transition is written external: the composite state Work exits and re-enters where v1 stayed in it, running its exit and entry behaviors) Trigger Plant::Line::main:::: _trDawn (the absolute time is an instant on the simulation clock, which starts at 0.0 [SI::s]) OpaqueBehavior Plant::Line::main::::log _resumeLog -> Plant::Line::unnamed::log (the JavaScript body is translated to v2; the console print println(…) is left out, as it writes to the tool's console and changes nothing of the model; the JavaScript body is written as v2 assignments) OpaqueBehavior Plant::Line::main::Work::tally _workExit -> Plant::Line::Work::tally (the JavaScript body is translated to v2; the JavaScript body is written as v2 assignments) @@ -53,10 +53,10 @@ FinalState Plant::Line::main:: _fin -> done Pseudostate Plant::Line::main:: _init0 (written as the entry of the region) Transition Plant::Line::main:: _t0 Transition Plant::Line::main:: _tGo -Transition Plant::Line::main:: _tStop (the source 'Run' lies in another region and is named by its path Work.Run) -Transition Plant::Line::main:: _tDirect (the target 'Run' lies in another region and is named by its path Work.Run) +Transition Plant::Line::main:: _tStop (the source 'Run' lies in another region and is named by its path Work.steps.Run) +Transition Plant::Line::main:: _tDirect (the target 'Run' lies in another region and is named by its path Work.steps.Run) Transition Plant::Line::main:: _tHold -Transition Plant::Line::main:: _tResume (the target 'last' lies in another region and is named by its path Work.last) +Transition Plant::Line::main:: _tResume (the target 'last' lies in another region and is named by its path Work.steps.last) Transition Plant::Line::main:: _tEnter Transition Plant::Line::main:: _tLeave Transition Plant::Line::main:: _tRoute @@ -83,12 +83,12 @@ Trigger Plant::Line::main:::: _trSplit Trigger Plant::Line::main:::: _trPing Trigger Plant::Line::main:::: _trBump State Plant::Line::main::Both _both -> Both -Region Plant::Line::main::Both::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) +Region Plant::Line::main::Both::a _ra -> a (an orthogonal region is written as a sub-state of the parallel state regions) Transition Plant::Line::main::Both::a:: _tA Trigger Plant::Line::main::Both::a:::: _trA State Plant::Line::main::Both::a::A1 _a1 -> A1 State Plant::Line::main::Both::a::A2 _a2 -> A2 -Region Plant::Line::main::Both::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) +Region Plant::Line::main::Both::b _rb -> b (an orthogonal region is written as a sub-state of the parallel state regions) Transition Plant::Line::main::Both::b:: _tB Trigger Plant::Line::main::Both::b:::: _trB State Plant::Line::main::Both::b::B1 _b1 -> B1 @@ -99,7 +99,7 @@ ConnectionPointReference Plant::Line::main::Cell:: _cp State Plant::Line::main::Idle _idle -> Idle State Plant::Line::main::Pause _pause -> Pause State Plant::Line::main::Work _work -> Work -Region Plant::Line::main::Work::steps _rw (the one region is written as the body of its owner) +Region Plant::Line::main::Work::steps _rw -> steps (the region is written as the sub-state steps of the parallel state Work, as a composite state's regions are) Pseudostate Plant::Line::main::Work::steps:: _initW (written as the entry of the region) Transition Plant::Line::main::Work::steps:: _tWi Transition Plant::Line::main::Work::steps:: _tNext diff --git a/tests/migrate/testdata/xmi/plant_states.golden.sysml b/tests/migrate/testdata/xmi/plant_states.golden.sysml index 13259188fc..bed64ac13d 100644 --- a/tests/migrate/testdata/xmi/plant_states.golden.sysml +++ b/tests/migrate/testdata/xmi/plant_states.golden.sysml @@ -17,18 +17,20 @@ part def Plant { attribute dawn : Time::TimeInstantValue = 21600.0 [SI::s]; entry; then Idle; state Idle; - state Work { - entry; then Prep; + state Work parallel { exit action tally { rep language "JavaScript" /* count = count + 1; */ assign context.count := context.count + 1; } - state Prep; - state Run; - #StateMachines::shallowHistory state last; - #StateMachines::deepHistory state deepest; - transition first Prep accept Next then Run; - transition first last then Prep; + state steps { + entry; then Prep; + state Prep; + state Run; + #StateMachines::shallowHistory state last; + #StateMachines::deepHistory state deepest; + transition first Prep accept Next then Run; + transition first last then Prep; + } } state Pause; state Cell : CellMachine { in ref :>> context = Line::context; } @@ -36,7 +38,7 @@ part def Plant { fork spread; join gather; state Both { - /* no default entry: the regions 'a', 'b' have no initial pseudostate, so only a fork or a transition naming a nested state enters the regions */ + entry; then regions; state regions parallel { state a { /* the region has no initial pseudostate: nothing enters it */ @@ -55,14 +57,14 @@ part def Plant { metadata MigrationMetadata::SynthesizedName about regions; } transition first Idle accept Go then Work; - transition first Work.Run accept Stop then Idle; - transition first Idle accept Resume then Work.Run; + transition first Work.steps.Run accept Stop then Idle; + transition first Idle accept Resume then Work.steps.Run; transition first Work accept Hold then Pause; transition first Pause accept Resume do action log { rep language "JavaScript" /* println("resuming at " + count); */ } - then Work.last; + then Work.steps.last; transition first Idle accept Enter then Cell.warmStart; transition first Cell.spent then Idle; transition first Idle accept Route then route; @@ -75,7 +77,7 @@ part def Plant { transition first Both.regions.b.B2 then gather; transition first gather then Idle; transition first Idle accept Ping then Idle; - transition first Work accept Bump then Work.Prep; + transition first Work accept Bump then Work.steps.Prep; transition first Pause accept at dawn then Idle; /* transition (_tNever) from Pause to done not migrated — a trigger is dropped: the time event's time is not written: the instant "next Tuesday" is neither a number with a time unit nor an expression: not v2 expression syntax */ } diff --git a/tests/migrate/testdata/xmi/station_points.golden.report.txt b/tests/migrate/testdata/xmi/station_points.golden.report.txt index 048637ae07..2e9f3922b1 100644 --- a/tests/migrate/testdata/xmi/station_points.golden.report.txt +++ b/tests/migrate/testdata/xmi/station_points.golden.report.txt @@ -25,15 +25,15 @@ OpaqueBehavior Station::Cycle::main::Sync::b::B1:: _b1Entry -> S 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) +OpaqueBehavior Station::Cycle::main::Work::steps:::: _startEff -> Station::Cycle::Work::steps::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::steps::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::steps::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::steps::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::steps::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::steps::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::steps::Run::pace::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::steps::Run::pace::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::steps::Run::pace::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) @@ -62,12 +62,12 @@ 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:: _tGo (the target 'Start' lies in another region and is named by its path Work.steps.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:: _tDive (the target 'Deep' lies in another region and is named by its path Work.steps.Run.pace.Deep) +Transition Station::Cycle::main:: _tResume (the target 'H' lies in another region and is named by its path Work.steps.H) +Transition Station::Cycle::main:: _tLeave (the source 'Leave' lies in another region and is named by its path Work.steps.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) @@ -85,7 +85,7 @@ 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) +Region Station::Cycle::main::Sync::a _ra -> a (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:: _tA @@ -95,7 +95,7 @@ Constraint Station::Cycle::main::Sync::a:::: _bothAGuard 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) +Region Station::Cycle::main::Sync::b _rb -> b (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:: _tB @@ -108,13 +108,13 @@ State Station::Cycle::main::Work _work -> Work Pseudostate Station::Cycle::main::Work::Leave _leave -> Leave (written as a `#StateMachines::junction state` 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 `#StateMachines::junction state` 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) +Region Station::Cycle::main::Work::steps _steps -> steps (the region is written as the sub-state steps of the parallel state Work, as a composite state's regions are) 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 Start) -Transition Station::Cycle::main::Work::steps:: _tFinish (the target 'Leave' lies in another region and is named by its path Leave) -Transition Station::Cycle::main::Work::steps:: _tOut (the source 'Out' lies in another region and is named by its path Run.Out) +Transition Station::Cycle::main::Work::steps:: _tStart +Transition Station::Cycle::main::Work::steps:: _tFinish +Transition Station::Cycle::main::Work::steps:: _tOut (the source 'Out' lies in another region and is named by its path steps.Run.pace.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 `#StateMachines::shallowHistory state` shallow history, which re-enters the substate active when its state was last left) @@ -122,11 +122,11 @@ 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 `#StateMachines::junction state` 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 `#StateMachines::junction state` 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) +Region Station::Cycle::main::Work::steps::Run::pace _pace -> pace (the region is written as the sub-state pace of the parallel state Run, as a composite state's regions are) 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 Deep) -Transition Station::Cycle::main::Work::steps::Run::pace:: _tBack (the target 'Out' lies in another region and is named by its path Out) +Transition Station::Cycle::main::Work::steps::Run::pace:: _tDeep +Transition Station::Cycle::main::Work::steps::Run::pace:: _tBack 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 diff --git a/tests/migrate/testdata/xmi/station_points.golden.sysml b/tests/migrate/testdata/xmi/station_points.golden.sysml index 9d382ea371..7cc875c25a 100644 --- a/tests/migrate/testdata/xmi/station_points.golden.sysml +++ b/tests/migrate/testdata/xmi/station_points.golden.sysml @@ -16,78 +16,82 @@ part def Station { in ref context : Station[1]; entry; then Idle; state Idle; - state Work { + state Work parallel { entry action { rep language "JavaScript" /* trace = trace * 100 + 11; */ assign context.trace := context.trace * 100 + 11; } - then Prep; exit action { rep language "JavaScript" /* trace = trace * 100 + 12; */ assign context.trace := context.trace * 100 + 12; } - #StateMachines::junction state 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 */ - #StateMachines::junction state Leave; - state Prep { - entry action { - rep language "JavaScript" /* trace = trace * 100 + 13; */ - assign context.trace := context.trace * 100 + 13; - } - } - state Run { - entry action { - rep language "JavaScript" /* trace = trace * 100 + 14; */ - assign context.trace := context.trace * 100 + 14; - } - then Slow; - exit action { - rep language "JavaScript" /* trace = trace * 100 + 15; */ - assign context.trace := context.trace * 100 + 15; + state steps { + #StateMachines::junction state 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 */ + #StateMachines::junction state Leave; + entry; then Prep; + state Prep { + entry action { + rep language "JavaScript" /* trace = trace * 100 + 13; */ + assign context.trace := context.trace * 100 + 13; + } } - #StateMachines::junction state Deep; - #StateMachines::junction state Out; - state Slow; - state Fast { + state Run parallel { entry action { - rep language "JavaScript" /* trace = trace * 100 + 17; */ - assign context.trace := context.trace * 100 + 17; + rep language "JavaScript" /* trace = trace * 100 + 14; */ + assign context.trace := context.trace * 100 + 14; + } + exit action { + rep language "JavaScript" /* trace = trace * 100 + 15; */ + assign context.trace := context.trace * 100 + 15; + } + state pace { + #StateMachines::junction state Deep; + #StateMachines::junction state Out; + entry; then Slow; + state Slow; + state Fast { + entry action { + rep language "JavaScript" /* trace = trace * 100 + 17; */ + assign context.trace := context.trace * 100 + 17; + } + } + transition first Deep + do action { + rep language "JavaScript" /* trace = trace * 100 + 21; */ + assign context.trace := context.trace * 100 + 21; + } + then Fast; + transition first Fast accept Back + do action { + rep language "JavaScript" /* trace = trace * 100 + 22; */ + assign context.trace := context.trace * 100 + 22; + } + then Out; } } - transition first Deep + #StateMachines::shallowHistory state H; + transition first Prep accept Next then Run; + transition first Start do action { - rep language "JavaScript" /* trace = trace * 100 + 21; */ - assign context.trace := context.trace * 100 + 21; + rep language "JavaScript" /* trace = trace * 100 + 23; */ + assign context.trace := context.trace * 100 + 23; } - then Fast; - transition first Fast accept Back + 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 { - rep language "JavaScript" /* trace = trace * 100 + 22; */ - assign context.trace := context.trace * 100 + 22; + rep language "JavaScript" /* trace = trace * 100 + 24; */ + assign context.trace := context.trace * 100 + 24; } - then Out; + then Leave; + transition first steps.Run.pace.Out + do action { + rep language "JavaScript" /* trace = trace * 100 + 25; */ + assign context.trace := context.trace * 100 + 25; + } + then Prep; } - #StateMachines::shallowHistory state H; - transition first Prep accept Next then Run; - transition first Start - do action { - rep language "JavaScript" /* trace = trace * 100 + 23; */ - assign context.trace := context.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 { - rep language "JavaScript" /* trace = trace * 100 + 24; */ - assign context.trace := context.trace * 100 + 24; - } - then Leave; - transition first Run.Out - do action { - rep language "JavaScript" /* trace = trace * 100 + 25; */ - assign context.trace := context.trace * 100 + 25; - } - then Prep; } state Sync { entry action { @@ -155,7 +159,7 @@ part def Station { rep language "JavaScript" /* trace = trace * 100 + 41; */ assign context.trace := context.trace * 100 + 41; } - then Work.Start; + then Work.steps.Start; transition first Idle accept Go if false then Work; transition first Idle accept Enter then Work; transition first Idle accept Dive @@ -163,10 +167,10 @@ part def Station { rep language "JavaScript" /* trace = trace * 100 + 42; */ assign context.trace := context.trace * 100 + 42; } - then Work.Run.Deep; - transition first Idle accept Resume then Work.H; + then Work.steps.Run.pace.Deep; + transition first Idle accept Resume then Work.steps.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 + transition first Work.steps.Leave do action { rep language "JavaScript" /* trace = trace * 100 + 43; */ assign context.trace := context.trace * 100 + 43; diff --git a/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt b/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt index 2092a56663..038983a511 100644 --- a/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt +++ b/tests/migrate/testdata/xmi/terminate_orthogonal.golden.report.txt @@ -8,14 +8,14 @@ StateMachine Machine::Run _run -> Machine::Run (the classifier behavior is run b SignalEvent _stopEvent (written where a trigger refers to it, as accept Stop) SignalEvent _pingEvent (written where a trigger refers to it, as accept Ping) «Block» Class Machine _machine -> Machine -Region Machine::Run::R1 _r1 -> R1 (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) +Region Machine::Run::R1 _r1 -> R1 (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Machine::Run::R1:: _r1Initial (written as the entry of the region) Pseudostate Machine::Run::R1:: _end -> terminated (written as a terminate action, which ends the state machine's performance in every region) Transition Machine::Run::R1:: _r1Start Transition Machine::Run::R1:: _stopTransition Trigger Machine::Run::R1:::: _stopTrigger State Machine::Run::R1::A _a -> A -Region Machine::Run::R2 _r2 -> R2 (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) +Region Machine::Run::R2 _r2 -> R2 (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Machine::Run::R2:: _r2Initial (written as the entry of the region) Transition Machine::Run::R2:: _r2Start Transition Machine::Run::R2:: _pingTransition diff --git a/tests/migrate/testdata/xmi/transition_relocation.golden.report.txt b/tests/migrate/testdata/xmi/transition_relocation.golden.report.txt index 0a3f2a4738..6f60233db3 100644 --- a/tests/migrate/testdata/xmi/transition_relocation.golden.report.txt +++ b/tests/migrate/testdata/xmi/transition_relocation.golden.report.txt @@ -13,21 +13,21 @@ Region Plant::Life::main _main (the one region is written as the body of its own Pseudostate Plant::Life::main:: _init (written as the entry of the region) Transition Plant::Life::main:: _enterWork State Plant::Life::main::Parallel _parallel -> Parallel -Region Plant::Life::main::Parallel::region3 _region3 -> region3 (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) +Region Plant::Life::main::Parallel::region3 _region3 -> region3 (an orthogonal region is written as a sub-state of the parallel state regions) Pseudostate Plant::Life::main::Parallel::region3:: _region3Init (written as the entry of the region) Transition Plant::Life::main::Parallel::region3:: _enterWait3 State Plant::Life::main::Parallel::region3::wait3 _wait3 -> wait3 -Region Plant::Life::main::Parallel::region4 _region4 -> region4 (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) +Region Plant::Life::main::Parallel::region4 _region4 -> region4 (an orthogonal region is written as a sub-state of the parallel state regions) State Plant::Life::main::Parked _parked -> Parked -Region Plant::Life::main::Parked::parked _parkedRegion (the one region is written as the body of its owner) +Region Plant::Life::main::Parked::parked _parkedRegion -> parked (the region is written as the sub-state parked of the parallel state Parked, as a composite state's regions are) Pseudostate Plant::Life::main::Parked::parked:: _parkedInit (written as the entry of the region) Transition Plant::Life::main::Parked::parked:: _enterReady State Plant::Life::main::Parked::parked::Ready _ready -> Ready State Plant::Life::main::Work _work -> Work -Region Plant::Life::main::Work::work _workRegion (the one region is written as the body of its owner) +Region Plant::Life::main::Work::work _workRegion -> work (the region is written as the sub-state work of the parallel state Work, as a composite state's regions are) Pseudostate Plant::Life::main::Work::work:: _workInit (written as the entry of the region) Transition Plant::Life::main::Work::work:: _enterRun -Transition Plant::Life::main::Work::work:: _leaveRun (the source 'Run' lies in another region and is named by its path Work.Run; the target 'Ready' lies in another region and is named by its path Parked.Ready) +Transition Plant::Life::main::Work::work:: _leaveRun (the source 'Run' lies in another region and is named by its path Work.work.Run; the target 'Ready' lies in another region and is named by its path Parked.parked.Ready) Trigger Plant::Life::main::Work::work:::: _stopTrigger State Plant::Life::main::Work::work::Run _run -> Run Signal Stop _stop -> Stop diff --git a/tests/migrate/testdata/xmi/transition_relocation.golden.sysml b/tests/migrate/testdata/xmi/transition_relocation.golden.sysml index a9145e55f8..21fcbb7b30 100644 --- a/tests/migrate/testdata/xmi/transition_relocation.golden.sysml +++ b/tests/migrate/testdata/xmi/transition_relocation.golden.sysml @@ -2,16 +2,20 @@ item def Stop; part def Plant { state def Life { entry; then Work; - state Work { - entry; then Run; - state Run; + state Work parallel { + state work { + entry; then Run; + state Run; + } } - state Parked { - entry; then Ready; - state Ready; + state Parked parallel { + state parked { + entry; then Ready; + state Ready; + } } state Parallel { - /* no default entry: these regions have no written entry: 'region4'; only a fork or a transition naming a nested state enters them */ + entry; then regions; state regions parallel { state region3 { entry; then wait3; @@ -24,6 +28,6 @@ part def Plant { transition first regions then done; metadata MigrationMetadata::SynthesizedName about regions; } - transition first Work.Run accept Stop then Parked.Ready; + transition first Work.work.Run accept Stop then Parked.parked.Ready; } } From c04a40c86e375af79c5e57fa86840a03150648eb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:10:09 +0000 Subject: [PATCH 06/19] chore(census): regenerate migration inventories Co-Authored-By: jason.han --- .../census-states-events.changed.md | 3 + ...sml-v1-transformation-census-baseline.json | 400 +++++++++++------- .../project/sysml-v1-transformation-census.md | 207 +++++---- .../testdata/pssm_migration_expected.txt | 6 +- 4 files changed, 349 insertions(+), 267 deletions(-) create mode 100644 changes/unreleased/census-states-events.changed.md diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md new file mode 100644 index 0000000000..6e16fc5f4a --- /dev/null +++ b/changes/unreleased/census-states-events.changed.md @@ -0,0 +1,3 @@ +- **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; a region without an entry remains inactive until a transition or fork branch enters it, and blocks its owner's completion; terminate pseudostates now terminate the whole state machine. +- **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index 61988677cd..58a8292552 100644 --- a/docs/project/sysml-v1-transformation-census-baseline.json +++ b/docs/project/sysml-v1-transformation-census-baseline.json @@ -22,7 +22,7 @@ }, { "name": "fixtures", - "files": 93 + "files": 94 } ], "counts": { @@ -136,7 +136,7 @@ }, "uml:Class": { "pssm": 311, - "fixtures": 469 + "fixtures": 470 }, "uml:ClearAssociationAction": { "pssm": 0, @@ -248,7 +248,7 @@ }, "uml:Pseudostate": { "pssm": 289, - "fixtures": 71 + "fixtures": 74 }, "uml:RaiseExceptionAction": { "pssm": 0, @@ -296,7 +296,7 @@ }, "uml:Region": { "pssm": 248, - "fixtures": 54 + "fixtures": 56 }, "uml:RemoveStructuralFeatureValueAction": { "pssm": 0, @@ -6637,15 +6637,17 @@ "ownedRelationship" ], "ocl": "sha256:3f4ba616a89403b7434108f7d6e931f2731f96306a5f6a0966acaa645618a884", - "status": "approximate", + "status": "faithful", "implementation": [ - "internal/translate/migrate/activity.go:opaqueAction" + "internal/translate/migrate/activity.go:opaqueAction", + "internal/translate/migrate/behavior.go:textualReps", + "internal/translate/migrate/values.go:opaqueBodyPairs" ], "tests": [ - "tests/migrate/stub_actions_test.go:TestStubActionsInMethodsKeepTheirPinsAndAllocation", + "tests/migrate/opaque_textual_rep_test.go:TestOpaqueActionTextualRepresentations", "tests/migrate/opaque_migration_test.go:TestTranslatedOutputPinsFeedTheirFlows" ], - "reason": "the body is translated into action statements when its language is read; otherwise the action is a stub keeping its pins (fixtures: 78 of 109 approximated)", + "reason": "translated bodies retain executable action statements and each nonempty body/language pair is also written as a textual representation; a body without a language is kept as a comment", "scope": [ "uml:OpaqueAction" ] @@ -13988,15 +13990,19 @@ "ownedMemberParameter" ], "ocl": "sha256:b599b3105bd411023d03ef6f65ce83d30b10fbe15d78fd0a6e0f77bdaeb7e2e8", - "status": "approximate", + "status": "deliberate", "implementation": [ "internal/translate/migrate/behavior.go:event", - "internal/translate/migrate/states.go:triggerAccept" + "internal/translate/migrate/states.go:triggerAccept", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates" ], - "reason": "an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8)", + "reason": "time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped", "scope": [ "uml:TimeEvent", "uml:ChangeEvent", @@ -14018,15 +14024,19 @@ "direction" ], "ocl": "sha256:5fffed7c1a7497a40c4a426d15855b961fb30ed60601f01f05f85e95503d5b7e", - "status": "approximate", + "status": "deliberate", "implementation": [ "internal/translate/migrate/behavior.go:event", - "internal/translate/migrate/states.go:triggerAccept" + "internal/translate/migrate/states.go:triggerAccept", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates" ], - "reason": "an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8)", + "reason": "time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped", "scope": [ "uml:TimeEvent", "uml:ChangeEvent", @@ -14048,15 +14058,19 @@ "ownedRelationship" ], "ocl": "sha256:991063d5d273e68b510443110b1363d49b80f06a2e1e35553b4576b356931519", - "status": "approximate", + "status": "deliberate", "implementation": [ "internal/translate/migrate/behavior.go:event", - "internal/translate/migrate/states.go:triggerAccept" + "internal/translate/migrate/states.go:triggerAccept", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates" ], - "reason": "an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8)", + "reason": "time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped", "scope": [ "uml:TimeEvent", "uml:ChangeEvent", @@ -14680,16 +14694,17 @@ "ownedMemberElement" ], "ocl": "sha256:f16e0694037266ec5c66a075f58da7f8afe0456eef0e783bbd2230a936c80042", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/behavior.go:opaqueBehaviorBody", - "internal/translate/migrate/behavior.go:calcBody" + "internal/translate/migrate/behavior.go:calcBody", + "internal/translate/migrate/behavior.go:textualReps", + "internal/translate/migrate/values.go:opaqueBodyPairs" ], "tests": [ - "tests/migrate/opaque_migration_test.go:TestNonScalarFeaturesAndScriptLiterals", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/opaque_textual_rep_test.go:TestOpaqueBehaviorTextualRepresentations" ], - "reason": "an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated)", + "reason": "action and calc definitions retain translated executable statements and each nonempty body/language pair is written as a textual representation; a body without a language is kept as a comment", "scope": [ "uml:OpaqueBehavior" ] @@ -14710,16 +14725,17 @@ "language" ], "ocl": "sha256:e32dc2615a865f5c3529771060cf49d5ab93c3febd301780eeba603df6a268ad", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/behavior.go:opaqueBehaviorBody", - "internal/translate/migrate/behavior.go:calcBody" + "internal/translate/migrate/behavior.go:calcBody", + "internal/translate/migrate/behavior.go:textualReps", + "internal/translate/migrate/values.go:opaqueBodyPairs" ], "tests": [ - "tests/migrate/opaque_migration_test.go:TestNonScalarFeaturesAndScriptLiterals", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/opaque_textual_rep_test.go:TestOpaqueBehaviorTextualRepresentations" ], - "reason": "an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated)", + "reason": "candidate erratum: the mapping pairs body i with language i, so a body with no language is dropped; it is kept as a comment. Each nonempty pair is written as a textual representation, while translated bodies remain executable", "scope": [ "uml:OpaqueBehavior" ] @@ -14739,16 +14755,17 @@ "ownedRelationship" ], "ocl": "sha256:5b8bc31f50e3ce6c8471a11457f4bdb438f88a735e94fcb4fb26714b74ab952c", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/behavior.go:opaqueBehaviorBody", - "internal/translate/migrate/behavior.go:calcBody" + "internal/translate/migrate/behavior.go:calcBody", + "internal/translate/migrate/behavior.go:textualReps", + "internal/translate/migrate/values.go:opaqueBodyPairs" ], "tests": [ - "tests/migrate/opaque_migration_test.go:TestNonScalarFeaturesAndScriptLiterals", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/opaque_textual_rep_test.go:TestOpaqueBehaviorTextualRepresentations" ], - "reason": "an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated)", + "reason": "action and calc definitions retain translated executable statements and each nonempty body/language pair is written as a textual representation; a body without a language is kept as a comment", "scope": [ "uml:OpaqueBehavior" ] @@ -14819,15 +14836,19 @@ "ownedRelationship" ], "ocl": "sha256:2c49efe7744ba67b0a3b6c556c41408b6b79a3045399dfaa7f976d5a64ed7d0d", - "status": "approximate", + "status": "deliberate", "implementation": [ "internal/translate/migrate/behavior.go:event", - "internal/translate/migrate/states.go:triggerAccept" + "internal/translate/migrate/states.go:triggerAccept", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates" ], - "reason": "an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8)", + "reason": "time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped", "scope": [ "uml:TimeEvent", "uml:ChangeEvent", @@ -14849,16 +14870,18 @@ "ownedRelationship" ], "ocl": "sha256:ed59c3e997e910a41221d2ae7ad509599208e3269cf55778fd646296a20c3adc", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -14879,16 +14902,18 @@ "ownedRelationship" ], "ocl": "sha256:d717104fa2d018b0e7149d820c525abdbfc8d533e44bcdf0a50abb4d9520d187", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -14908,16 +14933,18 @@ "ownedMemberFeature" ], "ocl": "sha256:56c5e4333e81634810b0cb75449136029bb93769d1afd78bd0f00930a8200679", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -14937,16 +14964,18 @@ "chainingFeature" ], "ocl": "sha256:cea9c905816edf7ec2fa68e9b7f10d39156b10b681dfd9ed33d92c8963155211", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -14966,16 +14995,18 @@ "chainingFeature" ], "ocl": "sha256:1f201e33bb28ac3c3e474697a118d1d00e092f4dad268b83dc683568557d2795", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -14995,16 +15026,18 @@ "type" ], "ocl": "sha256:65cc81047f3797e5ca3fc0444e0cd064d1d7235ccd236ee77d95751c2b520668", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15024,16 +15057,18 @@ "value" ], "ocl": "sha256:ee431ee6fb4a4779777953496d73432438011b0eedb9c84c1c5b2d3a4b01a8d9", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15053,16 +15088,18 @@ "ownedRelationship" ], "ocl": "sha256:1cb76b92a56e4f8a0b138ddbbd0642e3ab6983c638c40f2a86559d21126d5d10", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15082,16 +15119,18 @@ "ownedRelationship" ], "ocl": "sha256:e0a0d993afdc09f0e438f8d2ea07d0c67458e4df37ee1e98188d447319d267f3", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15111,16 +15150,18 @@ "ownedMemberParameter" ], "ocl": "sha256:0c6e80744f1ca172c940f22b094e2364bc6d69582a3b9a15144ce5745c65b318", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15140,16 +15181,18 @@ "ownedMemberFeature" ], "ocl": "sha256:c7df47fe2a9fa31107d518e24f01829668c676e6665e1da8b5ce0e6d06011984", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15169,16 +15212,18 @@ "type" ], "ocl": "sha256:99f71aa12ed3dcdd79e31f0037f3245c739c6fe39fa433b88ff6e1399bef9af2", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15198,16 +15243,18 @@ "value" ], "ocl": "sha256:e3cb5fd1edea70fa17ec22c62fc20a65c3771e9518391c0e70e197fe5dd315c3", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15227,16 +15274,18 @@ "ownedRelationship" ], "ocl": "sha256:9db5c3e4b5135c8927c6b963e3afd27d821f72ee478bbabec299d65383e0c0ad", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15257,16 +15306,18 @@ "ownedRelationship" ], "ocl": "sha256:a17957f49053c192a111d4e6e4f61ad414db26e0d3a93c03bfb3f6c04376aee9", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15287,16 +15338,18 @@ "ownedRelatedElement" ], "ocl": "sha256:270fcbe6db145562d533b5be7d37ffc91751ff4cda63310d7cfeb2db2d600aef", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15317,16 +15370,18 @@ "direction" ], "ocl": "sha256:572fc9fa7d11d4c2fe67b9718d79459b8008f7facd82aa998179820d92cf4c6e", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15346,16 +15401,18 @@ "ownedMemberFeature" ], "ocl": "sha256:ef786324ce5947676f2470d703d799b0a8a77a2ecd31995da4eceef5dd030aea", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15375,16 +15432,18 @@ "ownedMemberParameter" ], "ocl": "sha256:dbdb6b7988362cf2e92b97f5a1db7d08b773bf55c27b2277cc5f450b65473841", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15404,16 +15463,18 @@ "ownedRelationship" ], "ocl": "sha256:53783e2be10546a837ddfa5615b62a941a65bda451b78c275a92019aabf3387d", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15433,16 +15494,18 @@ "referencedFeature" ], "ocl": "sha256:cbad9ec43d951dfc41eed13556de29f62bd195afc1763a6e3a2c8b03e3252f61", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -15463,16 +15526,18 @@ "ownedRelationship" ], "ocl": "sha256:cc4cf1080da81b630e3530b262eb0bc299aba09d95694f62d2d7c3dae91f8e70", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:triggerAccept", - "internal/translate/migrate/values.go:valueExpr" + "internal/translate/migrate/values.go:valueExpr", + "internal/translate/migrate/events.go:instantExpr" ], "tests": [ - "tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature", + "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", + "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards" ], - "reason": "a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events)", + "reason": "the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value", "scope": [ "uml:TimeEvent" ] @@ -18225,16 +18290,19 @@ "ownedRelationship" ], "ocl": "sha256:d0135ff9e57bd8d4894be3bbaaf71452667f8b6c799b6e5a6c7dfb8d98384598", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:pseudostateLine", - "internal/translate/migrate/states.go:vertex" + "internal/translate/migrate/states.go:vertex", + "internal/translate/migrate/states.go:vertexBase", + "internal/translate/migrate/states.go:target" ], "tests": [ "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/terminate_test.go:TestTerminatePseudostateEndsOrthogonalMachine", + "tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateConnectionPointsStayInRegion" ], - "reason": "choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5)", + "reason": "candidate erratum: the mapping writes every pseudostate other than an initial one as a plain StateUsage, dropping its kind: a choice or junction becomes a state that waits, a fork enters one target and a terminate ends nothing. Choice, junction, and history are written as `#StateMachines:: state`; forks and joins as `fork`/`join` nodes; terminate as `action terminate;`. A transition that crosses directly from an entry point to an exit point without settling in a state remains unmapped (one fixture case)", "scope": [ "uml:Pseudostate" ] @@ -18434,16 +18502,19 @@ "filter" ], "ocl": "sha256:abd3cb2bfdfe4cd3c1b36d93405651adfe4ca80da714267f65411cc45e153481", - "status": "approximate", + "status": "faithful", "implementation": [ "internal/translate/migrate/states.go:pseudostateLine", - "internal/translate/migrate/states.go:vertex" + "internal/translate/migrate/states.go:vertex", + "internal/translate/migrate/states.go:vertexBase", + "internal/translate/migrate/states.go:target" ], "tests": [ "tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates", - "tests/migrate/migrate_test.go:TestGoldenNotation" + "tests/migrate/terminate_test.go:TestTerminatePseudostateEndsOrthogonalMachine", + "tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateConnectionPointsStayInRegion" ], - "reason": "choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5)", + "reason": "candidate erratum: the mapping writes every pseudostate other than an initial one as a plain StateUsage, dropping its kind: a choice or junction becomes a state that waits, a fork enters one target and a terminate ends nothing. Choice, junction, and history are written as `#StateMachines:: state`; forks and joins as `fork`/`join` nodes; terminate as `action terminate;`. A transition that crosses directly from an entry point to an exit point without settling in a state remains unmapped (one fixture case)", "scope": [ "uml:Pseudostate" ] @@ -18463,15 +18534,24 @@ "ownedRelationship" ], "ocl": "sha256:b442f34f152532b9b29b28f5b1ecc2e0886e1f0a17f1377b578c52ceef9350af", - "status": "approximate", + "status": "faithful", "implementation": [ - "internal/translate/migrate/states.go:region" + "internal/translate/migrate/states.go:state", + "internal/translate/migrate/states.go:nameRegions", + "internal/translate/migrate/states.go:regions", + "internal/translate/migrate/states.go:regionWrittenAsState", + "internal/translate/migrate/states.go:region", + "internal/translate/migrate/names.go:path", + "internal/translate/migrate/edges.go:edgePlaceIn" ], "tests": [ + "tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateIsWrapped", + "tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateConnectionPointsStayInRegion", + "tests/migrate/region_wrapper_test.go:TestStateMachineSingleRegionRemainsFlattened", "tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards", "tests/migrate/empty_region_test.go:TestEmptyRegionsAreNotWritten" ], - "reason": "the region of a single-region state is flattened into its state rather than written as a state usage; orthogonal regions become the substates of a parallel state; empty regions are not written", + "reason": "candidate erratum: for a state machine's only Region the mapping writes a StateUsage in a state definition that is not parallel (`isParallel = region->size() > 1`) and nothing enters it, so it is never entered; its vertices become the state definition's own body. A StateMachine's single Region remains flattened; a composite State's single Region is written as a sub-state of its `parallel` state; orthogonal Regions are parallel sub-states; empty Regions are omitted because an empty parallel sub-state never completes", "scope": [ "uml:Region" ] diff --git a/docs/project/sysml-v1-transformation-census.md b/docs/project/sysml-v1-transformation-census.md index 294a34688e..40de862d17 100644 --- a/docs/project/sysml-v1-transformation-census.md +++ b/docs/project/sysml-v1-transformation-census.md @@ -34,7 +34,7 @@ that build one relationship of a target — share the verdict of the construct t -**Census:** 783 mapping classes — 311 ✅ faithful, 267 ⚠️ approximate, 141 ❌ not implemented, 64 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. +**Census:** 783 mapping classes — 340 ✅ faithful, 234 ⚠️ approximate, 141 ❌ not implemented, 68 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. | Package | Mappings | ✅ | ⚠️ | ❌ | ⛔ | 🚧 | ❔ | |---|---|---|---|---|---|---|---| @@ -47,7 +47,7 @@ that build one relationship of a target — share the verdict of the construct t | SysMLv1::Ports&Flows | 20 | 8 | 10 | 2 | 0 | 0 | 0 | | SysMLv1::Requirements | 49 | 34 | 10 | 0 | 5 | 0 | 0 | | UML4SysML::Actions::AcceptEventActions | 25 | 22 | 0 | 3 | 0 | 0 | 0 | -| UML4SysML::Actions::Actions | 8 | 7 | 1 | 0 | 0 | 0 | 0 | +| UML4SysML::Actions::Actions | 8 | 8 | 0 | 0 | 0 | 0 | 0 | | UML4SysML::Actions::InvocationActions | 40 | 2 | 34 | 4 | 0 | 0 | 0 | | UML4SysML::Actions::LinkActions | 7 | 0 | 0 | 7 | 0 | 0 | 0 | | UML4SysML::Actions::ObjectActions | 42 | 3 | 5 | 34 | 0 | 0 | 0 | @@ -57,17 +57,17 @@ that build one relationship of a target — share the verdict of the construct t | UML4SysML::Actions::VariableActions | 31 | 0 | 0 | 31 | 0 | 0 | 0 | | UML4SysML::Activities | 68 | 54 | 2 | 5 | 7 | 0 | 0 | | UML4SysML::Classification | 48 | 33 | 3 | 8 | 4 | 0 | 0 | -| UML4SysML::CommonBehavior | 57 | 28 | 29 | 0 | 0 | 0 | 0 | +| UML4SysML::CommonBehavior | 57 | 53 | 0 | 0 | 4 | 0 | 0 | | UML4SysML::CommonStructure | 18 | 14 | 4 | 0 | 0 | 0 | 0 | | UML4SysML::InformationFlows | 9 | 0 | 6 | 3 | 0 | 0 | 0 | | UML4SysML::Interactions | 19 | 6 | 0 | 0 | 13 | 0 | 0 | | UML4SysML::Packages | 34 | 7 | 9 | 9 | 9 | 0 | 0 | | UML4SysML::SimpleClassifiers | 22 | 19 | 1 | 0 | 2 | 0 | 0 | -| UML4SysML::StateMachines | 26 | 22 | 4 | 0 | 0 | 0 | 0 | +| UML4SysML::StateMachines | 26 | 25 | 1 | 0 | 0 | 0 | 0 | | UML4SysML::StructuredClassifiers | 39 | 12 | 26 | 1 | 0 | 0 | 0 | | UML4SysML::UseCases | 14 | 10 | 0 | 4 | 0 | 0 | 0 | | UML4SysML::Values | 33 | 10 | 7 | 0 | 16 | 0 | 0 | -| **Total** | 783 | 311 | 267 | 141 | 64 | 0 | 0 | +| **Total** | 783 | 340 | 234 | 141 | 68 | 0 | 0 | ## Census rows @@ -354,7 +354,7 @@ that build one relationship of a target — share the verdict of the construct t | `CommonAction_Mapping` (abstract) | `Action` | `ActionUsage` | `internal/translate/migrate/activity.go:declareKind` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `OABodyMembership_Mapping` | `OpaqueAction` | `OwningMembership` | `internal/translate/migrate/activity.go:declareKind` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `OABody_Mapping` | `OpaqueAction` | `TextualRepresentation` | `internal/translate/migrate/activity.go:declareKind` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `OpaqueAction_Mapping` | `OpaqueAction` | `ActionUsage` | `internal/translate/migrate/activity.go:opaqueAction` | `tests/migrate/stub_actions_test.go:TestStubActionsInMethodsKeepTheirPinsAndAllocation`
`tests/migrate/opaque_migration_test.go:TestTranslatedOutputPinsFeedTheirFlows` | ⚠️ approximate | the body is translated into action statements when its language is read; otherwise the action is a stub keeping its pins (fixtures: 78 of 109 approximated) | +| `OpaqueAction_Mapping` | `OpaqueAction` | `ActionUsage` | `internal/translate/migrate/activity.go:opaqueAction`
`internal/translate/migrate/behavior.go:textualReps`
`internal/translate/migrate/values.go:opaqueBodyPairs` | `tests/migrate/opaque_textual_rep_test.go:TestOpaqueActionTextualRepresentations`
`tests/migrate/opaque_migration_test.go:TestTranslatedOutputPinsFeedTheirFlows` | ✅ faithful | translated bodies retain executable action statements and each nonempty body/language pair is also written as a textual representation; a body without a language is kept as a comment | | `Pin_Mapping` | `Pin` | `ReferenceUsage` | `internal/translate/migrate/activity.go:declarePins` | `tests/migrate/arguments_test.go:TestAllocatedCallsWithoutBehaviorDeclareTheirPins`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `ValuePinFeatureValue_Mapping` | `ValuePin` | `FeatureValue` | `internal/translate/migrate/activity.go:declarePins`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `ValuePinUntyped_Mapping` | `ValuePin` | `ReferenceUsage` | `internal/translate/migrate/activity.go:declarePins`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | @@ -685,9 +685,9 @@ that build one relationship of a target — share the verdict of the construct t | OMG mapping | v1 source | v2 target | Implementation | Test | Status | Reason | |---|---|---|---|---|---|---| | `Behavior_Mapping` (abstract) | `Behavior` | `Behavior` | `internal/translate/migrate/behavior.go:behaviorBody`
`internal/translate/migrate/behavior.go:classifierBehavior` | `tests/migrate/behavior_more_test.go:TestClassifierBehaviorContextFollowsInputParameter`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `ChangeEventReturnParameterMembership_Mapping` | `ChangeEvent` | `ReturnParameterMembership` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | -| `ChangeEventReturnParameter_Mapping` | `ChangeEvent` | `ReferenceUsage` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | -| `ChangeEvent_Mapping` | `ChangeEvent` | `CalculationUsage` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | +| `ChangeEventReturnParameterMembership_Mapping` | `ChangeEvent` | `ReturnParameterMembership` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates` | ⛔ deliberate | time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped | +| `ChangeEventReturnParameter_Mapping` | `ChangeEvent` | `ReferenceUsage` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates` | ⛔ deliberate | time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped | +| `ChangeEvent_Mapping` | `ChangeEvent` | `CalculationUsage` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates` | ⛔ deliberate | time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped | | `ChangeTriggerBindingConnector_Mapping` | `Trigger` | `BindingConnectorAsUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `ChangeTriggerConstraintUsage_Mapping` | `Trigger` | `ConstraintUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `ChangeTriggerEndFeatureMembership_Mapping` | `Trigger` | `EndFeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | @@ -711,34 +711,34 @@ that build one relationship of a target — share the verdict of the construct t | `ChangeTriggerReturnParameter_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `ChangeTriggerReturnReferenceSubsetting_Mapping` | `Trigger` | `ReferenceSubsetting` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `ChangeTriggerReturnReferenceUsage_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `OpaqueBehaviorMembership_Mapping` | `OpaqueBehavior` | `OwningMembership` | `internal/translate/migrate/behavior.go:opaqueBehaviorBody`
`internal/translate/migrate/behavior.go:calcBody` | `tests/migrate/opaque_migration_test.go:TestNonScalarFeaturesAndScriptLiterals`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated) | -| `OpaqueBehaviorSpecification_Mapping` | `OpaqueBehavior` | `TextualRepresentation` | `internal/translate/migrate/behavior.go:opaqueBehaviorBody`
`internal/translate/migrate/behavior.go:calcBody` | `tests/migrate/opaque_migration_test.go:TestNonScalarFeaturesAndScriptLiterals`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated) | -| `OpaqueBehavior_Mapping` | `OpaqueBehavior` | `ActionDefinition` | `internal/translate/migrate/behavior.go:opaqueBehaviorBody`
`internal/translate/migrate/behavior.go:calcBody` | `tests/migrate/opaque_migration_test.go:TestNonScalarFeaturesAndScriptLiterals`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated) | +| `OpaqueBehaviorMembership_Mapping` | `OpaqueBehavior` | `OwningMembership` | `internal/translate/migrate/behavior.go:opaqueBehaviorBody`
`internal/translate/migrate/behavior.go:calcBody`
`internal/translate/migrate/behavior.go:textualReps`
`internal/translate/migrate/values.go:opaqueBodyPairs` | `tests/migrate/opaque_textual_rep_test.go:TestOpaqueBehaviorTextualRepresentations` | ✅ faithful | action and calc definitions retain translated executable statements and each nonempty body/language pair is written as a textual representation; a body without a language is kept as a comment | +| `OpaqueBehaviorSpecification_Mapping` | `OpaqueBehavior` | `TextualRepresentation` | `internal/translate/migrate/behavior.go:opaqueBehaviorBody`
`internal/translate/migrate/behavior.go:calcBody`
`internal/translate/migrate/behavior.go:textualReps`
`internal/translate/migrate/values.go:opaqueBodyPairs` | `tests/migrate/opaque_textual_rep_test.go:TestOpaqueBehaviorTextualRepresentations` | ✅ faithful | candidate erratum: the mapping pairs body i with language i, so a body with no language is dropped; it is kept as a comment. Each nonempty pair is written as a textual representation, while translated bodies remain executable | +| `OpaqueBehavior_Mapping` | `OpaqueBehavior` | `ActionDefinition` | `internal/translate/migrate/behavior.go:opaqueBehaviorBody`
`internal/translate/migrate/behavior.go:calcBody`
`internal/translate/migrate/behavior.go:textualReps`
`internal/translate/migrate/values.go:opaqueBodyPairs` | `tests/migrate/opaque_textual_rep_test.go:TestOpaqueBehaviorTextualRepresentations` | ✅ faithful | action and calc definitions retain translated executable statements and each nonempty body/language pair is written as a textual representation; a body without a language is kept as a comment | | `SignalTriggerReferenceUsageFeatureTyping_Mapping` | `Trigger` | `FeatureTyping` | `internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `SignalTriggerReferenceUsage_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `TimeEvent_Mapping` | `TimeEvent` | `CalculationUsage` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | -| `TimeTriggerBindingConnector_Mapping` | `Trigger` | `BindingConnectorAsUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerCalculationUsage_Mapping` | `Trigger` | `CalculationUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerEndFeatureMembership_Mapping` | `Trigger` | `EndFeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerEventChainingFeature_Mapping` | `Trigger` | `FeatureChaining` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerEventReturnParameterChainingFeature_Mapping` | `Trigger` | `FeatureChaining` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerExpressionFeatureTyping_Mapping` | `Trigger` | `FeatureTyping` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerExpressionFeatureValue_Mapping` | `Trigger` | `FeatureValue` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerExpressionFeature_Mapping` | `Trigger` | `Feature` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerExpressionInvocationExpression_Mapping` | `Trigger` | `InvocationExpression` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerExpressionParameterMembership_Mapping` | `Trigger` | `ParameterMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerFeatureMembership_Mapping` | `Trigger` | `FeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerFeatureTyping_Mapping` | `Trigger` | `FeatureTyping` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerFeatureValue_Mapping` | `Trigger` | `FeatureValue` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerFeature_Mapping` | `Trigger` | `Feature` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerInvocationExpression_Mapping` | `Trigger` | `TriggerInvocationExpression` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReferenceSubsetting_Mapping` | `Trigger` | `ReferenceSubsetting` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReferenceUsage_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReturnEndFeatureMembership_Mapping` | `Trigger` | `EndFeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReturnParameterMembership_Mapping` | `Trigger` | `ReturnParameterMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReturnParameter_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReturnReferenceSubsetting_Mapping` | `Trigger` | `ReferenceSubsetting` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| `TimeTriggerReturnReferenceUsage_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/duration_units_test.go:TestDurationWithNoUnitIsInMilliseconds`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | +| `TimeEvent_Mapping` | `TimeEvent` | `CalculationUsage` | `internal/translate/migrate/behavior.go:event`
`internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates` | ⛔ deliberate | time and change events are written inline at each referring trigger as `accept after`, `accept at`, or `accept when`, in the trigger's scope; a standalone package-owned calc cannot resolve contextual features (2 of 16 fixture TimeEvents read `tcs.ditSetup`). An event no trigger refers to is skipped; CallEvents remain unmapped | +| `TimeTriggerBindingConnector_Mapping` | `Trigger` | `BindingConnectorAsUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerCalculationUsage_Mapping` | `Trigger` | `CalculationUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerEndFeatureMembership_Mapping` | `Trigger` | `EndFeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerEventChainingFeature_Mapping` | `Trigger` | `FeatureChaining` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerEventReturnParameterChainingFeature_Mapping` | `Trigger` | `FeatureChaining` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerExpressionFeatureTyping_Mapping` | `Trigger` | `FeatureTyping` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerExpressionFeatureValue_Mapping` | `Trigger` | `FeatureValue` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerExpressionFeature_Mapping` | `Trigger` | `Feature` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerExpressionInvocationExpression_Mapping` | `Trigger` | `InvocationExpression` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerExpressionParameterMembership_Mapping` | `Trigger` | `ParameterMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerFeatureMembership_Mapping` | `Trigger` | `FeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerFeatureTyping_Mapping` | `Trigger` | `FeatureTyping` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerFeatureValue_Mapping` | `Trigger` | `FeatureValue` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerFeature_Mapping` | `Trigger` | `Feature` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerInvocationExpression_Mapping` | `Trigger` | `TriggerInvocationExpression` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReferenceSubsetting_Mapping` | `Trigger` | `ReferenceSubsetting` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReferenceUsage_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReturnEndFeatureMembership_Mapping` | `Trigger` | `EndFeatureMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReturnParameterMembership_Mapping` | `Trigger` | `ReturnParameterMembership` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReturnParameter_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReturnReferenceSubsetting_Mapping` | `Trigger` | `ReferenceSubsetting` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | +| `TimeTriggerReturnReferenceUsage_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr`
`internal/translate/migrate/events.go:instantExpr` | `tests/migrate/context_test.go:TestRelativeAcceptEventReadsOwnerFeature`
`tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards` | ✅ faithful | the trigger payload reference usage typed `DurationValue` or `TimeInstantValue` is written with its after/at invocation as `accept after ` or `accept at `; a time event whose `when` is neither an expression nor a number with a time unit remains unmapped because v1 supplies no value | | `TriggerParameterMembership_Mapping` | `Trigger` | `ParameterMembership` | `internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `Trigger_Mapping` | `Trigger` | `AcceptActionUsage` | `internal/translate/migrate/states.go:triggerAccept` | `tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | @@ -874,7 +874,7 @@ that build one relationship of a target — share the verdict of the construct t | OMG mapping | v1 source | v2 target | Implementation | Test | Status | Reason | |---|---|---|---|---|---|---| | `ChangeTriggerReferenceUsage2_Mapping` | `Trigger` | `ReferenceUsage` | `internal/translate/migrate/states.go:triggerAccept`
`internal/translate/migrate/values.go:valueExpr` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `CommonPseudostate_Mapping` (abstract) | `Pseudostate` | `Namespace` | `internal/translate/migrate/states.go:pseudostateLine`
`internal/translate/migrate/states.go:vertex` | `tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5) | +| `CommonPseudostate_Mapping` (abstract) | `Pseudostate` | `Namespace` | `internal/translate/migrate/states.go:pseudostateLine`
`internal/translate/migrate/states.go:vertex`
`internal/translate/migrate/states.go:vertexBase`
`internal/translate/migrate/states.go:target` | `tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/terminate_test.go:TestTerminatePseudostateEndsOrthogonalMachine`
`tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateConnectionPointsStayInRegion` | ✅ faithful | candidate erratum: the mapping writes every pseudostate other than an initial one as a plain StateUsage, dropping its kind: a choice or junction becomes a state that waits, a fork enters one target and a terminate ends nothing. Choice, junction, and history are written as `#StateMachines:: state`; forks and joins as `fork`/`join` nodes; terminate as `action terminate;`. A transition that crosses directly from an entry point to an exit point without settling in a state remains unmapped (one fixture case) | | `ConnectionPointReference_Mapping` | `ConnectionPointReference` | `StateUsage` | `internal/translate/migrate/states.go:connectionPoint` | `tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates` | ✅ faithful | — | | `DoBehaviorStateSubactionMembership_Mapping` | `Behavior` | `StateSubactionMembership` | `internal/translate/migrate/states.go:state`
`internal/translate/migrate/behavior.go:behaviorBody` | `tests/migrate/behavior_more_test.go:TestStateBehaviorsSharingANameAreDistinguished`
`tests/migrate/carriers_test.go:TestStateBehaviorsTakeTheSignalTheirTransitionsAccept` | ✅ faithful | — | | `EntryBehaviorStateSubactionMembership_Mapping` | `Behavior` | `StateSubactionMembership` | `internal/translate/migrate/states.go:state`
`internal/translate/migrate/behavior.go:behaviorBody` | `tests/migrate/behavior_more_test.go:TestStateBehaviorsSharingANameAreDistinguished`
`tests/migrate/carriers_test.go:TestStateBehaviorsTakeTheSignalTheirTransitionsAccept` | ✅ faithful | — | @@ -882,8 +882,8 @@ that build one relationship of a target — share the verdict of the construct t | `FinalState_Mapping` | `FinalState` | `StateUsage` | `internal/translate/migrate/states.go:state` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `InitialStateSubactionMembership_Mapping` | `Pseudostate` | `StateSubactionMembership` | `internal/translate/migrate/states.go:initial`
`internal/translate/migrate/states.go:enter` | `tests/migrate/behavior_test.go:TestStateMachineMigratesToAnExecutableStateDef` | ✅ faithful | — | | `InitialState_Mapping` | `Pseudostate` | `ActionUsage` | `internal/translate/migrate/states.go:initial`
`internal/translate/migrate/states.go:enter` | `tests/migrate/behavior_test.go:TestStateMachineMigratesToAnExecutableStateDef` | ✅ faithful | — | -| `PseudoState_Mapping` | `Pseudostate` | `StateUsage` | `internal/translate/migrate/states.go:pseudostateLine`
`internal/translate/migrate/states.go:vertex` | `tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5) | -| `Region_Mapping` | `Region` | `StateUsage` | `internal/translate/migrate/states.go:region` | `tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/empty_region_test.go:TestEmptyRegionsAreNotWritten` | ⚠️ approximate | the region of a single-region state is flattened into its state rather than written as a state usage; orthogonal regions become the substates of a parallel state; empty regions are not written | +| `PseudoState_Mapping` | `Pseudostate` | `StateUsage` | `internal/translate/migrate/states.go:pseudostateLine`
`internal/translate/migrate/states.go:vertex`
`internal/translate/migrate/states.go:vertexBase`
`internal/translate/migrate/states.go:target` | `tests/migrate/states_test.go:TestStateMachineCrossRegionTransitionsAndPseudostates`
`tests/migrate/terminate_test.go:TestTerminatePseudostateEndsOrthogonalMachine`
`tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateConnectionPointsStayInRegion` | ✅ faithful | candidate erratum: the mapping writes every pseudostate other than an initial one as a plain StateUsage, dropping its kind: a choice or junction becomes a state that waits, a fork enters one target and a terminate ends nothing. Choice, junction, and history are written as `#StateMachines:: state`; forks and joins as `fork`/`join` nodes; terminate as `action terminate;`. A transition that crosses directly from an entry point to an exit point without settling in a state remains unmapped (one fixture case) | +| `Region_Mapping` | `Region` | `StateUsage` | `internal/translate/migrate/states.go:state`
`internal/translate/migrate/states.go:nameRegions`
`internal/translate/migrate/states.go:regions`
`internal/translate/migrate/states.go:regionWrittenAsState`
`internal/translate/migrate/states.go:region`
`internal/translate/migrate/names.go:path`
`internal/translate/migrate/edges.go:edgePlaceIn` | `tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateIsWrapped`
`tests/migrate/region_wrapper_test.go:TestSingleRegionCompositeStateConnectionPointsStayInRegion`
`tests/migrate/region_wrapper_test.go:TestStateMachineSingleRegionRemainsFlattened`
`tests/migrate/behavior_more_test.go:TestStateMachineWithOrthogonalRegionsAndGuards`
`tests/migrate/empty_region_test.go:TestEmptyRegionsAreNotWritten` | ✅ faithful | candidate erratum: for a state machine's only Region the mapping writes a StateUsage in a state definition that is not parallel (`isParallel = region->size() > 1`) and nothing enters it, so it is never entered; its vertices become the state definition's own body. A StateMachine's single Region remains flattened; a composite State's single Region is written as a sub-state of its `parallel` state; orthogonal Regions are parallel sub-states; empty Regions are omitted because an empty parallel sub-state never completes | | `StateBehaviorPerformActionUsageFeatureTyping_Mapping` | `Behavior` | `FeatureTyping` | `internal/translate/migrate/states.go:state`
`internal/translate/migrate/behavior.go:behaviorBody` | `tests/migrate/behavior_more_test.go:TestStateBehaviorsSharingANameAreDistinguished`
`tests/migrate/carriers_test.go:TestStateBehaviorsTakeTheSignalTheirTransitionsAccept` | ✅ faithful | — | | `StateBehaviorPerformActionUsage_Mapping` | `Behavior` | `PerformActionUsage` | `internal/translate/migrate/states.go:state`
`internal/translate/migrate/behavior.go:behaviorBody` | `tests/migrate/behavior_more_test.go:TestStateBehaviorsSharingANameAreDistinguished`
`tests/migrate/carriers_test.go:TestStateBehaviorsTakeTheSignalTheirTransitionsAccept` | ✅ faithful | — | | `StateBehaviorStateSubactionMembership_Mapping` (abstract) | `Behavior` | `StateSubactionMembership` | `internal/translate/migrate/states.go:state`
`internal/translate/migrate/behavior.go:behaviorBody` | `tests/migrate/behavior_more_test.go:TestStateBehaviorsSharingANameAreDistinguished`
`tests/migrate/carriers_test.go:TestStateBehaviorsTakeTheSignalTheirTransitionsAccept` | ✅ faithful | — | @@ -1040,11 +1040,15 @@ expects instead. | `ControlNodeObjectFlowReferenceUsage_Mapping` | `ObjectFlow` | `ReferenceUsage` | ✅ faithful | the `outputObject` branch of the OCL tests `from.target.oclIsTypeOf(UML::MergeNode)` where it means `from.source`, which is written | | `JoinMergeNodeObjectFlowOperatorExpression_Mapping` | `ObjectFlow` | `OperatorExpression` | ✅ faithful | a merge forwards each arriving value on its own rather than the sequence of every input, which is written | | `Slot_Mapping` | `Slot` | `Feature` | ✅ faithful | the mapping creates a KerML Feature; a SysML redefinition of the classifier's feature is a Usage (AttributeUsage / ReferenceUsage / PartUsage per the feature) | +| `OpaqueBehaviorSpecification_Mapping` | `OpaqueBehavior` | `TextualRepresentation` | ✅ faithful | the mapping pairs body i with language i, so a body with no language is dropped; it is kept as a comment. Each nonempty pair is written as a textual representation, while translated bodies remain executable | | `InformationFlow_Mapping` | `InformationFlow` | `FlowDefinition` | ⚠️ approximate | the mapping creates a FlowDefinition for a v1 information flow between specific sources and targets; a definition has no ends bound to those features, which a FlowUsage would have | | `CombinedFragment_Mapping` | `CombinedFragment` | `Interaction` | ⛔ deliberate | the mapping creates an Interaction, a definition, for a fragment that occurs once inside its owning interaction; an occurrence usage would be the matching kind | | `InteractionUse_Mapping` | `InteractionUse` | `Step` | ✅ faithful | the mapping creates a KerML Step; the SysML counterpart of performing another behavior is a PerformActionUsage, which is written | | `Message_Mapping` | `Message` | `Flow` | ⛔ deliberate | the mapping creates a KerML Flow; in a SysML v2 model a message is a FlowUsage (the `message` declaration), which is a Usage and can be owned by definitions and usages | | `StateInvariant_Mapping` | `StateInvariant` | `Invariant` | ✅ faithful | the mapping creates a KerML Invariant; the SysML counterpart is an AssertConstraintUsage, which is written | +| `CommonPseudostate_Mapping` | `Pseudostate` | `Namespace` | ✅ faithful | the mapping writes every pseudostate other than an initial one as a plain StateUsage, dropping its kind: a choice or junction becomes a state that waits, a fork enters one target and a terminate ends nothing. Choice, junction, and history are written as `#StateMachines:: state`; forks and joins as `fork`/`join` nodes; terminate as `action terminate;`. A transition that crosses directly from an entry point to an exit point without settling in a state remains unmapped (one fixture case) | +| `PseudoState_Mapping` | `Pseudostate` | `StateUsage` | ✅ faithful | the mapping writes every pseudostate other than an initial one as a plain StateUsage, dropping its kind: a choice or junction becomes a state that waits, a fork enters one target and a terminate ends nothing. Choice, junction, and history are written as `#StateMachines:: state`; forks and joins as `fork`/`join` nodes; terminate as `action terminate;`. A transition that crosses directly from an entry point to an exit point without settling in a state remains unmapped (one fixture case) | +| `Region_Mapping` | `Region` | `StateUsage` | ✅ faithful | for a state machine's only Region the mapping writes a StateUsage in a state definition that is not parallel (`isParallel = region->size() > 1`) and nothing enters it, so it is never entered; its vertices become the state definition's own body. A StateMachine's single Region remains flattened; a composite State's single Region is written as a sub-state of its `parallel` state; orthogonal Regions are parallel sub-states; empty Regions are omitted because an empty parallel sub-state never completes | Editorially, several class names in the pinned model are misspelled — `COAInvocationExpessionFeatureTyping_Mapping`, @@ -1065,74 +1069,69 @@ SysML profile namespace. The counts are elements affected, not effort. | 2 | 19: `COAOutputPinFeatureChainExpressionMembership_Mapping`, `COAOutputPinFeatureChainExpression_Mapping`, `COAOutputPinFeatureFeatureMembership_Mapping`, `COAOutputPinFeatureFeatureValue_Mapping`, `COAOutputPinFeatureFeature_Mapping`, `COAOutputPinFeatureMembership_Mapping`, `COAOutputPinFeatureReferenceExpressionMembership_Mapping`, `COAOutputPinFeatureReferenceExpression_Mapping`, `COAOutputPinFeature_Mapping`, `COAOutputPinParameterMembership_Mapping`, `COAOutputPinReferenceUsageFeatureValue_Mapping`, `COAOutputPinReferenceUsage_Mapping`, `COAPerformActionFeatureChainingOperation_Mapping`, `COAPerformActionFeatureChainingTarget_Mapping`, `COAPerformActionFeatureMembership_Mapping`, `COAPerformActionFeature_Mapping`, `COAPerformActionReferenceSubsetting_Mapping`, `COAPerformAction_Mapping`, `CallOperationAction_Mapping` | ⚠️ approximate | `uml:CallOperationAction` | 944 | 10 | 954 | a call is written `perform target.op` when its target resolves to a feature that has the operation, otherwise as an action typed by the operation's action def (PSSM: 914 of 932 approximated) | | 3 | 13: `RSFAReferenceUsageExpressionFeatureMembership_Mapping`, `RSFAReferenceUsageExpressionFeatureReferenceExpression_Mapping`, `RSFAReferenceUsageExpressionFeatureValue_Mapping`, `RSFAReferenceUsageExpressionFeature_Mapping`, `RSFAReferenceUsageFeatureChainExpressionFeature_Mapping`, `RSFAReferenceUsageFeatureChainExpressionMembership_Mapping`, `RSFAReferenceUsageFeatureChainExpression_Mapping`, `RSFAReferenceUsageFeatureMembership_Mapping`, `RSFAReferenceUsageFeatureValue_Mapping`, `RSFAReferenceUsageMembership_Mapping`, `RSFAReferenceUsageParameterMembership_Mapping`, `RSFAReferenceUsage_Mapping`, `ReadStructuralFeatureAction_Mapping` | ⚠️ approximate | `uml:ReadStructuralFeatureAction` | 896 | 6 | 902 | a read whose object resolves is written as the feature chain on its result pin; one whose object comes through a pin it cannot resolve is unmapped (PSSM: 655 of 896) | | 4 | 1: `StartObjectBehaviorAction_Mapping` | ❌ not implemented | `uml:StartObjectBehaviorAction` | 832 | 0 | 832 | a StartObjectBehaviorAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 5 | 1: `Class_Mapping` | ⚠️ approximate | `uml:Class` | 311 | 469 | 780 | a plain UML class is written as a part def, not an occurrence def; the scope counts every uml:Class, blocks included, so it is an upper bound | +| 5 | 1: `Class_Mapping` | ⚠️ approximate | `uml:Class` | 311 | 470 | 781 | a plain UML class is written as a part def, not an occurrence def; the scope counts every uml:Class, blocks included, so it is an upper bound | | 6 | 19: `ASFVAFeatureTyping_Mapping`, `ASFVAObjectFeatureMembership_Mapping`, `ASFVAObjectReferenceUsageFeatureTyping_Mapping`, `ASFVAObjectReferenceUsageRedefinition_Mapping`, `ASFVAObjectReferenceUsage_Mapping`, `ASFVATargetFeatureChainExpression_Mapping`, `ASFVATargetFeatureMembership_Mapping`, `ASFVATargetFeatureValue_Mapping`, `ASFVATargetParameterExpressionFeatureMembership_Mapping`, `ASFVATargetParameterExpressionFeature_Mapping`, `ASFVATargetParameterExpressionMembership_Mapping`, `ASFVATargetParameterFeatureExpressionMembership_Mapping`, `ASFVATargetParameterFeatureReferenceExpression_Mapping`, `ASFVATargetParameterFeatureValue_Mapping`, `ASFVATargetParameterFeature_Mapping`, `ASFVATargetParameterMembership_Mapping`, `ASFVATargetReferenceUsageRedefinition_Mapping`, `ASFVATargetReferenceUsage_Mapping`, `AddStructuralFeatureValueAction_Mapping` | ⚠️ approximate | `uml:AddStructuralFeatureValueAction` | 525 | 18 | 543 | an assignment `assign obj.feature := value` replaces the value whatever isReplaceAll says; one whose object it cannot resolve is unmapped (PSSM: 411 approximated, 114 unmapped) | | 7 | 1: `ClearStructuralFeatureAction_Mapping` | ❌ not implemented | `uml:ClearStructuralFeatureAction` | 516 | 0 | 516 | a ClearStructuralFeatureAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | | 8 | 3: `PropertyCommon_Mapping`, `PropertyTypedByClassInterface_Mapping`, `PropertyUntyped_Mapping` | ⚠️ approximate | `uml:Property` | 25 | 459 | 484 | a property typed by a plain class is written as a part (or a reference part when shared), not as an occurrence usage, since the class is written as a part def; the scope counts every uml:Property, so it is an upper bound | | 9 | 5: `COAInvocationExpessionFeatureTyping_Mapping`, `COAInvocationExpression_Mapping`, `COAPinFeatureValue_Mapping`, `COAPin_Mapping`, `CreateObjectAction_Mapping` | ❌ not implemented | `uml:CreateObjectAction` | 426 | 5 | 431 | a CreateObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 10 | 2: `CommonPseudostate_Mapping`, `PseudoState_Mapping` | ⚠️ approximate | `uml:Pseudostate` | 289 | 71 | 360 | choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5) | -| 11 | 1: `Region_Mapping` | ⚠️ approximate | `uml:Region` | 248 | 54 | 302 | the region of a single-region state is flattened into its state rather than written as a state usage; orthogonal regions become the substates of a parallel state; empty regions are not written | -| 12 | 15: `SSAFeatureMembership_Mapping`, `SSAItemParameterMembership_Mapping`, `SSAItemReferenceUsageFeatureTyping_Mapping`, `SSAItemReferenceUsageFeatureValue_Mapping`, `SSAItemReferenceUsageInvocationExpression_Mapping`, `SSAItemReferenceUsage_Mapping`, `SSAParameterMembership_Mapping`, `SSAReferenceUsage_Mapping`, `SSASendActionUsage_Mapping`, `SSATargetParameterMembership_Mapping`, `SSATargetReferenceUsageFeatureValueExpression_Mapping`, `SSATargetReferenceUsageFeatureValueMembership_Mapping`, `SSATargetReferenceUsageFeatureValue_Mapping`, `SSATargetReferenceUsage_Mapping`, `SendSignalAction_Mapping` | ⚠️ approximate | `uml:SendSignalAction` | 254 | 9 | 263 | `send new Signal(…) via port` or `to target`; a send whose target is not a resolvable port or feature is approximated (PSSM: 176 of 254) | -| 13 | 1: `OpaqueAction_Mapping` | ⚠️ approximate | `uml:OpaqueAction` | 0 | 131 | 131 | the body is translated into action statements when its language is read; otherwise the action is a stub keeping its pins (fixtures: 78 of 109 approximated) | -| 14 | 4: `ConstrainedElementFeatureMembership_Mapping`, `ConstraintUsageFeatureTyping_Mapping`, `ConstraintUsage_Mapping`, `Constraint_Mapping` | ⚠️ approximate | `uml:Constraint` | 59 | 24 | 83 | an owned rule is written as a constraint usage `constraint name { expr }` in its context, not as a constraint def plus an asserted constraint usage; a specification yielding no Boolean is a comment (unmapped) | -| 15 | 31: `ViewpointConcernReferenceSubsetting_Mapping`, `ViewpointConcernUsage_Mapping`, `ViewpointConstraintUsageDocumentation_Mapping`, `ViewpointConstraintUsageOwningMembership_Mapping`, `ViewpointConstraintUsage_Mapping`, `ViewpointFramedConcernMembership_Mapping`, `ViewpointLanguagesMetadataFeatureMembership_Mapping`, `ViewpointLanguagesMetadataFeatureValue_Mapping`, `ViewpointLanguagesMetadataOperatorExpression_Mapping`, `ViewpointLanguagesMetadataRedefinition_Mapping`, `ViewpointLanguagesMetadataReferenceUsage_Mapping`, `ViewpointMetadataFeatureTyping_Mapping`, `ViewpointMetadataOwningMembership_Mapping`, `ViewpointMetadataUsage_Mapping`, `ViewpointPresentationsMetadataFeatureMembership_Mapping`, `ViewpointPresentationsMetadataFeatureValue_Mapping`, `ViewpointPresentationsMetadataOperatorExpression_Mapping`, `ViewpointPresentationsMetadataRedefinition_Mapping`, `ViewpointPresentationsMetadataReferenceUsage_Mapping`, `ViewpointRenderingFeatureMembership_Mapping`, `ViewpointRenderingUsageActionUsageFeatureMembership_Mapping`, `ViewpointRenderingUsageActionUsageFeatureTyping_Mapping`, `ViewpointRenderingUsageActionUsage_Mapping`, `ViewpointRenderingUsage_Mapping`, `ViewpointRequirementConstraintMembership_Mapping`, `ViewpointSatisfyFeatureMembership_Mapping`, `ViewpointSatisfyRequirementUsageReferenceSubsetting_Mapping`, `ViewpointSatisfyRequirementUsage_Mapping`, `ViewpointViewpointUsageFeatureMembership_Mapping`, `ViewpointViewpointUsage_Mapping`, `Viewpoint_Mapping` | ⚠️ approximate | `sysml:Viewpoint` | 0 | 62 | 62 | a «Viewpoint» is written as a `viewpoint` usage framing its concerns and stakeholders, not as a view def owning a viewpoint usage; its purpose, languages, method and presentation are kept as documentation rather than metadata usages | -| 16 | 1: `InstanceSpecificationLink_Mapping` | ❌ not implemented | `uml:InstanceSpecification` | 0 | 54 | 54 | an instance specification classified by an association (a link) is not written; the scope counts every instance specification, links or not, so it is an upper bound | -| 17 | 9: `StereotypeOccurenceUsageFeatureTyping_Mapping`, `StereotypeOccurenceUsageInfinityReturnParameterMembership_Mapping`, `StereotypeOccurenceUsageInfinityReturnParameter_Mapping`, `StereotypeOccurenceUsageMembership_Mapping`, `StereotypeOccurenceUsageMultiplicityMembership_Mapping`, `StereotypeOccurenceUsageMultiplicityRangeInfinity_Mapping`, `StereotypeOccurenceUsageMultiplicityRangeMembership_Mapping`, `StereotypeOccurenceUsageMultiplicityRange_Mapping`, `StereotypeOccurenceUsage_Mapping` | ⚠️ approximate | `uml:Stereotype` | 0 | 50 | 50 | the base_* attributes and uml:Extensions binding a stereotype to its metaclass are skipped, so the metadata def restricts no annotated element; v2 metadata applies to any element and the migrated model applies it where v1 did | -| 18 | 3: `OpaqueBehaviorMembership_Mapping`, `OpaqueBehaviorSpecification_Mapping`, `OpaqueBehavior_Mapping` | ⚠️ approximate | `uml:OpaqueBehavior` | 0 | 47 | 47 | an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated) | -| 19 | 7: `EqualOperatorExpressionFeatureValue_Mapping`, `EqualOperatorExpressionFeature_Mapping`, `EqualOperatorExpressionOperandParameterMembership_Mapping`, `ExpressionElseMembership_Mapping`, `ExpressionElseSpecification_Mapping`, `ExpressionElse_Mapping`, `Expression_Mapping` | ⚠️ approximate | `uml:Expression` | 4 | 41 | 45 | an Expression is written as the v2 operator expression its symbol names when the symbol is one v2 has; other operators are not written | -| 20 | 4: `ChangeEventReturnParameterMembership_Mapping`, `ChangeEventReturnParameter_Mapping`, `ChangeEvent_Mapping`, `TimeEvent_Mapping` | ⚠️ approximate | `uml:TimeEvent`
`uml:ChangeEvent`
`uml:CallEvent` | 8 | 16 | 24 | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | -| 21 | 25: `AssociationClass_Mapping`, `AssociationCommon_Mapping`, `AssociationMetadataUsageFeatureMembership_Mapping`, `AssociationMetadataUsageFeatureTyping_Mapping`, `AssociationMetadataUsageFeatureValue_Mapping`, `AssociationMetadataUsageFeature_Mapping`, `AssociationMetadataUsageMembership_Mapping`, `AssociationMetadataUsageRedefinition_Mapping`, `AssociationMetadataUsage_Mapping`, `ConnectionDefEndMembership_Mapping`, `ConnectionDefEnd_Mapping`, `ConnectorTypeDerived_Mapping`, `ConnectorType_Mapping`, `EndMembership_Mapping`, `EndToSubsettedFeatureChaining_Mapping`, `EndToSubsettedFeature_Mapping`, `End_Mapping`, `NonOwnedEndFeatureTyping_Mapping`, `NonOwnedEndMembership_Mapping`, `NonOwnedEndSubsettingMembership_Mapping`, `NonOwnedEndSubsetting_Mapping`, `NonOwnedEndToSubsettedFeatureMembership_Mapping`, `NonOwnedEnd_Mapping`, `OwnedEndMembership_Mapping`, `OwnedEnd_Mapping` | ⚠️ approximate | `uml:Association` | 4 | 14 | 18 | a named association is written as a connection def with its ends; an anonymous one only as its navigable ends; no association metadata is written | -| 22 | 23: `TimeTriggerBindingConnector_Mapping`, `TimeTriggerCalculationUsage_Mapping`, `TimeTriggerEndFeatureMembership_Mapping`, `TimeTriggerEventChainingFeature_Mapping`, `TimeTriggerEventReturnParameterChainingFeature_Mapping`, `TimeTriggerExpressionFeatureTyping_Mapping`, `TimeTriggerExpressionFeatureValue_Mapping`, `TimeTriggerExpressionFeature_Mapping`, `TimeTriggerExpressionInvocationExpression_Mapping`, `TimeTriggerExpressionParameterMembership_Mapping`, `TimeTriggerFeatureMembership_Mapping`, `TimeTriggerFeatureTyping_Mapping`, `TimeTriggerFeatureValue_Mapping`, `TimeTriggerFeature_Mapping`, `TimeTriggerInvocationExpression_Mapping`, `TimeTriggerReferenceSubsetting_Mapping`, `TimeTriggerReferenceUsage2_Mapping`, `TimeTriggerReferenceUsage_Mapping`, `TimeTriggerReturnEndFeatureMembership_Mapping`, `TimeTriggerReturnParameterMembership_Mapping`, `TimeTriggerReturnParameter_Mapping`, `TimeTriggerReturnReferenceSubsetting_Mapping`, `TimeTriggerReturnReferenceUsage_Mapping` | ⚠️ approximate | `uml:TimeEvent` | 0 | 16 | 16 | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| 23 | 7: `ProbabilityMetadataUsageFeatureMembership_Mapping`, `ProbabilityMetadataUsageFeatureTyping_Mapping`, `ProbabilityMetadataUsageReferenceUsageFeatureValue_Mapping`, `ProbabilityMetadataUsageReferenceUsageRedefinition_Mapping`, `ProbabilityMetadataUsageReferenceUsage_Mapping`, `ProbabilityMetadataUsage_Mapping`, `ProbabilityOwningMembership_Mapping` | ⚠️ approximate | `sysml:Probability` | 0 | 14 | 14 | written as @Stochastic::Probability { p = … } on the succession out of the decision, from the bundled library, not the SysML v1 library metadata the mapping types; a probability not provably in [0,1] is not written | -| 24 | 3: `TIAOperatorExpression_Mapping`, `TIAResultExpressionMembership_Mapping`, `TestIdentityAction_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | a TestIdentityAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 25 | 9: `RICOAFeatureValueOperatorExpressionFeatureValue_Mapping`, `RICOAFeatureValueOperatorExpressionFeature_Mapping`, `RICOAFeatureValueOperatorExpression_Mapping`, `RICOAFeatureValueOperatorFeatureReferenceExpression_Mapping`, `RICOAFeatureValueOperatorMembership_Mapping`, `RICOAFeatureValueOperatorParameterMembership_Mapping`, `RICOAFeatureValue_Mapping`, `RICOAOutputPin_Mapping`, `ReadIsClassifiedObjectAction_Mapping` | ❌ not implemented | `uml:ReadIsClassifiedObjectAction` | 12 | 0 | 12 | a ReadIsClassifiedObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 26 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 10 | 10 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | -| 27 | 1: `InterfaceRealization_Mapping` | ⚠️ approximate | `uml:InterfaceRealization` | 0 | 8 | 8 | a block realizing an interface gets a port typed by the interface's port def rather than specializing it; an interface block realizing one specializes it | -| 28 | 8: `AllocationFeatureTyping_Mapping`, `AllocationSourceFeatureMembership_Mapping`, `AllocationSourceReferenceUsageRedefinition_Mapping`, `AllocationSourceReferenceUsage_Mapping`, `AllocationTargetFeatureMembership_Mapping`, `AllocationTargetReferenceUsageRedefinition_Mapping`, `AllocationTargetReferenceUsage_Mapping`, `Allocation_Mapping` | ⚠️ approximate | `sysml:Allocate` | 0 | 6 | 6 | an `allocation def` is written only for an «Allocate» between two definitions; any other «Allocate» becomes an allocation usage or a dependency, not also a definition | -| 29 | 13: `AllocationUsageFeatureChainingChainedFeature_Mapping`, `AllocationUsageFeatureMembership_Mapping`, `AllocationUsageSourceEndFeatureMembership_Mapping`, `AllocationUsageSourceFeatureChaining_Mapping`, `AllocationUsageSourceFeatureSubsettingFeature_Mapping`, `AllocationUsageSourceFeatureSubsetting_Mapping`, `AllocationUsageSourceFeature_Mapping`, `AllocationUsageTargetEndFeatureMembership_Mapping`, `AllocationUsageTargetFeatureChaining_Mapping`, `AllocationUsageTargetFeatureSubsettingFeature_Mapping`, `AllocationUsageTargetFeatureSubsetting_Mapping`, `AllocationUsageTargetFeature_Mapping`, `AllocationUsage_Mapping` | ⚠️ approximate | `sysml:Allocate` | 0 | 6 | 6 | `allocate a to b` where both ends are features of one body; between a definition and a usage, or with an end no allocate reaches, it is written as a dependency | -| 30 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | -| 31 | 10: `CommonFullPort_Mapping`, `FullPortMetadataFeatureMembership_Mapping`, `FullPortMetadataFeatureTyping_Mapping`, `FullPortMetadataOwningMembership_Mapping`, `FullPortMetadataReferenceUsageFeatureValue_Mapping`, `FullPortMetadataReferenceUsageRedefinition_Mapping`, `FullPortMetadataReferenceUsage_Mapping`, `FullPortMetadata_Mapping`, `FullPortUntyped_Mapping`, `FullPort_Mapping` | ⚠️ approximate | `sysml:FullPort` | 0 | 2 | 2 | a «FullPort» is written as a `port` typed by its block (like a proxy port), not as a part usage carrying full-port metadata | -| 32 | 8: `EncapsulatedBlockMetadataFeatureMembership_Mapping`, `EncapsulatedBlockMetadataFeatureTyping_Mapping`, `EncapsulatedBlockMetadataFeatureValue_Mapping`, `EncapsulatedBlockMetadataMembership_Mapping`, `EncapsulatedBlockMetadataRedefinition_Mapping`, `EncapsulatedBlockMetadataReferenceUsage_Mapping`, `EncapsulatedBlockMetadata_Mapping`, `EncapsulatedBlock_Mapping` | ⚠️ approximate | `sysml:Block[isEncapsulated=true]` | 0 | 2 | 2 | isEncapsulated = true is kept as a comment and reported as having no v2 form, not written as metadata on the part def | -| 33 | 5: `InformationFlowConveyedFeatureMembership_Mapping`, `InformationFlowEndFeatureMembership_Mapping`, `InformationFlowEnd_Mapping`, `InformationFlowFeatureTyping_Mapping`, `InformationFlowSubclassification_Mapping` | ⚠️ approximate | `uml:InformationFlow` | 0 | 2 | 2 | an information flow realized by a connector is written as a flow usage `flow of Item from a to b` on that connector, not as a flow def (see candidate errata) | -| 34 | 1: `InformationFlow_Mapping` | ⚠️ approximate | `uml:InformationFlow` | 0 | 2 | 2 | an information flow realized by a connector is written as a flow usage `flow of Item from a to b` on that connector, not as a flow def (see candidate errata); candidate erratum: the mapping creates a FlowDefinition for a v1 information flow between specific sources and targets; a definition has no ends bound to those features, which a FlowUsage would have | -| 35 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 0 | 2 | a loop node is written as `action x { }` holding its body once; the loop test and iteration are not written | -| 36 | 7: `StakeholderMetadataFeatureMembership_Mapping`, `StakeholderMetadataFeatureTyping_Mapping`, `StakeholderMetadataOwningMembership_Mapping`, `StakeholderMetadataReferenceUsageFeatureValue_Mapping`, `StakeholderMetadataReferenceUsageRedefinition_Mapping`, `StakeholderMetadataReferenceUsage_Mapping`, `StakeholderMetadataUsage_Mapping` | ⚠️ approximate | `sysml:Stakeholder` | 0 | 2 | 2 | a «Stakeholder» class is written as a part def, which a viewpoint's `stakeholder` usage can type; its concern tag stays a comment (see candidate errata for the item def the mapping writes) | -| 37 | 1: `Stakeholder_Mapping` | ⚠️ approximate | `sysml:Stakeholder` | 0 | 2 | 2 | a «Stakeholder» class is written as a part def, which a viewpoint's `stakeholder` usage can type; its concern tag stays a comment (see candidate errata for the item def the mapping writes); candidate erratum: the mapping maps «Stakeholder» to an ItemDefinition, but a v2 stakeholder parameter is a PartUsage (StakeholderMembership::ownedStakeholderParameter), which requires a PartDefinition among its definitions; a PartDefinition is the type that can be used | -| 38 | 9: `DOADestroyActionUsageFeatureMembership_Mapping`, `DOADestroyActionUsageFeatureReferenceExpression_Mapping`, `DOADestroyActionUsageFeatureTyping_Mapping`, `DOADestroyActionUsageFeatureValue_Mapping`, `DOADestroyActionUsageMembership_Mapping`, `DOADestroyActionUsageReferenceUsage_Mapping`, `DOADestroyActionUsage_Mapping`, `DOADestroyFeatureMembership_Mapping`, `DestroyObjectAction_Mapping` | ❌ not implemented | `uml:DestroyObjectAction` | 0 | 1 | 1 | a DestroyObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 39 | 12: `AVVAFeatureTyping_Mapping`, `AVVAFeatureValue_Mapping`, `AVVAIsReplaceAllFeatureMembership_Mapping`, `AVVAIsReplaceAllRedefinition_Mapping`, `AVVAIsReplaceAllValue_Mapping`, `AVVAIsReplaceAll_Mapping`, `AVVAValueExpressionMembership_Mapping`, `AVVAValueFeatureReferenceExpression_Mapping`, `AVVAVariableFeatureMembership_Mapping`, `AVVAVariableRedefinition_Mapping`, `AVVAVariable_Mapping`, `AddVariableValueAction_Mapping` | ❌ not implemented | `uml:AddVariableValueAction` | 0 | 0 | 0 | a AddVariableValueAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 40 | 1: `AcceptCallAction_Mapping` | ❌ not implemented | `uml:AcceptCallAction` | 0 | 0 | 0 | a AcceptCallAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 41 | 1: `AcceptChangeStructuralFeatureEventAction_Mapping` | ❌ not implemented | `sysml:AcceptChangeStructuralFeatureEventAction` | 0 | 0 | 0 | an AcceptChangeStructuralFeatureEventAction is not migrated: activity.go writes it as an unmapped placeholder action | -| 42 | 1: `BroadcastSignalAction_Mapping` | ❌ not implemented | `uml:BroadcastSignalAction` | 0 | 0 | 0 | a BroadcastSignalAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 43 | 4: `CVAFeatureMembership_Mapping`, `CVAReferenceUsageFeatureValue_Mapping`, `CVAReferenceUsage_Mapping`, `ClearVariableAction_Mapping` | ❌ not implemented | `uml:ClearVariableAction` | 0 | 0 | 0 | a ClearVariableAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 44 | 2: `CentralBufferNode_Mapping`, `DataStoreNode_Mapping` | ⚠️ approximate | `uml:CentralBufferNode`
`uml:DataStoreNode` | 0 | 0 | 0 | an object node is written as an action that holds no tokens: the flows through it are written from its sources to its targets | -| 45 | 1: `ClearAssociationAction_Mapping` | ❌ not implemented | `uml:ClearAssociationAction` | 0 | 0 | 0 | a ClearAssociationAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 46 | 5: `CommonVariable_Mapping`, `VariableAttribute_Mapping`, `VariableFeatureTyping_Mapping`, `VariableItem_Mapping`, `VariableMembership_Mapping` | ❌ not implemented | `uml:Variable` | 0 | 0 | 0 | activity variables are not migrated: the variable actions that read or write them are placeholders | -| 47 | 8: `ConcernDocumentation_Mapping`, `ConcernOwningMembership_Mapping`, `ConcernStakeholderMembership_Mapping`, `ConcernStakeholderPartUsageFeatureTyping_Mapping`, `ConcernStakeholderPartUsageFeature_Mapping`, `ConcernStakeholderPartUsageOwningMembership_Mapping`, `ConcernStakeholderPartUsage_Mapping`, `Concern_Mapping` | ⚠️ approximate | `sysml:Concern` | 0 | 0 | 0 | a viewpoint's concern is written as an anonymous `frame concern` documented by the concern text; a «Concern» comment outside a viewpoint is a plain comment | -| 48 | 1: `CreateLinkAction_Mapping` | ❌ not implemented | `uml:CreateLinkAction` | 0 | 0 | 0 | a CreateLinkAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 49 | 1: `CreateLinkObjectAction_Mapping` | ❌ not implemented | `uml:CreateLinkObjectAction` | 0 | 0 | 0 | a CreateLinkObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 50 | 1: `DestroyLinkAction_Mapping` | ❌ not implemented | `uml:DestroyLinkAction` | 0 | 0 | 0 | a DestroyLinkAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 51 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | -| 52 | 1: `ElementImport_Mapping` | ❌ not implemented | `uml:ElementImport` | 0 | 0 | 0 | element imports are not written: names are resolved and written qualified instead | -| 53 | 3: `InformationItemFlowConveyedItemUsageFeatureTyping_Mapping`, `InformationItemFlowConveyedItemUsage_Mapping`, `InformationItem_Mapping` | ❌ not implemented | `uml:InformationItem` | 0 | 0 | 0 | no migrator code writes an InformationItem as an item def (it is only recognised as a classifier when resolving names) | -| 54 | 1: `OperationDirectedFeature_Mapping` | ❌ not implemented | `sysml:DirectedFeature` | 0 | 0 | 0 | «DirectedFeature» on operations is not read; the operation is written without a direction | -| 55 | 8: `PackageURIFeatureMembership_Mapping`, `PackageURIFeatureTyping_Mapping`, `PackageURIMetadataFeatureValue_Mapping`, `PackageURIMetadataMembership_Mapping`, `PackageURIMetadataReferenceUsage_Mapping`, `PackageURIMetadataUsage_Mapping`, `PackageURIRedefinition_Mapping`, `PackageURIValue_Mapping` | ❌ not implemented | `uml:Package[URI]` | 0 | 0 | 0 | a package's URI is not written as metadata | -| 56 | 7: `ParameterSetMembership_Mapping`, `ParameterSetParameterFeatureMembership_Mapping`, `ParameterSetParameterReferenceUsageFeatureValueExpression_Mapping`, `ParameterSetParameterReferenceUsageFeatureValue_Mapping`, `ParameterSetParameterReferenceUsageMembership_Mapping`, `ParameterSetParameterReferenceUsage_Mapping`, `ParameterSet_Mapping` | ❌ not implemented | `uml:ParameterSet` | 0 | 0 | 0 | parameter sets are not read by the migrator (no code refers to ParameterSet): the parameters are written, the set grouping them is not | -| 57 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | -| 58 | 1: `QualifierMembership_Mapping` | ❌ not implemented | `uml:Property[qualifier]` | 0 | 0 | 0 | association-end qualifiers are not read | -| 59 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 60 | 7: `RVAFeatureMembership_Mapping`, `RVAReferenceUsageExpressionMembership_Mapping`, `RVAReferenceUsageFeatureReferenceExpression_Mapping`, `RVAReferenceUsageFeatureTyping_Mapping`, `RVAReferenceUsageFeatureValue_Mapping`, `RVAReferenceUsage_Mapping`, `ReadVariableAction_Mapping` | ❌ not implemented | `uml:ReadVariableAction` | 0 | 0 | 0 | a ReadVariableAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 61 | 8: `RVVAFeatureTyping_Mapping`, `RVVAVariableExpressionMembership_Mapping`, `RVVAVariableFeatureMembership_Mapping`, `RVVAVariableFeatureReferenceExpression_Mapping`, `RVVAVariableFeatureValue_Mapping`, `RVVAVariableRedefinition_Mapping`, `RVVAVariable_Mapping`, `RemoveVariableValueAction_Mapping` | ❌ not implemented | `uml:RemoveVariableValueAction` | 0 | 0 | 0 | a RemoveVariableValueAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 62 | 1: `RaiseExceptionAction_Mapping` | ❌ not implemented | `uml:RaiseExceptionAction` | 0 | 0 | 0 | a RaiseExceptionAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 63 | 10: `RateMetadataUsageContinuousFeatureMembership_Mapping`, `RateMetadataUsageContinuousReferenceUsageRedefinition_Mapping`, `RateMetadataUsageContinuousReferenceUsage_Mapping`, `RateMetadataUsageDiscreteFeatureMembership_Mapping`, `RateMetadataUsageDiscreteReferenceUsageRedefinition_Mapping`, `RateMetadataUsageDiscreteReferenceUsage_Mapping`, `RateMetadataUsageFeatureTyping_Mapping`, `RateMetadataUsageFeatureValue_Mapping`, `RateMetadataUsage_Mapping`, `RateOwningMembership_Mapping` | ❌ not implemented | `sysml:Rate`
`sysml:Continuous`
`sysml:Discrete` | 0 | 0 | 0 | «Rate», «Continuous» and «Discrete» are not read by the migrator (no code refers to them): the flow is written without rate semantics or rate metadata | -| 64 | 1: `ReadLinkAction_Mapping` | ❌ not implemented | `uml:ReadLinkAction` | 0 | 0 | 0 | a ReadLinkAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 65 | 1: `ReadLinkObjectEndAction_Mapping` | ❌ not implemented | `uml:ReadLinkObjectEndAction` | 0 | 0 | 0 | a ReadLinkObjectEndAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 66 | 1: `ReadLinkObjectEndQualifierAction_Mapping` | ❌ not implemented | `uml:ReadLinkObjectEndQualifierAction` | 0 | 0 | 0 | a ReadLinkObjectEndQualifierAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 67 | 1: `ReclassifyObjectAction_Mapping` | ❌ not implemented | `uml:ReclassifyObjectAction` | 0 | 0 | 0 | a ReclassifyObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 68 | 1: `ReduceAction_Mapping` | ❌ not implemented | `uml:ReduceAction` | 0 | 0 | 0 | a ReduceAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 69 | 1: `RemoveStructuralFeatureValueAction_Mapping` | ❌ not implemented | `uml:RemoveStructuralFeatureValueAction` | 0 | 0 | 0 | a RemoveStructuralFeatureValueAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 70 | 1: `ReplyAction_Mapping` | ❌ not implemented | `uml:ReplyAction` | 0 | 0 | 0 | a ReplyAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 71 | 1: `SendObjectAction_Mapping` | ❌ not implemented | `uml:SendObjectAction` | 0 | 0 | 0 | a SendObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 72 | 1: `StartClassifierBehaviorAction_Mapping` | ❌ not implemented | `uml:StartClassifierBehaviorAction` | 0 | 0 | 0 | a StartClassifierBehaviorAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 73 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | -| 74 | 1: `UnmarshallAction_Mapping` | ❌ not implemented | `uml:UnmarshallAction` | 0 | 0 | 0 | a UnmarshallAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 10 | 15: `SSAFeatureMembership_Mapping`, `SSAItemParameterMembership_Mapping`, `SSAItemReferenceUsageFeatureTyping_Mapping`, `SSAItemReferenceUsageFeatureValue_Mapping`, `SSAItemReferenceUsageInvocationExpression_Mapping`, `SSAItemReferenceUsage_Mapping`, `SSAParameterMembership_Mapping`, `SSAReferenceUsage_Mapping`, `SSASendActionUsage_Mapping`, `SSATargetParameterMembership_Mapping`, `SSATargetReferenceUsageFeatureValueExpression_Mapping`, `SSATargetReferenceUsageFeatureValueMembership_Mapping`, `SSATargetReferenceUsageFeatureValue_Mapping`, `SSATargetReferenceUsage_Mapping`, `SendSignalAction_Mapping` | ⚠️ approximate | `uml:SendSignalAction` | 254 | 9 | 263 | `send new Signal(…) via port` or `to target`; a send whose target is not a resolvable port or feature is approximated (PSSM: 176 of 254) | +| 11 | 4: `ConstrainedElementFeatureMembership_Mapping`, `ConstraintUsageFeatureTyping_Mapping`, `ConstraintUsage_Mapping`, `Constraint_Mapping` | ⚠️ approximate | `uml:Constraint` | 59 | 24 | 83 | an owned rule is written as a constraint usage `constraint name { expr }` in its context, not as a constraint def plus an asserted constraint usage; a specification yielding no Boolean is a comment (unmapped) | +| 12 | 31: `ViewpointConcernReferenceSubsetting_Mapping`, `ViewpointConcernUsage_Mapping`, `ViewpointConstraintUsageDocumentation_Mapping`, `ViewpointConstraintUsageOwningMembership_Mapping`, `ViewpointConstraintUsage_Mapping`, `ViewpointFramedConcernMembership_Mapping`, `ViewpointLanguagesMetadataFeatureMembership_Mapping`, `ViewpointLanguagesMetadataFeatureValue_Mapping`, `ViewpointLanguagesMetadataOperatorExpression_Mapping`, `ViewpointLanguagesMetadataRedefinition_Mapping`, `ViewpointLanguagesMetadataReferenceUsage_Mapping`, `ViewpointMetadataFeatureTyping_Mapping`, `ViewpointMetadataOwningMembership_Mapping`, `ViewpointMetadataUsage_Mapping`, `ViewpointPresentationsMetadataFeatureMembership_Mapping`, `ViewpointPresentationsMetadataFeatureValue_Mapping`, `ViewpointPresentationsMetadataOperatorExpression_Mapping`, `ViewpointPresentationsMetadataRedefinition_Mapping`, `ViewpointPresentationsMetadataReferenceUsage_Mapping`, `ViewpointRenderingFeatureMembership_Mapping`, `ViewpointRenderingUsageActionUsageFeatureMembership_Mapping`, `ViewpointRenderingUsageActionUsageFeatureTyping_Mapping`, `ViewpointRenderingUsageActionUsage_Mapping`, `ViewpointRenderingUsage_Mapping`, `ViewpointRequirementConstraintMembership_Mapping`, `ViewpointSatisfyFeatureMembership_Mapping`, `ViewpointSatisfyRequirementUsageReferenceSubsetting_Mapping`, `ViewpointSatisfyRequirementUsage_Mapping`, `ViewpointViewpointUsageFeatureMembership_Mapping`, `ViewpointViewpointUsage_Mapping`, `Viewpoint_Mapping` | ⚠️ approximate | `sysml:Viewpoint` | 0 | 62 | 62 | a «Viewpoint» is written as a `viewpoint` usage framing its concerns and stakeholders, not as a view def owning a viewpoint usage; its purpose, languages, method and presentation are kept as documentation rather than metadata usages | +| 13 | 1: `InstanceSpecificationLink_Mapping` | ❌ not implemented | `uml:InstanceSpecification` | 0 | 54 | 54 | an instance specification classified by an association (a link) is not written; the scope counts every instance specification, links or not, so it is an upper bound | +| 14 | 9: `StereotypeOccurenceUsageFeatureTyping_Mapping`, `StereotypeOccurenceUsageInfinityReturnParameterMembership_Mapping`, `StereotypeOccurenceUsageInfinityReturnParameter_Mapping`, `StereotypeOccurenceUsageMembership_Mapping`, `StereotypeOccurenceUsageMultiplicityMembership_Mapping`, `StereotypeOccurenceUsageMultiplicityRangeInfinity_Mapping`, `StereotypeOccurenceUsageMultiplicityRangeMembership_Mapping`, `StereotypeOccurenceUsageMultiplicityRange_Mapping`, `StereotypeOccurenceUsage_Mapping` | ⚠️ approximate | `uml:Stereotype` | 0 | 50 | 50 | the base_* attributes and uml:Extensions binding a stereotype to its metaclass are skipped, so the metadata def restricts no annotated element; v2 metadata applies to any element and the migrated model applies it where v1 did | +| 15 | 7: `EqualOperatorExpressionFeatureValue_Mapping`, `EqualOperatorExpressionFeature_Mapping`, `EqualOperatorExpressionOperandParameterMembership_Mapping`, `ExpressionElseMembership_Mapping`, `ExpressionElseSpecification_Mapping`, `ExpressionElse_Mapping`, `Expression_Mapping` | ⚠️ approximate | `uml:Expression` | 4 | 41 | 45 | an Expression is written as the v2 operator expression its symbol names when the symbol is one v2 has; other operators are not written | +| 16 | 25: `AssociationClass_Mapping`, `AssociationCommon_Mapping`, `AssociationMetadataUsageFeatureMembership_Mapping`, `AssociationMetadataUsageFeatureTyping_Mapping`, `AssociationMetadataUsageFeatureValue_Mapping`, `AssociationMetadataUsageFeature_Mapping`, `AssociationMetadataUsageMembership_Mapping`, `AssociationMetadataUsageRedefinition_Mapping`, `AssociationMetadataUsage_Mapping`, `ConnectionDefEndMembership_Mapping`, `ConnectionDefEnd_Mapping`, `ConnectorTypeDerived_Mapping`, `ConnectorType_Mapping`, `EndMembership_Mapping`, `EndToSubsettedFeatureChaining_Mapping`, `EndToSubsettedFeature_Mapping`, `End_Mapping`, `NonOwnedEndFeatureTyping_Mapping`, `NonOwnedEndMembership_Mapping`, `NonOwnedEndSubsettingMembership_Mapping`, `NonOwnedEndSubsetting_Mapping`, `NonOwnedEndToSubsettedFeatureMembership_Mapping`, `NonOwnedEnd_Mapping`, `OwnedEndMembership_Mapping`, `OwnedEnd_Mapping` | ⚠️ approximate | `uml:Association` | 4 | 14 | 18 | a named association is written as a connection def with its ends; an anonymous one only as its navigable ends; no association metadata is written | +| 17 | 1: `TimeTriggerReferenceUsage2_Mapping` | ⚠️ approximate | `uml:TimeEvent` | 0 | 16 | 16 | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | +| 18 | 7: `ProbabilityMetadataUsageFeatureMembership_Mapping`, `ProbabilityMetadataUsageFeatureTyping_Mapping`, `ProbabilityMetadataUsageReferenceUsageFeatureValue_Mapping`, `ProbabilityMetadataUsageReferenceUsageRedefinition_Mapping`, `ProbabilityMetadataUsageReferenceUsage_Mapping`, `ProbabilityMetadataUsage_Mapping`, `ProbabilityOwningMembership_Mapping` | ⚠️ approximate | `sysml:Probability` | 0 | 14 | 14 | written as @Stochastic::Probability { p = … } on the succession out of the decision, from the bundled library, not the SysML v1 library metadata the mapping types; a probability not provably in [0,1] is not written | +| 19 | 3: `TIAOperatorExpression_Mapping`, `TIAResultExpressionMembership_Mapping`, `TestIdentityAction_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | a TestIdentityAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 20 | 9: `RICOAFeatureValueOperatorExpressionFeatureValue_Mapping`, `RICOAFeatureValueOperatorExpressionFeature_Mapping`, `RICOAFeatureValueOperatorExpression_Mapping`, `RICOAFeatureValueOperatorFeatureReferenceExpression_Mapping`, `RICOAFeatureValueOperatorMembership_Mapping`, `RICOAFeatureValueOperatorParameterMembership_Mapping`, `RICOAFeatureValue_Mapping`, `RICOAOutputPin_Mapping`, `ReadIsClassifiedObjectAction_Mapping` | ❌ not implemented | `uml:ReadIsClassifiedObjectAction` | 12 | 0 | 12 | a ReadIsClassifiedObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 21 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 10 | 10 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | +| 22 | 1: `InterfaceRealization_Mapping` | ⚠️ approximate | `uml:InterfaceRealization` | 0 | 8 | 8 | a block realizing an interface gets a port typed by the interface's port def rather than specializing it; an interface block realizing one specializes it | +| 23 | 8: `AllocationFeatureTyping_Mapping`, `AllocationSourceFeatureMembership_Mapping`, `AllocationSourceReferenceUsageRedefinition_Mapping`, `AllocationSourceReferenceUsage_Mapping`, `AllocationTargetFeatureMembership_Mapping`, `AllocationTargetReferenceUsageRedefinition_Mapping`, `AllocationTargetReferenceUsage_Mapping`, `Allocation_Mapping` | ⚠️ approximate | `sysml:Allocate` | 0 | 6 | 6 | an `allocation def` is written only for an «Allocate» between two definitions; any other «Allocate» becomes an allocation usage or a dependency, not also a definition | +| 24 | 13: `AllocationUsageFeatureChainingChainedFeature_Mapping`, `AllocationUsageFeatureMembership_Mapping`, `AllocationUsageSourceEndFeatureMembership_Mapping`, `AllocationUsageSourceFeatureChaining_Mapping`, `AllocationUsageSourceFeatureSubsettingFeature_Mapping`, `AllocationUsageSourceFeatureSubsetting_Mapping`, `AllocationUsageSourceFeature_Mapping`, `AllocationUsageTargetEndFeatureMembership_Mapping`, `AllocationUsageTargetFeatureChaining_Mapping`, `AllocationUsageTargetFeatureSubsettingFeature_Mapping`, `AllocationUsageTargetFeatureSubsetting_Mapping`, `AllocationUsageTargetFeature_Mapping`, `AllocationUsage_Mapping` | ⚠️ approximate | `sysml:Allocate` | 0 | 6 | 6 | `allocate a to b` where both ends are features of one body; between a definition and a usage, or with an end no allocate reaches, it is written as a dependency | +| 25 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | +| 26 | 10: `CommonFullPort_Mapping`, `FullPortMetadataFeatureMembership_Mapping`, `FullPortMetadataFeatureTyping_Mapping`, `FullPortMetadataOwningMembership_Mapping`, `FullPortMetadataReferenceUsageFeatureValue_Mapping`, `FullPortMetadataReferenceUsageRedefinition_Mapping`, `FullPortMetadataReferenceUsage_Mapping`, `FullPortMetadata_Mapping`, `FullPortUntyped_Mapping`, `FullPort_Mapping` | ⚠️ approximate | `sysml:FullPort` | 0 | 2 | 2 | a «FullPort» is written as a `port` typed by its block (like a proxy port), not as a part usage carrying full-port metadata | +| 27 | 8: `EncapsulatedBlockMetadataFeatureMembership_Mapping`, `EncapsulatedBlockMetadataFeatureTyping_Mapping`, `EncapsulatedBlockMetadataFeatureValue_Mapping`, `EncapsulatedBlockMetadataMembership_Mapping`, `EncapsulatedBlockMetadataRedefinition_Mapping`, `EncapsulatedBlockMetadataReferenceUsage_Mapping`, `EncapsulatedBlockMetadata_Mapping`, `EncapsulatedBlock_Mapping` | ⚠️ approximate | `sysml:Block[isEncapsulated=true]` | 0 | 2 | 2 | isEncapsulated = true is kept as a comment and reported as having no v2 form, not written as metadata on the part def | +| 28 | 5: `InformationFlowConveyedFeatureMembership_Mapping`, `InformationFlowEndFeatureMembership_Mapping`, `InformationFlowEnd_Mapping`, `InformationFlowFeatureTyping_Mapping`, `InformationFlowSubclassification_Mapping` | ⚠️ approximate | `uml:InformationFlow` | 0 | 2 | 2 | an information flow realized by a connector is written as a flow usage `flow of Item from a to b` on that connector, not as a flow def (see candidate errata) | +| 29 | 1: `InformationFlow_Mapping` | ⚠️ approximate | `uml:InformationFlow` | 0 | 2 | 2 | an information flow realized by a connector is written as a flow usage `flow of Item from a to b` on that connector, not as a flow def (see candidate errata); candidate erratum: the mapping creates a FlowDefinition for a v1 information flow between specific sources and targets; a definition has no ends bound to those features, which a FlowUsage would have | +| 30 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 0 | 2 | a loop node is written as `action x { }` holding its body once; the loop test and iteration are not written | +| 31 | 7: `StakeholderMetadataFeatureMembership_Mapping`, `StakeholderMetadataFeatureTyping_Mapping`, `StakeholderMetadataOwningMembership_Mapping`, `StakeholderMetadataReferenceUsageFeatureValue_Mapping`, `StakeholderMetadataReferenceUsageRedefinition_Mapping`, `StakeholderMetadataReferenceUsage_Mapping`, `StakeholderMetadataUsage_Mapping` | ⚠️ approximate | `sysml:Stakeholder` | 0 | 2 | 2 | a «Stakeholder» class is written as a part def, which a viewpoint's `stakeholder` usage can type; its concern tag stays a comment (see candidate errata for the item def the mapping writes) | +| 32 | 1: `Stakeholder_Mapping` | ⚠️ approximate | `sysml:Stakeholder` | 0 | 2 | 2 | a «Stakeholder» class is written as a part def, which a viewpoint's `stakeholder` usage can type; its concern tag stays a comment (see candidate errata for the item def the mapping writes); candidate erratum: the mapping maps «Stakeholder» to an ItemDefinition, but a v2 stakeholder parameter is a PartUsage (StakeholderMembership::ownedStakeholderParameter), which requires a PartDefinition among its definitions; a PartDefinition is the type that can be used | +| 33 | 9: `DOADestroyActionUsageFeatureMembership_Mapping`, `DOADestroyActionUsageFeatureReferenceExpression_Mapping`, `DOADestroyActionUsageFeatureTyping_Mapping`, `DOADestroyActionUsageFeatureValue_Mapping`, `DOADestroyActionUsageMembership_Mapping`, `DOADestroyActionUsageReferenceUsage_Mapping`, `DOADestroyActionUsage_Mapping`, `DOADestroyFeatureMembership_Mapping`, `DestroyObjectAction_Mapping` | ❌ not implemented | `uml:DestroyObjectAction` | 0 | 1 | 1 | a DestroyObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 34 | 12: `AVVAFeatureTyping_Mapping`, `AVVAFeatureValue_Mapping`, `AVVAIsReplaceAllFeatureMembership_Mapping`, `AVVAIsReplaceAllRedefinition_Mapping`, `AVVAIsReplaceAllValue_Mapping`, `AVVAIsReplaceAll_Mapping`, `AVVAValueExpressionMembership_Mapping`, `AVVAValueFeatureReferenceExpression_Mapping`, `AVVAVariableFeatureMembership_Mapping`, `AVVAVariableRedefinition_Mapping`, `AVVAVariable_Mapping`, `AddVariableValueAction_Mapping` | ❌ not implemented | `uml:AddVariableValueAction` | 0 | 0 | 0 | a AddVariableValueAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 35 | 1: `AcceptCallAction_Mapping` | ❌ not implemented | `uml:AcceptCallAction` | 0 | 0 | 0 | a AcceptCallAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 36 | 1: `AcceptChangeStructuralFeatureEventAction_Mapping` | ❌ not implemented | `sysml:AcceptChangeStructuralFeatureEventAction` | 0 | 0 | 0 | an AcceptChangeStructuralFeatureEventAction is not migrated: activity.go writes it as an unmapped placeholder action | +| 37 | 1: `BroadcastSignalAction_Mapping` | ❌ not implemented | `uml:BroadcastSignalAction` | 0 | 0 | 0 | a BroadcastSignalAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 38 | 4: `CVAFeatureMembership_Mapping`, `CVAReferenceUsageFeatureValue_Mapping`, `CVAReferenceUsage_Mapping`, `ClearVariableAction_Mapping` | ❌ not implemented | `uml:ClearVariableAction` | 0 | 0 | 0 | a ClearVariableAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 39 | 2: `CentralBufferNode_Mapping`, `DataStoreNode_Mapping` | ⚠️ approximate | `uml:CentralBufferNode`
`uml:DataStoreNode` | 0 | 0 | 0 | an object node is written as an action that holds no tokens: the flows through it are written from its sources to its targets | +| 40 | 1: `ClearAssociationAction_Mapping` | ❌ not implemented | `uml:ClearAssociationAction` | 0 | 0 | 0 | a ClearAssociationAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 41 | 5: `CommonVariable_Mapping`, `VariableAttribute_Mapping`, `VariableFeatureTyping_Mapping`, `VariableItem_Mapping`, `VariableMembership_Mapping` | ❌ not implemented | `uml:Variable` | 0 | 0 | 0 | activity variables are not migrated: the variable actions that read or write them are placeholders | +| 42 | 8: `ConcernDocumentation_Mapping`, `ConcernOwningMembership_Mapping`, `ConcernStakeholderMembership_Mapping`, `ConcernStakeholderPartUsageFeatureTyping_Mapping`, `ConcernStakeholderPartUsageFeature_Mapping`, `ConcernStakeholderPartUsageOwningMembership_Mapping`, `ConcernStakeholderPartUsage_Mapping`, `Concern_Mapping` | ⚠️ approximate | `sysml:Concern` | 0 | 0 | 0 | a viewpoint's concern is written as an anonymous `frame concern` documented by the concern text; a «Concern» comment outside a viewpoint is a plain comment | +| 43 | 1: `CreateLinkAction_Mapping` | ❌ not implemented | `uml:CreateLinkAction` | 0 | 0 | 0 | a CreateLinkAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 44 | 1: `CreateLinkObjectAction_Mapping` | ❌ not implemented | `uml:CreateLinkObjectAction` | 0 | 0 | 0 | a CreateLinkObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 45 | 1: `DestroyLinkAction_Mapping` | ❌ not implemented | `uml:DestroyLinkAction` | 0 | 0 | 0 | a DestroyLinkAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 46 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | +| 47 | 1: `ElementImport_Mapping` | ❌ not implemented | `uml:ElementImport` | 0 | 0 | 0 | element imports are not written: names are resolved and written qualified instead | +| 48 | 3: `InformationItemFlowConveyedItemUsageFeatureTyping_Mapping`, `InformationItemFlowConveyedItemUsage_Mapping`, `InformationItem_Mapping` | ❌ not implemented | `uml:InformationItem` | 0 | 0 | 0 | no migrator code writes an InformationItem as an item def (it is only recognised as a classifier when resolving names) | +| 49 | 1: `OperationDirectedFeature_Mapping` | ❌ not implemented | `sysml:DirectedFeature` | 0 | 0 | 0 | «DirectedFeature» on operations is not read; the operation is written without a direction | +| 50 | 8: `PackageURIFeatureMembership_Mapping`, `PackageURIFeatureTyping_Mapping`, `PackageURIMetadataFeatureValue_Mapping`, `PackageURIMetadataMembership_Mapping`, `PackageURIMetadataReferenceUsage_Mapping`, `PackageURIMetadataUsage_Mapping`, `PackageURIRedefinition_Mapping`, `PackageURIValue_Mapping` | ❌ not implemented | `uml:Package[URI]` | 0 | 0 | 0 | a package's URI is not written as metadata | +| 51 | 7: `ParameterSetMembership_Mapping`, `ParameterSetParameterFeatureMembership_Mapping`, `ParameterSetParameterReferenceUsageFeatureValueExpression_Mapping`, `ParameterSetParameterReferenceUsageFeatureValue_Mapping`, `ParameterSetParameterReferenceUsageMembership_Mapping`, `ParameterSetParameterReferenceUsage_Mapping`, `ParameterSet_Mapping` | ❌ not implemented | `uml:ParameterSet` | 0 | 0 | 0 | parameter sets are not read by the migrator (no code refers to ParameterSet): the parameters are written, the set grouping them is not | +| 52 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | +| 53 | 1: `QualifierMembership_Mapping` | ❌ not implemented | `uml:Property[qualifier]` | 0 | 0 | 0 | association-end qualifiers are not read | +| 54 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 55 | 7: `RVAFeatureMembership_Mapping`, `RVAReferenceUsageExpressionMembership_Mapping`, `RVAReferenceUsageFeatureReferenceExpression_Mapping`, `RVAReferenceUsageFeatureTyping_Mapping`, `RVAReferenceUsageFeatureValue_Mapping`, `RVAReferenceUsage_Mapping`, `ReadVariableAction_Mapping` | ❌ not implemented | `uml:ReadVariableAction` | 0 | 0 | 0 | a ReadVariableAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 56 | 8: `RVVAFeatureTyping_Mapping`, `RVVAVariableExpressionMembership_Mapping`, `RVVAVariableFeatureMembership_Mapping`, `RVVAVariableFeatureReferenceExpression_Mapping`, `RVVAVariableFeatureValue_Mapping`, `RVVAVariableRedefinition_Mapping`, `RVVAVariable_Mapping`, `RemoveVariableValueAction_Mapping` | ❌ not implemented | `uml:RemoveVariableValueAction` | 0 | 0 | 0 | a RemoveVariableValueAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 57 | 1: `RaiseExceptionAction_Mapping` | ❌ not implemented | `uml:RaiseExceptionAction` | 0 | 0 | 0 | a RaiseExceptionAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 58 | 10: `RateMetadataUsageContinuousFeatureMembership_Mapping`, `RateMetadataUsageContinuousReferenceUsageRedefinition_Mapping`, `RateMetadataUsageContinuousReferenceUsage_Mapping`, `RateMetadataUsageDiscreteFeatureMembership_Mapping`, `RateMetadataUsageDiscreteReferenceUsageRedefinition_Mapping`, `RateMetadataUsageDiscreteReferenceUsage_Mapping`, `RateMetadataUsageFeatureTyping_Mapping`, `RateMetadataUsageFeatureValue_Mapping`, `RateMetadataUsage_Mapping`, `RateOwningMembership_Mapping` | ❌ not implemented | `sysml:Rate`
`sysml:Continuous`
`sysml:Discrete` | 0 | 0 | 0 | «Rate», «Continuous» and «Discrete» are not read by the migrator (no code refers to them): the flow is written without rate semantics or rate metadata | +| 59 | 1: `ReadLinkAction_Mapping` | ❌ not implemented | `uml:ReadLinkAction` | 0 | 0 | 0 | a ReadLinkAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 60 | 1: `ReadLinkObjectEndAction_Mapping` | ❌ not implemented | `uml:ReadLinkObjectEndAction` | 0 | 0 | 0 | a ReadLinkObjectEndAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 61 | 1: `ReadLinkObjectEndQualifierAction_Mapping` | ❌ not implemented | `uml:ReadLinkObjectEndQualifierAction` | 0 | 0 | 0 | a ReadLinkObjectEndQualifierAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 62 | 1: `ReclassifyObjectAction_Mapping` | ❌ not implemented | `uml:ReclassifyObjectAction` | 0 | 0 | 0 | a ReclassifyObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 63 | 1: `ReduceAction_Mapping` | ❌ not implemented | `uml:ReduceAction` | 0 | 0 | 0 | a ReduceAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 64 | 1: `RemoveStructuralFeatureValueAction_Mapping` | ❌ not implemented | `uml:RemoveStructuralFeatureValueAction` | 0 | 0 | 0 | a RemoveStructuralFeatureValueAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 65 | 1: `ReplyAction_Mapping` | ❌ not implemented | `uml:ReplyAction` | 0 | 0 | 0 | a ReplyAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 66 | 1: `SendObjectAction_Mapping` | ❌ not implemented | `uml:SendObjectAction` | 0 | 0 | 0 | a SendObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 67 | 1: `StartClassifierBehaviorAction_Mapping` | ❌ not implemented | `uml:StartClassifierBehaviorAction` | 0 | 0 | 0 | a StartClassifierBehaviorAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 68 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | +| 69 | 1: `UnmarshallAction_Mapping` | ❌ not implemented | `uml:UnmarshallAction` | 0 | 0 | 0 | a UnmarshallAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | diff --git a/tests/corpus/testdata/pssm_migration_expected.txt b/tests/corpus/testdata/pssm_migration_expected.txt index c4b6976143..8a69294dec 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 -9404 approximated -28652 mapped +9414 approximated +28713 mapped 6 skipped -9660 unmapped +9655 unmapped 13 validation-errors From fe4963340c278e772faaee4985b6f05c1b6bd783 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:45:25 +0000 Subject: [PATCH 07/19] fix(runtime): enter region stand-ins without completing composites Co-Authored-By: jason.han --- .../census-states-events.changed.md | 2 +- .../design/precise-semantics-alignment.md | 34 +++++++++++-------- docs/project/pssm-referee.md | 16 +++++---- docs/project/spec-compliance.md | 4 +-- .../robustness_parallel_wrapper_test.go | 17 +++++++--- internal/exec/runtime/state_region_entry.go | 3 -- ...res_inactive_parallel_region.expected.json | 2 +- ...starts_outer_region_inactive.expected.json | 2 +- ...teless_region_with_behaviors.expected.json | 2 +- ...llel_stateless_region_with_behaviors.sysml | 5 +-- ...ateless_region_with_behaviors.trace.golden | 8 +++++ internal/translate/migrate/states.go | 4 +-- tests/migrate/states_test.go | 12 +++---- 13 files changed, 67 insertions(+), 44 deletions(-) diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index 6e16fc5f4a..501576e6c3 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ - **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. -- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; a region without an entry remains inactive until a transition or fork branch enters it, and blocks its owner's completion; terminate pseudostates now terminate the whole state machine. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; default entry enters a region's state stand-in even without a start in its body, leaving no active child, while a region with no stand-in and no start remains inactive; either blocks owner completion until all regions are final. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/internals/design/precise-semantics-alignment.md b/docs/internals/design/precise-semantics-alignment.md index 6d102ec849..91ed16beac 100644 --- a/docs/internals/design/precise-semantics-alignment.md +++ b/docs/internals/design/precise-semantics-alignment.md @@ -1669,7 +1669,7 @@ which supersede the hand count this section was first written with — the moves | The UML `StateMachine` as the class under test (a standalone machine with attributes, operations, a constructor) | `part def` with `attribute`s, `action def`s and `exhibit state`, as an owned machine's: the reader (`reader.go`) reads the machine as the `Target` whose `Machine` is itself, with its attributes, operations and their methods, and its constructor | standard | | A guard whose behavior acts on the model (calls `trace(...)` before returning its value) | none: a v2 guard is a Boolean expression (§7.18.3, `validateTransitionFeatureMembershipGuardExpression`; `bool guard[*]` in `TransitionPerformances.kerml`, the effect a separate `step`), and an expression has no spelling for an action. UML 2.5.1 §14.5.11 `Transition::guard` itself calls such a guard ill formed. Recording the runtime's guard reads as the referee's observable instead is refused too: the reads the suite traces are a junction's on the target's default entry, read at the incoming transition's selection, where the library orders every transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | no translation | | A guard whose behavior is an opaque behavior, not an activity | none: the reader follows an activity's nodes to tell whether the behavior acts, and does not read an opaque body, so the guard is refused rather than carried as its Boolean text alone. A `FunctionBehavior` is the exception — it accesses no object by UML's contract (§13.2.3.3) — and is translated as the expression it spells | no translation | -| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a branch enters its target without an `entry; then`, while a region omitted by default entry remains inactive if it has no entry transition (finding 6 below, fixed) | extension | +| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a branch enters its target without an `entry; then`. Default entry enters a region's state stand-in even without a start in its body; the stand-in has no active child. A region with no stand-in and no start remains inactive and blocks owner completion (finding 6 below, fixed) | extension | The classification is by construct, in the order of the table: a test whose model uses any construct with no spelling or no translation is counted as not expressible whatever else it @@ -1736,17 +1736,19 @@ is where a later translation moves them back. | *Event 019-E* | standard | *translated since the emitter binds entry parameters and returns outputs:* `T2` accepts `'or'(left, right)` and stores both in `trigger_v_or_left`, `trigger_v_or_right`; the two regions' substate entries declare `in left = trigger_v_or_left; in right = trigger_v_or_right;` and assign the operation's two outputs (`out result`, `out 'return'`), each region's entry writing them in the order the regions are entered, so the caller receives the second region's values and the two admitted traces are both reached and nothing else | | *Deferred 007* | extension | *translated since the emitter spells a returning effect with inputs:* the deferred `op(p1)` call fires `T4` once the second state is active; `T4`'s effect is `action def T4_effect` with `in p` bound to the accept's `p1` and `out 'return'`, its `return` an assignment of `not p`; the one admitted trace, the first state's exit, `T3(effect)`, `T4(effect)[in=true][out=false]` and the tester's `[out=false]`, is reached | | *Standalone 003* | standard | *the standalone machine is read as the target since the reader does so, and translated since the emitter binds entry parameters and returns outputs:* the same shape as *Event 019-E*, the machine itself the class under test with `or` its operation; both admitted traces are reached | -| *Fork 002* | extension | *translated:* the fork enters two regions without initial pseudostates; a no-entry region stays inactive on default entry and is activated by the branch | -| *Join 001* | extension | *translated:* the fork enters two regions without initial pseudostates; omitted no-entry regions stay inactive and block owner completion | +| *Fork 002* | extension | *translated:* the fork enters two regions without initial pseudostates; default entry enters their state stand-ins with no active child, and the fork activates its named targets | +| *Join 001* | extension | *translated:* the fork enters two regions without initial pseudostates; an omitted region's stand-in is entered with no active child, which blocks owner completion | | *Choice 005* | extension | *refused, settled:* the guards of the junction's and the choice's four outgoing transitions each call `trace("T1.n(guard)")` and the admitted trace records the calls, to show when each guard is read; a v2 guard is an expression with no room for an action, so the translation keeps only the guard's value and cannot reach the trace, and is refused rather than run short. Making the runtime's guard reads the referee's observable would not reach the trace either: the junction sits on the composite's default entry and the suite reads its guards before `T2(effect)` and the composite's entry, where the library reads a transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | The last two were kept apart from the other seven and from the 29 with no spelling: UML allows a fork to target states inside orthogonal regions that have no initial pseudostate, and SysML -v2 `parallel` regions can spell the shape. A composite-state region without an entry remains -inactive on default entry, whether or not a fork can enter it; the owner cannot complete until -all regions reach final states. Explicit branches and transitions can enter the region, and -the PSSM classifier no longer records the former lowerer refusal. Entry 002 E remains -not-expressible on its entry-point construct. +v2 `parallel` regions can spell the shape. Default entry enters a region's state stand-in even +without an entry transition; if the stand-in has no start in its body, it remains active with +no active child. A region with no stand-in and no start remains inactive. Both an unstarted +stand-in and an inactive region block owner completion until all regions reach final states. +Explicit branches and transitions can enter an inactive region, and the PSSM classifier no +longer records the former lowerer refusal. Entry 002 E remains not-expressible on its +entry-point construct. #### Behavior parameters, operation results, tester traces and standalone machines @@ -2327,14 +2329,16 @@ sites of the runtime's fixed, the pool's order and the do step drawn on the entr of them has to change (second open decision). `state_choice_pseudostate` does not reach it. *Fixed* with the second open decision: the code now matches the note (SM30's *Decided* sentence), and Track E records the finding as landed. -6. **A no-entry region of a composite state remains inactive until explicitly entered.** +6. **An unstarted stand-in does not complete its composite-state region.** UML permits a region without an initial pseudostate, and SysML v2 `parallel` can represent - it. `recordRegionInitials` records no default start for such composite-state regions; - `regionStart` returns no state and `enterRegion` leaves the region absent from the active - configuration. This applies whether or not a fork elsewhere can enter the region. A fork - branch or later transition naming a vertex activates it normally. `regionComplete` treats a - missing region state as incomplete, so the owner cannot complete until every region reaches - a final state. The rule is covered by `TestRuntimeRobustnessInactiveRegion`, + it. When `RegionState[region]` supplies a stand-in, `regionStart` enters that state even + without a start transition in its body; no child state becomes active. When no stand-in and + no start exist, `regionStart` returns no state and the region remains inactive. In either + case, `regionComplete` and `stateComplete` keep the owning composite from completing until + every region reaches a final state. A later transition or fork branch can enter an inactive + region. Coverage includes `TestRuntimeRobustnessInactiveRegion`, + `TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, + `state_parallel_stateless_region_with_behaviors`, `state_parallel_region_without_entry_inactive`, `state_fork_only_region_entered_by_default`, `state_nested_fork_starts_outer_region_inactive` and `state_history_restores_inactive_parallel_region`. The PSSM classifier no longer reports diff --git a/docs/project/pssm-referee.md b/docs/project/pssm-referee.md index 38f071915f..d53caf522e 100644 --- a/docs/project/pssm-referee.md +++ b/docs/project/pssm-referee.md @@ -133,8 +133,9 @@ under [Behavior parameters, operation results, tester traces and standalone mach [candidate table](../internals/design/precise-semantics-alignment.md#a-guard-whose-behavior-acts-on-the-model) records the three candidates: the settled refusal of a construct v2 cannot spell faithfully. - **A fork into orthogonal regions that have no initial pseudostate**: *Fork 002*, *Join 001* - — translated with the general inactive-region rule: regions without an entry remain - inactive on default entry, and fork branches activate the regions they name; see + — translated with the region-entry rule: a region's state stand-in is entered on default + entry even when its body has no start, leaving no active child; a region without a stand-in + and without a start remains inactive, while fork branches enter their named targets; see [Findings about our own conformance](#findings-about-our-own-conformance). ## Translating @@ -834,12 +835,15 @@ By reason, as the classifier names them: exit left, only by completion transitions or by paths accepting different events). - A standalone state machine is read as the target class, and a tester's `trace(...)` after a call is driven, so neither is a reason any longer; Event 019 A runs and passes. -- A composite-state region without an entry is valid: default entry leaves it inactive until - a transition or fork branch names one of its states, and its owner cannot complete while - that region is inactive. Entry 002 E remains not-expressible on its entry-point construct; +- A composite-state region without an entry is valid: default entry enters its state stand-in; + when the stand-in has no start in its body, it is active with no active child. A region with + no stand-in and no start remains inactive, and either condition blocks owner completion until + all regions are final. A later transition or fork branch may activate an inactive region. + Entry 002 E remains not-expressible on its entry-point construct; the no-entry refusal is no longer a classifier reason. Coverage includes `TestRuntimeRobustnessInactiveRegion`, `state_parallel_region_without_entry_inactive`, - `state_fork_only_region_entered_by_default` and + `state_parallel_stateless_region_with_behaviors`, `state_fork_only_region_entered_by_default`, + `state_nested_fork_starts_outer_region_inactive` and `state_history_restores_inactive_parallel_region`. - **guard side effect** (no translation, settled): Choice 005. Making the runtime's guard reads the referee's observable instead was tried and refused: the suite reads the junction's diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 8a28f7ef36..7720cb9e6b 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -847,7 +847,7 @@ nothing above. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| -| A composite-state region with no entry transition stays inactive on default entry; its owner cannot complete until every region is final, and a later transition or fork branch may activate it | `lower/state_graph.go` `recordRegionInitials`; `state_region_entry.go` `regionStart`, `enterRegion`, `enterLazily`, `forkEntry`, `enterForkBranches`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `TestToStateGraph_ForkOmissionLeavesRegionInactive`, `TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`; `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens), `state_transition_cross_region_nested_target.sysml` | ✅ Faithful | +| A region represented by a sub-state enters that stand-in on default entry; if the stand-in has no start in its body, it is active with no active child. A region with no stand-in and no start remains inactive; an incomplete region blocks its composite owner's completion, while a later transition or fork branch may activate an inactive region | `lower/state_graph.go` `recordRegionInitials`; `state_region_entry.go` `regionStart`, `enterRegion`, `enterLazily`, `forkEntry`, `enterForkBranches`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `TestToStateGraph_ForkOmissionLeavesRegionInactive`, `TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens), `state_transition_cross_region_nested_target.sysml` | ✅ Faithful | | A named succession or transition endpoint that resolves to a non-vertex or non-action node is reported during analysis; positional, implied, unresolved and flow ends remain unchanged | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go`; `passes/action_endpoint.go` `ActionEndpointPass`; `lower/action_nodes.go` `ActionNodes` and `ActionEndpointAccepted`, including lazy inherited action-node collection | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestActionEndpointPassReportsResolvedNonNodes`, `:TestActionEndpointPassAcceptsLoweredNodes`, `:TestActionEndpointPassDiagnosticAgreesWithLowering`, `:TestActionEndpointPassInheritedNodes`, `:TestActionEndpointPassIgnoresFeatureChainsThroughNonNodes`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`; `lower/action_inherited_test.go`; `runtime/testdata/conformance/action_inherited_endpoint.sysml` | ⚠️ Approximate (a feature chain rooted in a part usage is legal notation and stays silent during validation, but execution still reports the existing lowering error because invoking an action through that chain is not implemented) | | Initial state identification | `lower/state_graph.go:ToStateGraph`; `state_executor.go:686` initialize | `state_simple.sysml` | ✅ Faithful | | A succession out of a state body's own entry subaction names the state it starts in (`entry; then off;`), which is how a machine designates where it starts | `lower/entry_transition.go` `addEntryTransition`, `UnconditionalStart`; `lower/state_graph.go` collectTransitions SuccessionEdge case + `isEntrySubaction` | `lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState`, `lower/transition_source_test.go:TestToStateGraph_UnguardedEntryTransitionIsInitial`, `state_entry_succession_initial.sysml` conformance, `tests/parser/testdata/parse/behavior_exhibit_state_body.golden` | ✅ Faithful | @@ -915,7 +915,7 @@ nothing above. | A succession (`succession first s then a;`) names its endpoints the way a transition does, so it reaches a nested, region-local or qualified vertex, and a pseudostate as well as a state | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go` route succession ends; `passes/state_transition.go`; `lower/state_graph.go` `collectTransitions` (UsageSuccession and SuccessionEdge cases) → `(*StateGraph).endpointVertex`/`endpointState`, over the same `lower.EndpointResolver` a transition's endpoints resolve through | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`, `lower/state_graph_nested_test.go:TestSuccessionReachesAPseudostate`, `:TestSuccessionQualifiedTargetNamesTheVertexItQualifies`, `:TestToStateGraph_EntrySuccessionNamesInitialState` | ✅ Faithful (previously matched the endpoint's last name segment against a flat state list, which reached no pseudostate and could bind a same-named vertex of another state) | | Two regions may declare same-named pseudostates, and each is a vertex of its own | `lower/state_graph.go` `StateGraph.Pseudostates` (declaration-ordered slice, not keyed by name), `addPseudostate` | `lower/state_graph_nested_test.go:TestSameNamedPseudostatesInSiblingRegionsAreBothCollected` | ✅ Faithful | | Junction pseudostates: the route through a junction is resolved statically before the incoming effect; every outgoing guard is read against the current data, several enabled branches are a recorded transition choice, and an unguarded branch is the default when none of the guards holds. The settled route continues through further junctions but stays open at the first choice; a history default is resolved the same way. If a route has no way through, its compound transition is not enabled. The occurrence may go to another enabled transition, be deferred or be discarded as unmatched; a completion with no route is dropped. For a join, selection checks the way out using only the candidate and prior arrivals. Signal/change dispatches count same-occurrence peers only when their regions chose them; firing checks completion against those peers, and a dead route fires none. Timer expiries are separate occurrences even when due together; a dead completing segment is disabled, leaving its source's timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). A history default route disables the transition only when its source is outside the history owner. Only `errNoWayThrough` disables: cycles, unevaluable guards and binding failures still fail as the transition fires. Dead branches whose continuation has no way through are filtered from a junction draw; one surviving branch is followed without a draw. Choice remains dynamic, and a dead junction reached beyond a choice remains a run error. UML 2.5.1 §14.2.3.8.1 (compound transition) and §14.2.3.7 (junction) are the extension's references. KerML `ControlPerformances::DecisionPerformance` declares `feature outgoingHBLink: HappensBefore[1]`; it does not specify no-way-through or unmatched-event behavior | `state_route.go` `resolveRoute`, `routeAvailable`, `followOut`, `enabledBranches`, `junctionDraw`, `travel`, `travelResolving`, `settleDraws`, `pickBranch`, `errNoWayThrough`; `state_executor.go` transition enablement and history-default routing | `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`, `state_junction_no_way_through_unmatched.sysml`, `state_junction_no_way_through_other_transition_fires.sysml`, `state_junction_no_way_through_deferred.sysml`, `state_junction_dead_branch_not_drawn.sysml`, `state_completion_no_way_through_dropped.sysml`, `state_history_default_no_way_through.sysml`, `state_history_self_transition_default_no_way_through_restores.sysml`, `state_join_no_way_out_disables_last_segment.sysml`; `explore_test.go:TestExploreStaticJunctionBranches`, `:TestExploreJunctionDrawnAsTransitionFires`, `:TestExploreHistoryDefaultThroughJunction`; `replay_test.go:TestReplayRefusedChoiceUndoesTheJunctionDrawBeforeIt`, `:TestReplayRefusedChoiceUndoesTheHistoryDefaultsJunctionDraw`; `robustness_junction_exit_route_test.go:TestRuntimeRobustnessJunctionExitRoute`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead junction beyond choice, cycle, unevaluable guard, join exit failure); `robustness_test.go:region_pseudostate_without_satisfied_guard` | ✅ Faithful | -| Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork starts at its default only when it has one; otherwise it remains inactive. Default entry into a composite likewise leaves any no-entry region inactive, regardless of whether another fork can enter it. A composite completes only after every region reaches a final state; an inactive region blocks completion. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `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`, `regionStart`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`; `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | +| Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork follows default entry: its stand-in is entered even without a start, leaving no active child; when no stand-in and no start exist, the region remains inactive. Default entry follows the same rule. A composite completes only after every region reaches a final state; a stand-in with no active child or an inactive region blocks completion. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region with a start does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `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`, `regionStart`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | | Join pseudostates: each occurrence fires every not-yet-arrived segment it enables and whose region's dispatch chose it, exiting each source, running its effect and recording each in `joinArrived` only after the firing succeeds; when multiple segments fire, they are drawn in `join ` order. At selection, including for each time expiry, probes count only the candidate and prior arrivals, checking the way out only if they complete the join. In signal/change dispatches, same-occurrence peers count only when their regions chose them; firing checks completion against those peers, and if the route is dead, none fire. Timer expiries are separate occurrences even at the same instant; a dead completing segment is disabled, leaving its source's other timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). The join completes when prior arrivals plus segments enabled by the current occurrence and chosen by their regions in that dispatch cover every incoming segment and the outgoing route is available. The current segments are drawn as `join `, arrivals clear and the owner is left after the last segment effect. Completion segments fire only on their own completion; waiting regions do not complete their owner; arrivals clear on region re-entry or when another transition leaves the owner. Snapshots, held images and canonical check state preserve arrivals. PSSM leaves the owner before the final incoming effect; leaving after that effect is the extension's reading recorded in SM34. `TransitionPerformances.kerml` says `private succession [1] transitionLinkSource then [*] effect;`; `StatePerformances.kerml` says `and hence happening during the state performance.` Neither supplies a cross-segment arrival rule; that is the UML/PSSM extension reference | `state_route.go` `joinFiringNow`; `state_executor.go` `fireJoinTransition`, `fireJoinSegment`, `fireJoinIncoming`, `leaveJoinOwner`, `regionWaitingAtJoin`, `setRegionState`, `clearJoinArrivalsOwnedBy`; `snapshot.go`, `held_image_behavior.go`, `check_state.go` preserve `joinArrived` | Conformance fixtures: `state_fork_join_pseudostate`, `state_join_arrival_dropped_when_owner_exited`, `state_join_change_segment_rises_alone`, `state_join_change_segment_waits_before_junction`, `state_join_change_segment_waits_before_junction_metadata`, `state_join_change_segments_rise_together`, `state_join_completion_is_not_a_timers_expiry`, `state_join_completion_segment_waits_for_do_behavior`, `state_join_dead_timer_join_keeps_group_alternative`, `state_join_exit_from_regions_before_owner_exit`, `state_join_from_composite_sources`, `state_join_from_nested_states`, `state_join_no_way_out_disables_last_segment`, `state_join_of_machine_regions_from_nested_source`, `state_join_runs_every_incoming_effect`, `state_join_segment_fires_on_own_signal`, `state_join_segments_arrive_together_on_one_occurrence`, `state_join_segment_not_chosen_does_not_arrive`, `state_join_peer_not_chosen_does_not_block_arrival`, `state_join_segment_reads_its_payload`, `state_join_segment_trigger_unmatched`, `state_join_time_segment_needs_same_occurrence`, `state_join_time_segment_not_taken_by_signal`, `state_join_time_segment_unsynchronized_reads_no_route`, `state_join_time_segment_unsynchronized_reads_no_route_metadata`, `state_join_time_segments_expire_apart`, `state_join_time_segments_expire_together`, `state_join_waits_for_every_segment_enabled`; tests: `pseudostate_test.go:TestForkAndJoinPseudostates`, `:TestJoinWaitsForEveryBranch`, `:TestJoinSegmentFiresOnOwnOccurrence`, `:TestJoinDisarmedSegmentCanArriveAlone`, `:TestJoinFromActiveCompositeSourcesFires`, `:TestJoinCompletionSegmentWaitsForItsSourcesDoBehavior`, `:TestJoinCompletionSegmentWaitsForItsSourcesBody`; `fork_join_test.go:TestForkJoinVisitOrderIsDeterministic`; `choice_test.go:TestCompletionTransitionArrivesAtUnreadyJoin`; `state_change_trigger_test.go:TestChangeTriggerDisarmedSegmentCanWaitForItsPeer`; `join_snapshot_test.go:TestSnapshotRestoresJoinArrivals`, `:TestCloneNilJoinArrivalsIsWritable`; `check_test.go:TestCheckJoinWaitsForSlowestBranch`; `replay_test.go:TestReplayRefusedJoinDrawChangesNothing`, `:TestReplayRefusedJoinDrawUndoesTheSegmentsChoice`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead signal and timer join exits, exit failure) | ✅ 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 ;`) and `lower/state_metadata.go` (`#shallowHistory state`/`#deepHistory state`); `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 | diff --git a/internal/exec/runtime/robustness_parallel_wrapper_test.go b/internal/exec/runtime/robustness_parallel_wrapper_test.go index d272094924..1af21c0770 100644 --- a/internal/exec/runtime/robustness_parallel_wrapper_test.go +++ b/internal/exec/runtime/robustness_parallel_wrapper_test.go @@ -1,6 +1,7 @@ package runtime import ( + "slices" "strings" "testing" @@ -8,21 +9,29 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" ) -func TestRuntimeRobustnessParallelWrapperLeavesRegionInactive(t *testing.T) { - err := stateRunErrorForSource(t, "Machine", ` +func TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive(t *testing.T) { + _, visits, err := executeStateSource(t, "Machine", ` package test { - state def Machine { + state Machine { entry; then S; state S parallel { state R { state A; } } + state finished; + transition first S then finished; } } `) if err != nil { - t.Fatalf("a wrapped region without an entry should remain inactive, not fail: %v", err) + t.Fatalf("execute parallel state with an unstarted stand-in: %v", err) + } + if slices.Contains(visits, "A") { + t.Errorf("the unstarted body state A was entered: %v", visits) + } + if slices.Contains(visits, "finished") { + t.Errorf("S completed while its stand-in body was unstarted: %v", visits) } } diff --git a/internal/exec/runtime/state_region_entry.go b/internal/exec/runtime/state_region_entry.go index e93b72f2af..d9302e58bd 100644 --- a/internal/exec/runtime/state_region_entry.go +++ b/internal/exec/runtime/state_region_entry.go @@ -175,9 +175,6 @@ func (e *StateExecutor) regionStart(w *regionEntry) (*ast.StateNode, error) { return w.target, nil } if owner := e.graph.RegionState[w.region]; owner != nil { - if w.container != nil && len(e.graph.StartOf(w.region)) == 0 { - return nil, nil - } return owner, nil } entry, err := e.startIn(w.region) diff --git a/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json index 88976b8e78..04b772411c 100644 --- a/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_history_restores_inactive_parallel_region.expected.json @@ -6,7 +6,7 @@ {"signal": "Leave"}, {"signal": "ReturnDeep"} ], - "finalState": "active", + "finalState": "active+b", "outputs": { "log": {"type": "String", "value": "A(entry) A(exit) A(entry) A(exit) A(entry) "} }, diff --git a/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json index 933eeb8a8f..8bddc56370 100644 --- a/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_nested_fork_starts_outer_region_inactive.expected.json @@ -1,6 +1,6 @@ { "type": "state", - "finalState": "a+inner+b", + "finalState": "a+inner+o2+b", "outputs": { "log": {"type": "String", "value": ""} }, diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json index 83c1b7c621..cdfa51b2e5 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.expected.json @@ -6,7 +6,7 @@ ], "finalState": "done", "outputs": { - "log": {"type": "Integer", "value": 0} + "log": {"type": "Integer", "value": 12} }, "trace": true } diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml index b64fc44c5d..c1095dfb87 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.sysml @@ -1,5 +1,6 @@ -// A region without an entry transition remains inactive, including when its -// only state declares entry and exit behaviors. +// A region of a parallel state stood for by a state declaring entry and exit +// behaviors, but no substates, needs no initial: it starts in that state, runs +// its entry, and runs its exit when the parallel state is left. package test { attribute def Go; attribute def Stop; diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden index e434580975..63f78db798 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stateless_region_with_behaviors.trace.golden @@ -1,7 +1,15 @@ exit: wstart enter: done transition: wstart -> done (event: accept Go) +choice exiting busy: next done(exit), idle(exit) (unordered; took done(exit) first) exit: done +stmt action body + stmt assign log + eval feature log -> 1 + eval literal 10 -> 10 + eval operator * -> 10 + eval literal 2 -> 2 + eval operator + -> 12 exit: busy enter: done transition: busy -> done (event: accept Stop) diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index 1cc02ecee5..d5193e889e 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -353,10 +353,10 @@ func (m *migration) entryPointForm(v, owner *sysmlv1.Element) pointForm { 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" withoutInitial, withoutEntry := m.regionsWithoutEntry(m.populatedRegions(owner)) if len(withoutInitial) > 0 { - note += "; no initial pseudostate starts the " + pluralRegion(len(withoutInitial)) + " " + strings.Join(withoutInitial, ", ") + ", which v1 too leaves inactive on entering the state" + note += "; no initial pseudostate starts the state stand-in for " + pluralRegion(len(withoutInitial)) + " " + strings.Join(withoutInitial, ", ") + ", so its body has no active substate, as in v1" } if len(withoutEntry) > 0 { - note += "; no initial pseudostate enters the " + pluralRegion(len(withoutEntry)) + " " + strings.Join(withoutEntry, ", ") + ", which v1 too leaves inactive on entering the state" + note += "; no initial pseudostate starts the state stand-in for " + pluralRegion(len(withoutEntry)) + " " + strings.Join(withoutEntry, ", ") + ", so its body has no active substate, as in v1" } return pointForm{defaultEntry: true, note: note} } diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index 1ebcdb1d45..96083bd998 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -1337,9 +1337,9 @@ 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. +// An entry point no transition leaves enters its state by default. With no initial +// pseudostate in the region, the wrapper stand-in is active but its body has no active +// substate; 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{ @@ -1354,15 +1354,15 @@ func TestDefaultEntryPointOnOwnerWithoutInitialEntersTheState(t *testing.T) { if strings.Contains(string(r.Notation), "#StateMachines::junction state 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, "_dIn", migrate.Mapped, "no initial pseudostate starts the state stand-in for region 'r', so its body has no active substate, as in v1") 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) + if out := meta(t, s, "%current"); !strings.Contains(out, "Current state: r") || strings.Contains(out, "W1") { + t.Errorf("entering Work by its default entry did not leave the stand-in's body inactive:\n%s", out) } meta(t, s, "%send Stop") meta(t, s, "%step") From 0f39297345fa4f8312548384631da772d64d4ac8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:14:30 +0000 Subject: [PATCH 08/19] fix(runtime): keep a composite without an active substate from completing Co-Authored-By: jason.han --- .../census-states-events.changed.md | 2 +- docs/project/spec-compliance.md | 1 + ...robustness_composite_without_entry_test.go | 31 +++++++++++++++++++ internal/exec/runtime/state_executor.go | 17 ++++++++-- ...hout_entry_does_not_complete.expected.json | 14 +++++++++ ...site_without_entry_does_not_complete.sysml | 21 +++++++++++++ ...thout_entry_does_not_complete.trace.golden | 27 ++++++++++++++++ 7 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 internal/exec/runtime/robustness_composite_without_entry_test.go create mode 100644 internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index 501576e6c3..49b7d5d1ef 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ - **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. -- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; default entry enters a region's state stand-in even without a start in its body, leaving no active child, while a region with no stand-in and no start remains inactive; either blocks owner completion until all regions are final. Terminate pseudostates now terminate the whole state machine. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; default entry enters a region's state stand-in even without a start in its body, leaving no active child, while a region with no stand-in and no start remains inactive; either blocks owner completion until all regions are final. A composite no longer completes immediately on entry when it declares substates but none is entered. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 560edc6f14..02da106272 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1004,6 +1004,7 @@ nothing above. | A transition's `do` effect written as a statement is terminated by the transition's own `;` (SysML.xtext `TransitionUsage` ends with `ActionBody`, while `EffectBehaviorUsage` carries no `;`), in the standard `first … then` spelling | `parser/behavior.go` `expectStatementEnd`/`atTransitionEffectStatement`/`atEffectEnd`; `parser/defusage.go` `parseUsage`/`parseReferenceMemberUsage` | `parse/state_transition_effect_statement.golden`, `conformance/state_transition_effect_assign.sysml`, `conformance/state_transition_effect_assign_first_then.sysml` (with their `.trace.golden`), `negative_test.go:transition_effect_perform_two_semicolons`, `:transition_effect_assign_two_semicolons`, `:transition_effect_no_semicolon`, `:body_assignment_no_semicolon` | ✅ Faithful (a second `;` is an error, as `ActionBody` takes one terminator; a statement outside a transition effect still needs its own `;`) | | Bodied `exhibit state` (`exhibit state spacecraftModes { … }`, SysML.xtext `ExhibitStateUsage`) | `parser/behavior.go` (the exhibited state's body is parsed as a state body); `ast/dump.go` (the `exhibit state` keyword is recorded) | `parse/behavior_exhibit_state_body.golden`, `parse/classifier_behaviors.golden`, `negative_test.go:exhibit_state_unclosed_body` | ✅ Faithful (the body parses, resolves and lowers as a state machine, and an object of the type runs it — see the Classifier Behaviors map) | | A state's `entry`, `do` or `exit` behavior written as a reference through a feature chain (`exit controller.coolDown;`) performs the chain's last action on the object the chain reaches from the machine's own performer, so the behavior's `this` is that object and its writes land there, not on the machine | `lower/state_behavior.go` (a `FeatureChainExpr` reference is kept as the behavior's target); `runtime/state_statements.go` (the chain is evaluated against the exhibiting object; the value must be one live object) ; `runtime/invoke_action.go` `performerOf`/`beginCallee` | conformance `state_subaction_on_part` (the part's `cooled` is set, the machine's own `cooled` stays `false`) | ✅ Faithful, **self-assessed** (no external referee executes state behaviors) | +| On entry, a state that declares substates and has no active substate does not immediately complete; its completion transition waits rather than leaving before a nested state is entered | `runtime/state_executor.go` `declaresSubstates`, `completesAtEntry` | `conformance/state_composite_without_entry_does_not_complete.sysml` + `.expected.json` + trace golden; `robustness_composite_without_entry_test.go:TestRuntimeRobustnessCompositeWithoutEntry` | ✅ Faithful | ### Classifier Behaviors (KerML §8.4.4.3 Behaviors / `performances`; SysML v2 §7.16 `exhibit`/`perform`) diff --git a/internal/exec/runtime/robustness_composite_without_entry_test.go b/internal/exec/runtime/robustness_composite_without_entry_test.go new file mode 100644 index 0000000000..265ea5686d --- /dev/null +++ b/internal/exec/runtime/robustness_composite_without_entry_test.go @@ -0,0 +1,31 @@ +package runtime + +import ( + "slices" + "testing" +) + +func TestRuntimeRobustnessCompositeWithoutEntry(t *testing.T) { + _, visits, err := executeStateSource(t, "Machine", `package test { + state Machine { + entry; then Work; + state Work { + state W1; + } + state X; + transition first Work then X; + } + }`) + if err != nil { + t.Fatalf("execute composite without an initial substate: %v", err) + } + if !slices.Contains(visits, "Work") { + t.Errorf("Work was not entered: %v", visits) + } + if slices.Contains(visits, "W1") { + t.Errorf("unstarted substate W1 was entered: %v", visits) + } + if slices.Contains(visits, "X") { + t.Errorf("Work completed without an active substate: %v", visits) + } +} diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index db4effa1cc..e65423735b 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -798,10 +798,9 @@ func (e *StateExecutor) scheduleCompletionTransitions(state *ast.StateNode) erro return nil } -// completesAtEntry reports whether entering state as the end of an entry path queues -// its completion at once: it has a completion transition and nothing below to enter. +// completesAtEntry reports whether entry queues completion for a bodyless state without substates. func (e *StateExecutor) completesAtEntry(state *ast.StateNode) bool { - return !e.hasBody(state) && completionCount(e.graph.Transitions[state]) > 0 + return !e.hasBody(state) && !e.declaresSubstates(state) && completionCount(e.graph.Transitions[state]) > 0 } // hasBody reports whether state has substates to enter: orthogonal regions or a start. @@ -810,6 +809,18 @@ func (e *StateExecutor) hasBody(state *ast.StateNode) bool { return orthogonal || len(e.graph.StartOf(state)) > 0 } +func (e *StateExecutor) declaresSubstates(state *ast.StateNode) bool { + if state == nil { + return false + } + for _, child := range e.graph.States { + if e.graph.ParentState[child] == state && !e.graph.Completes(child) { + return true + } + } + return false +} + // scheduleTimeTransitions queues a time event per time-triggered transition out // of the state whose timer is not running yet, due at the clock's instant. func (e *StateExecutor) scheduleTimeTransitions(state *ast.StateNode) error { diff --git a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json new file mode 100644 index 0000000000..6dfe8cedb6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json @@ -0,0 +1,14 @@ +{ + "type": "state", + "events": [ + {"signal": "Go"}, + {"signal": "Back"}, + {"signal": "Direct"}, + {"signal": "Finish"} + ], + "finalState": "X", + "outputs": { + "log": {"type": "String", "value": "W1(entry) X(entry) "} + }, + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml new file mode 100644 index 0000000000..528ff67519 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml @@ -0,0 +1,21 @@ +package CompositeWithoutEntry { + item def Go; + item def Back; + item def Direct; + item def Finish; + + state Machine { + attribute log : String = ""; + entry; then idle; + state idle; + state Work { + state W1 { entry { assign log := log + "W1(entry) "; } } + transition first W1 accept Finish then done; + } + state X { entry { assign log := log + "X(entry) "; } } + transition first idle accept Go then Work; + transition first Work accept Back then idle; + transition first idle accept Direct then W1; + transition first Work then X; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden new file mode 100644 index 0000000000..382608d7f4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden @@ -0,0 +1,27 @@ +exit: idle +enter: Work +transition: idle -> Work (event: accept Go) +exit: Work +enter: idle +transition: Work -> idle (event: accept Back) +exit: idle +enter: Work +enter: W1 (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "W1(entry) " -> "W1(entry) " + eval operator + -> "W1(entry) " +transition: idle -> W1 (event: accept Direct) +exit: W1 +enter: done +transition: W1 -> done (event: accept Finish) +exit: done +exit: Work +enter: X (entry action) +stmt action body + stmt assign log + eval feature log -> "W1(entry) " + eval literal "X(entry) " -> "X(entry) " + eval operator + -> "W1(entry) X(entry) " +transition: done -> X From 33ccd9c3f3245ed6ed7574921fdb4e6d4be5673f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:14:54 +0000 Subject: [PATCH 09/19] test(parser): add AST golden for composite completion fixture Co-Authored-By: jason.han --- ...ite_without_entry_does_not_complete.golden | 52 +++++++++++++++++++ ...site_without_entry_does_not_complete.sysml | 21 ++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden create mode 100644 tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml diff --git a/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden b/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden new file mode 100644 index 0000000000..4c88f44b04 --- /dev/null +++ b/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden @@ -0,0 +1,52 @@ +(RootNamespace + (Membership visibility="default" + (Package name="CompositeWithoutEntry" library=false standard=false + (Membership visibility="default" + (Definition kind="item" abstract=false variation=false name="Go")) + (Membership visibility="default" + (Definition kind="item" abstract=false variation=false name="Back")) + (Membership visibility="default" + (Definition kind="item" abstract=false variation=false name="Direct")) + (Membership visibility="default" + (Definition kind="item" abstract=false variation=false name="Finish")) + (Membership visibility="default" + (Usage kind="state" name="Machine" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Membership visibility="default" + (Usage kind="attribute" name="log" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=String + (*ast.QualifiedName)) + (LiteralString value="\"\""))) + (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 + (Membership visibility="default" + (Usage kind="state" name="W1" 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" + (*ast.AssignmentActionNode)))))) + (TransitionMember source="W1" target="done" + (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Finish + (*ast.QualifiedName)))))) + (Membership visibility="default" + (Usage kind="state" name="X" 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" + (*ast.AssignmentActionNode)))))) + (TransitionMember source="idle" target="Work" + (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Go + (*ast.QualifiedName)))) + (TransitionMember source="Work" target="idle" + (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Back + (*ast.QualifiedName)))) + (TransitionMember source="idle" target="W1" + (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Direct + (*ast.QualifiedName)))) + (TransitionMember source="Work" target="X")))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml b/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml new file mode 100644 index 0000000000..528ff67519 --- /dev/null +++ b/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml @@ -0,0 +1,21 @@ +package CompositeWithoutEntry { + item def Go; + item def Back; + item def Direct; + item def Finish; + + state Machine { + attribute log : String = ""; + entry; then idle; + state idle; + state Work { + state W1 { entry { assign log := log + "W1(entry) "; } } + transition first W1 accept Finish then done; + } + state X { entry { assign log := log + "X(entry) "; } } + transition first idle accept Go then Work; + transition first Work accept Back then idle; + transition first idle accept Direct then W1; + transition first Work then X; + } +} From 494cce636aef346a064c6f6ad3cbcc4a1dc7a7f0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:54:39 +0000 Subject: [PATCH 10/19] Revert "test(parser): add AST golden for composite completion fixture" This reverts commit 33ccd9c3f3245ed6ed7574921fdb4e6d4be5673f. --- ...ite_without_entry_does_not_complete.golden | 52 ------------------- ...site_without_entry_does_not_complete.sysml | 21 -------- 2 files changed, 73 deletions(-) delete mode 100644 tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden delete mode 100644 tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml diff --git a/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden b/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden deleted file mode 100644 index 4c88f44b04..0000000000 --- a/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.golden +++ /dev/null @@ -1,52 +0,0 @@ -(RootNamespace - (Membership visibility="default" - (Package name="CompositeWithoutEntry" library=false standard=false - (Membership visibility="default" - (Definition kind="item" abstract=false variation=false name="Go")) - (Membership visibility="default" - (Definition kind="item" abstract=false variation=false name="Back")) - (Membership visibility="default" - (Definition kind="item" abstract=false variation=false name="Direct")) - (Membership visibility="default" - (Definition kind="item" abstract=false variation=false name="Finish")) - (Membership visibility="default" - (Usage kind="state" name="Machine" ref=false direction="none" composite=false derived=false ordered=false nonunique=false - (Membership visibility="default" - (Usage kind="attribute" name="log" ref=false direction="none" composite=false derived=false ordered=false nonunique=false - (Relationship kind="typing" target=String - (*ast.QualifiedName)) - (LiteralString value="\"\""))) - (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 - (Membership visibility="default" - (Usage kind="state" name="W1" 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" - (*ast.AssignmentActionNode)))))) - (TransitionMember source="W1" target="done" - (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false - (Relationship kind="typing" target=Finish - (*ast.QualifiedName)))))) - (Membership visibility="default" - (Usage kind="state" name="X" 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" - (*ast.AssignmentActionNode)))))) - (TransitionMember source="idle" target="Work" - (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false - (Relationship kind="typing" target=Go - (*ast.QualifiedName)))) - (TransitionMember source="Work" target="idle" - (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false - (Relationship kind="typing" target=Back - (*ast.QualifiedName)))) - (TransitionMember source="idle" target="W1" - (Usage kind="attribute" name="" ref=true direction="out" composite=false derived=false ordered=false nonunique=false - (Relationship kind="typing" target=Direct - (*ast.QualifiedName)))) - (TransitionMember source="Work" target="X")))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml b/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml deleted file mode 100644 index 528ff67519..0000000000 --- a/tests/parser/testdata/parse/state_composite_without_entry_does_not_complete.sysml +++ /dev/null @@ -1,21 +0,0 @@ -package CompositeWithoutEntry { - item def Go; - item def Back; - item def Direct; - item def Finish; - - state Machine { - attribute log : String = ""; - entry; then idle; - state idle; - state Work { - state W1 { entry { assign log := log + "W1(entry) "; } } - transition first W1 accept Finish then done; - } - state X { entry { assign log := log + "X(entry) "; } } - transition first idle accept Go then Work; - transition first Work accept Back then idle; - transition first idle accept Direct then W1; - transition first Work then X; - } -} From f72659f5fe1acac5fa832b09b23efc4c82c68f04 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:54:39 +0000 Subject: [PATCH 11/19] Revert "fix(runtime): keep a composite without an active substate from completing" This reverts commit 0f39297345fa4f8312548384631da772d64d4ac8. --- .../census-states-events.changed.md | 2 +- docs/project/spec-compliance.md | 1 - ...robustness_composite_without_entry_test.go | 31 ------------------- internal/exec/runtime/state_executor.go | 17 ++-------- ...hout_entry_does_not_complete.expected.json | 14 --------- ...site_without_entry_does_not_complete.sysml | 21 ------------- ...thout_entry_does_not_complete.trace.golden | 27 ---------------- 7 files changed, 4 insertions(+), 109 deletions(-) delete mode 100644 internal/exec/runtime/robustness_composite_without_entry_test.go delete mode 100644 internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json delete mode 100644 internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml delete mode 100644 internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index 49b7d5d1ef..501576e6c3 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ - **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. -- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; default entry enters a region's state stand-in even without a start in its body, leaving no active child, while a region with no stand-in and no start remains inactive; either blocks owner completion until all regions are final. A composite no longer completes immediately on entry when it declares substates but none is entered. Terminate pseudostates now terminate the whole state machine. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; default entry enters a region's state stand-in even without a start in its body, leaving no active child, while a region with no stand-in and no start remains inactive; either blocks owner completion until all regions are final. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 02da106272..560edc6f14 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1004,7 +1004,6 @@ nothing above. | A transition's `do` effect written as a statement is terminated by the transition's own `;` (SysML.xtext `TransitionUsage` ends with `ActionBody`, while `EffectBehaviorUsage` carries no `;`), in the standard `first … then` spelling | `parser/behavior.go` `expectStatementEnd`/`atTransitionEffectStatement`/`atEffectEnd`; `parser/defusage.go` `parseUsage`/`parseReferenceMemberUsage` | `parse/state_transition_effect_statement.golden`, `conformance/state_transition_effect_assign.sysml`, `conformance/state_transition_effect_assign_first_then.sysml` (with their `.trace.golden`), `negative_test.go:transition_effect_perform_two_semicolons`, `:transition_effect_assign_two_semicolons`, `:transition_effect_no_semicolon`, `:body_assignment_no_semicolon` | ✅ Faithful (a second `;` is an error, as `ActionBody` takes one terminator; a statement outside a transition effect still needs its own `;`) | | Bodied `exhibit state` (`exhibit state spacecraftModes { … }`, SysML.xtext `ExhibitStateUsage`) | `parser/behavior.go` (the exhibited state's body is parsed as a state body); `ast/dump.go` (the `exhibit state` keyword is recorded) | `parse/behavior_exhibit_state_body.golden`, `parse/classifier_behaviors.golden`, `negative_test.go:exhibit_state_unclosed_body` | ✅ Faithful (the body parses, resolves and lowers as a state machine, and an object of the type runs it — see the Classifier Behaviors map) | | A state's `entry`, `do` or `exit` behavior written as a reference through a feature chain (`exit controller.coolDown;`) performs the chain's last action on the object the chain reaches from the machine's own performer, so the behavior's `this` is that object and its writes land there, not on the machine | `lower/state_behavior.go` (a `FeatureChainExpr` reference is kept as the behavior's target); `runtime/state_statements.go` (the chain is evaluated against the exhibiting object; the value must be one live object) ; `runtime/invoke_action.go` `performerOf`/`beginCallee` | conformance `state_subaction_on_part` (the part's `cooled` is set, the machine's own `cooled` stays `false`) | ✅ Faithful, **self-assessed** (no external referee executes state behaviors) | -| On entry, a state that declares substates and has no active substate does not immediately complete; its completion transition waits rather than leaving before a nested state is entered | `runtime/state_executor.go` `declaresSubstates`, `completesAtEntry` | `conformance/state_composite_without_entry_does_not_complete.sysml` + `.expected.json` + trace golden; `robustness_composite_without_entry_test.go:TestRuntimeRobustnessCompositeWithoutEntry` | ✅ Faithful | ### Classifier Behaviors (KerML §8.4.4.3 Behaviors / `performances`; SysML v2 §7.16 `exhibit`/`perform`) diff --git a/internal/exec/runtime/robustness_composite_without_entry_test.go b/internal/exec/runtime/robustness_composite_without_entry_test.go deleted file mode 100644 index 265ea5686d..0000000000 --- a/internal/exec/runtime/robustness_composite_without_entry_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package runtime - -import ( - "slices" - "testing" -) - -func TestRuntimeRobustnessCompositeWithoutEntry(t *testing.T) { - _, visits, err := executeStateSource(t, "Machine", `package test { - state Machine { - entry; then Work; - state Work { - state W1; - } - state X; - transition first Work then X; - } - }`) - if err != nil { - t.Fatalf("execute composite without an initial substate: %v", err) - } - if !slices.Contains(visits, "Work") { - t.Errorf("Work was not entered: %v", visits) - } - if slices.Contains(visits, "W1") { - t.Errorf("unstarted substate W1 was entered: %v", visits) - } - if slices.Contains(visits, "X") { - t.Errorf("Work completed without an active substate: %v", visits) - } -} diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index e65423735b..db4effa1cc 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -798,9 +798,10 @@ func (e *StateExecutor) scheduleCompletionTransitions(state *ast.StateNode) erro return nil } -// completesAtEntry reports whether entry queues completion for a bodyless state without substates. +// completesAtEntry reports whether entering state as the end of an entry path queues +// its completion at once: it has a completion transition and nothing below to enter. func (e *StateExecutor) completesAtEntry(state *ast.StateNode) bool { - return !e.hasBody(state) && !e.declaresSubstates(state) && completionCount(e.graph.Transitions[state]) > 0 + return !e.hasBody(state) && completionCount(e.graph.Transitions[state]) > 0 } // hasBody reports whether state has substates to enter: orthogonal regions or a start. @@ -809,18 +810,6 @@ func (e *StateExecutor) hasBody(state *ast.StateNode) bool { return orthogonal || len(e.graph.StartOf(state)) > 0 } -func (e *StateExecutor) declaresSubstates(state *ast.StateNode) bool { - if state == nil { - return false - } - for _, child := range e.graph.States { - if e.graph.ParentState[child] == state && !e.graph.Completes(child) { - return true - } - } - return false -} - // scheduleTimeTransitions queues a time event per time-triggered transition out // of the state whose timer is not running yet, due at the clock's instant. func (e *StateExecutor) scheduleTimeTransitions(state *ast.StateNode) error { diff --git a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json deleted file mode 100644 index 6dfe8cedb6..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.expected.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "state", - "events": [ - {"signal": "Go"}, - {"signal": "Back"}, - {"signal": "Direct"}, - {"signal": "Finish"} - ], - "finalState": "X", - "outputs": { - "log": {"type": "String", "value": "W1(entry) X(entry) "} - }, - "trace": true -} diff --git a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml deleted file mode 100644 index 528ff67519..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.sysml +++ /dev/null @@ -1,21 +0,0 @@ -package CompositeWithoutEntry { - item def Go; - item def Back; - item def Direct; - item def Finish; - - state Machine { - attribute log : String = ""; - entry; then idle; - state idle; - state Work { - state W1 { entry { assign log := log + "W1(entry) "; } } - transition first W1 accept Finish then done; - } - state X { entry { assign log := log + "X(entry) "; } } - transition first idle accept Go then Work; - transition first Work accept Back then idle; - transition first idle accept Direct then W1; - transition first Work then X; - } -} diff --git a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden b/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden deleted file mode 100644 index 382608d7f4..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_composite_without_entry_does_not_complete.trace.golden +++ /dev/null @@ -1,27 +0,0 @@ -exit: idle -enter: Work -transition: idle -> Work (event: accept Go) -exit: Work -enter: idle -transition: Work -> idle (event: accept Back) -exit: idle -enter: Work -enter: W1 (entry action) -stmt action body - stmt assign log - eval feature log -> "" - eval literal "W1(entry) " -> "W1(entry) " - eval operator + -> "W1(entry) " -transition: idle -> W1 (event: accept Direct) -exit: W1 -enter: done -transition: W1 -> done (event: accept Finish) -exit: done -exit: Work -enter: X (entry action) -stmt action body - stmt assign log - eval feature log -> "W1(entry) " - eval literal "X(entry) " -> "X(entry) " - eval operator + -> "W1(entry) X(entry) " -transition: done -> X From 263a11bde705e43d30b7fa0be49a7ca3fc66da09 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:32:04 +0000 Subject: [PATCH 12/19] fix(runtime): count a region left inactive by default entry as complete Co-Authored-By: jason.han --- .../census-states-events.changed.md | 2 +- .../design/precise-semantics-alignment.md | 33 ++++---- docs/project/pssm-referee.md | 9 ++- docs/project/spec-compliance.md | 4 +- internal/exec/runtime/check_state.go | 10 +++ .../robustness_inactive_region_test.go | 3 +- .../robustness_parallel_wrapper_test.go | 4 +- internal/exec/runtime/snapshot.go | 6 +- internal/exec/runtime/state_executor.go | 39 +++++++-- internal/exec/runtime/state_region_entry.go | 7 ++ .../exec/runtime/state_region_transition.go | 4 + internal/exec/runtime/state_route.go | 6 +- ...ion_entered_by_default.check.expected.json | 21 +---- ...n_entered_by_default.declared.trace.golden | 79 ------------------- ...ly_region_entered_by_default.expected.json | 53 +------------ ...ion_entered_by_default.seed-1.trace.golden | 79 ------------------- ...nly_region_entered_by_default.trace.golden | 72 +---------------- ...gion_without_entry_completes.expected.json | 8 ++ ...allel_region_without_entry_completes.sysml | 18 +++++ ...egion_without_entry_completes.trace.golden | 8 ++ ...without_entry_inactive.check.expected.json | 11 +-- ...thout_entry_inactive.declared.trace.golden | 49 ------------ ...egion_without_entry_inactive.expected.json | 19 +---- ...without_entry_inactive.seed-1.trace.golden | 49 ------------ ...region_without_entry_inactive.trace.golden | 32 +------- 25 files changed, 143 insertions(+), 482 deletions(-) delete mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden delete mode 100644 internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.trace.golden delete mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden delete mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index 501576e6c3..c1676ce9d5 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ - **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. -- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state; default entry enters a region's state stand-in even without a start in its body, leaving no active child, while a region with no stand-in and no start remains inactive; either blocks owner completion until all regions are final. Terminate pseudostates now terminate the whole state machine. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/internals/design/precise-semantics-alignment.md b/docs/internals/design/precise-semantics-alignment.md index 91ed16beac..b1cb4d4aa4 100644 --- a/docs/internals/design/precise-semantics-alignment.md +++ b/docs/internals/design/precise-semantics-alignment.md @@ -1669,7 +1669,7 @@ which supersede the hand count this section was first written with — the moves | The UML `StateMachine` as the class under test (a standalone machine with attributes, operations, a constructor) | `part def` with `attribute`s, `action def`s and `exhibit state`, as an owned machine's: the reader (`reader.go`) reads the machine as the `Target` whose `Machine` is itself, with its attributes, operations and their methods, and its constructor | standard | | A guard whose behavior acts on the model (calls `trace(...)` before returning its value) | none: a v2 guard is a Boolean expression (§7.18.3, `validateTransitionFeatureMembershipGuardExpression`; `bool guard[*]` in `TransitionPerformances.kerml`, the effect a separate `step`), and an expression has no spelling for an action. UML 2.5.1 §14.5.11 `Transition::guard` itself calls such a guard ill formed. Recording the runtime's guard reads as the referee's observable instead is refused too: the reads the suite traces are a junction's on the target's default entry, read at the incoming transition's selection, where the library orders every transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | no translation | | A guard whose behavior is an opaque behavior, not an activity | none: the reader follows an activity's nodes to tell whether the behavior acts, and does not read an opaque body, so the guard is refused rather than carried as its Boolean text alone. A `FunctionBehavior` is the exception — it accesses no object by UML's contract (§13.2.3.3) — and is translated as the expression it spells | no translation | -| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a branch enters its target without an `entry; then`. Default entry enters a region's state stand-in even without a start in its body; the stand-in has no active child. A region with no stand-in and no start remains inactive and blocks owner completion (finding 6 below, fixed) | extension | +| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a branch enters its target without an `entry; then`. Default entry enters a region's state stand-in even without a start in its body; if no child becomes active, the region counts as complete. A region with no stand-in and no start remains inactive and also counts as complete on default entry. A branch that activates a region must reach its final state | extension | The classification is by construct, in the order of the table: a test whose model uses any construct with no spelling or no translation is counted as not expressible whatever else it @@ -1737,18 +1737,18 @@ is where a later translation moves them back. | *Deferred 007* | extension | *translated since the emitter spells a returning effect with inputs:* the deferred `op(p1)` call fires `T4` once the second state is active; `T4`'s effect is `action def T4_effect` with `in p` bound to the accept's `p1` and `out 'return'`, its `return` an assignment of `not p`; the one admitted trace, the first state's exit, `T3(effect)`, `T4(effect)[in=true][out=false]` and the tester's `[out=false]`, is reached | | *Standalone 003* | standard | *the standalone machine is read as the target since the reader does so, and translated since the emitter binds entry parameters and returns outputs:* the same shape as *Event 019-E*, the machine itself the class under test with `or` its operation; both admitted traces are reached | | *Fork 002* | extension | *translated:* the fork enters two regions without initial pseudostates; default entry enters their state stand-ins with no active child, and the fork activates its named targets | -| *Join 001* | extension | *translated:* the fork enters two regions without initial pseudostates; an omitted region's stand-in is entered with no active child, which blocks owner completion | +| *Join 001* | extension | *translated:* the fork enters two regions without initial pseudostates; an omitted region's stand-in is entered with no active child and counts as complete on default entry | | *Choice 005* | extension | *refused, settled:* the guards of the junction's and the choice's four outgoing transitions each call `trace("T1.n(guard)")` and the admitted trace records the calls, to show when each guard is read; a v2 guard is an expression with no room for an action, so the translation keeps only the guard's value and cannot reach the trace, and is refused rather than run short. Making the runtime's guard reads the referee's observable would not reach the trace either: the junction sits on the composite's default entry and the suite reads its guards before `T2(effect)` and the composite's entry, where the library reads a transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | The last two were kept apart from the other seven and from the 29 with no spelling: UML allows a fork to target states inside orthogonal regions that have no initial pseudostate, and SysML v2 `parallel` regions can spell the shape. Default entry enters a region's state stand-in even -without an entry transition; if the stand-in has no start in its body, it remains active with -no active child. A region with no stand-in and no start remains inactive. Both an unstarted -stand-in and an inactive region block owner completion until all regions reach final states. -Explicit branches and transitions can enter an inactive region, and the PSSM classifier no -longer records the former lowerer refusal. Entry 002 E remains not-expressible on its -entry-point construct. +without an entry transition; if default entry leaves that stand-in with no active child, the +region counts as complete. A region with no stand-in and no start remains inactive and also +counts as complete on default entry. A later transition or fork branch that activates a region +does not make it complete; that region must reach its final state. PSSM *Entering 004* requires +immediate completion for the single-region default-entry case. Entry 002 E remains +not-expressible on its entry-point construct. #### Behavior parameters, operation results, tester traces and standalone machines @@ -2329,14 +2329,17 @@ sites of the runtime's fixed, the pool's order and the do step drawn on the entr of them has to change (second open decision). `state_choice_pseudostate` does not reach it. *Fixed* with the second open decision: the code now matches the note (SM30's *Decided* sentence), and Track E records the finding as landed. -6. **An unstarted stand-in does not complete its composite-state region.** +6. **A region left without an active child by default entry completes its composite.** UML permits a region without an initial pseudostate, and SysML v2 `parallel` can represent - it. When `RegionState[region]` supplies a stand-in, `regionStart` enters that state even - without a start transition in its body; no child state becomes active. When no stand-in and - no start exist, `regionStart` returns no state and the region remains inactive. In either - case, `regionComplete` and `stateComplete` keep the owning composite from completing until - every region reaches a final state. A later transition or fork branch can enter an inactive - region. Coverage includes `TestRuntimeRobustnessInactiveRegion`, + it. When `RegionState[region]` supplies a stand-in, `regionStart` enters it even without a + start transition in its body; if no child becomes active, the region is complete on default + entry. When no stand-in and no start exist, the region remains inactive and is likewise + complete on default entry. `enterRegion` records this default-entry case and + `enterStateInto` queues the composite's completion after its regions are entered. A region + activated later by a transition or fork branch is not default-complete and must reach its + final state; a running do-action still defers completion. PSSM *Entering 004* verifies the + single-region case. Coverage includes `state_parallel_region_without_entry_completes`, + `TestRuntimeRobustnessInactiveRegion`, `TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, `state_parallel_stateless_region_with_behaviors`, `state_parallel_region_without_entry_inactive`, `state_fork_only_region_entered_by_default`, diff --git a/docs/project/pssm-referee.md b/docs/project/pssm-referee.md index d53caf522e..fff084112e 100644 --- a/docs/project/pssm-referee.md +++ b/docs/project/pssm-referee.md @@ -836,10 +836,11 @@ By reason, as the classifier names them: - A standalone state machine is read as the target class, and a tester's `trace(...)` after a call is driven, so neither is a reason any longer; Event 019 A runs and passes. - A composite-state region without an entry is valid: default entry enters its state stand-in; - when the stand-in has no start in its body, it is active with no active child. A region with - no stand-in and no start remains inactive, and either condition blocks owner completion until - all regions are final. A later transition or fork branch may activate an inactive region. - Entry 002 E remains not-expressible on its entry-point construct; + when no child becomes active, the region counts as complete. A region with no stand-in and no + start remains inactive and also counts as complete on default entry. A region activated later + by a transition or fork branch must reach its final state. PSSM *Entering 004* verifies + immediate completion for the single-region case; `state_parallel_region_without_entry_completes` + covers it in the runtime. Entry 002 E remains not-expressible on its entry-point construct; the no-entry refusal is no longer a classifier reason. Coverage includes `TestRuntimeRobustnessInactiveRegion`, `state_parallel_region_without_entry_inactive`, `state_parallel_stateless_region_with_behaviors`, `state_fork_only_region_entered_by_default`, diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 560edc6f14..96ddc7886d 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -901,7 +901,7 @@ nothing above. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| -| A region represented by a sub-state enters that stand-in on default entry; if the stand-in has no start in its body, it is active with no active child. A region with no stand-in and no start remains inactive; an incomplete region blocks its composite owner's completion, while a later transition or fork branch may activate an inactive region | `lower/state_graph.go` `recordRegionInitials`; `state_region_entry.go` `regionStart`, `enterRegion`, `enterLazily`, `forkEntry`, `enterForkBranches`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `TestToStateGraph_ForkOmissionLeavesRegionInactive`, `TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens), `state_transition_cross_region_nested_target.sysml` | ✅ Faithful | +| On default entry, a region whose stand-in has no active child is complete; a region with no stand-in and no start is also complete while inactive. A region activated later by a transition or fork branch must reach its final state. A composite whose regions are all complete by default completes immediately (PSSM *Entering 004*); a running do-action still defers completion | `state_region_entry.go` `regionStart`, `enterRegion`; `state_executor.go` `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`, `scheduleCompletionTransitions` | `state_parallel_region_without_entry_completes.sysml` (+ trace golden; PSSM *Entering 004*), `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens); `lower/fork_plan_test.go` | ✅ Faithful | | A named succession or transition endpoint that resolves to a non-vertex or non-action node is reported during analysis; positional, implied, unresolved and flow ends remain unchanged | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go`; `passes/action_endpoint.go` `ActionEndpointPass`; `lower/action_nodes.go` `ActionNodes` and `ActionEndpointAccepted`, including lazy inherited action-node collection | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestActionEndpointPassReportsResolvedNonNodes`, `:TestActionEndpointPassAcceptsLoweredNodes`, `:TestActionEndpointPassDiagnosticAgreesWithLowering`, `:TestActionEndpointPassInheritedNodes`, `:TestActionEndpointPassIgnoresFeatureChainsThroughNonNodes`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`; `lower/action_inherited_test.go`; `runtime/testdata/conformance/action_inherited_endpoint.sysml` | ⚠️ Approximate (a feature chain rooted in a part usage is legal notation and stays silent during validation, but execution still reports the existing lowering error because invoking an action through that chain is not implemented) | | Initial state identification | `lower/state_graph.go:ToStateGraph`; `state_executor.go:686` initialize | `state_simple.sysml` | ✅ Faithful | | A succession out of a state body's own entry subaction names the state it starts in (`entry; then off;`), which is how a machine designates where it starts | `lower/entry_transition.go` `addEntryTransition`, `UnconditionalStart`; `lower/state_graph.go` collectTransitions SuccessionEdge case + `isEntrySubaction` | `lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState`, `lower/transition_source_test.go:TestToStateGraph_UnguardedEntryTransitionIsInitial`, `state_entry_succession_initial.sysml` conformance, `tests/parser/testdata/parse/behavior_exhibit_state_body.golden` | ✅ Faithful | @@ -969,7 +969,7 @@ nothing above. | A succession (`succession first s then a;`) names its endpoints the way a transition does, so it reaches a nested, region-local or qualified vertex, and a pseudostate as well as a state | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go` route succession ends; `passes/state_transition.go`; `lower/state_graph.go` `collectTransitions` (UsageSuccession and SuccessionEdge cases) → `(*StateGraph).endpointVertex`/`endpointState`, over the same `lower.EndpointResolver` a transition's endpoints resolve through | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`, `lower/state_graph_nested_test.go:TestSuccessionReachesAPseudostate`, `:TestSuccessionQualifiedTargetNamesTheVertexItQualifies`, `:TestToStateGraph_EntrySuccessionNamesInitialState` | ✅ Faithful (previously matched the endpoint's last name segment against a flat state list, which reached no pseudostate and could bind a same-named vertex of another state) | | Two regions may declare same-named pseudostates, and each is a vertex of its own | `lower/state_graph.go` `StateGraph.Pseudostates` (declaration-ordered slice, not keyed by name), `addPseudostate` | `lower/state_graph_nested_test.go:TestSameNamedPseudostatesInSiblingRegionsAreBothCollected` | ✅ Faithful | | Junction pseudostates: the route through a junction is resolved statically before the incoming effect; every outgoing guard is read against the current data, several enabled branches are a recorded transition choice, and an unguarded branch is the default when none of the guards holds. The settled route continues through further junctions but stays open at the first choice; a history default is resolved the same way. If a route has no way through, its compound transition is not enabled. The occurrence may go to another enabled transition, be deferred or be discarded as unmatched; a completion with no route is dropped. For a join, selection checks the way out using only the candidate and prior arrivals. Signal/change dispatches count same-occurrence peers only when their regions chose them; firing checks completion against those peers, and a dead route fires none. Timer expiries are separate occurrences even when due together; a dead completing segment is disabled, leaving its source's timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). A history default route disables the transition only when its source is outside the history owner. Only `errNoWayThrough` disables: cycles, unevaluable guards and binding failures still fail as the transition fires. Dead branches whose continuation has no way through are filtered from a junction draw; one surviving branch is followed without a draw. Choice remains dynamic, and a dead junction reached beyond a choice remains a run error. UML 2.5.1 §14.2.3.8.1 (compound transition) and §14.2.3.7 (junction) are the extension's references. KerML `ControlPerformances::DecisionPerformance` declares `feature outgoingHBLink: HappensBefore[1]`; it does not specify no-way-through or unmatched-event behavior | `state_route.go` `resolveRoute`, `routeAvailable`, `followOut`, `enabledBranches`, `junctionDraw`, `travel`, `travelResolving`, `settleDraws`, `pickBranch`, `errNoWayThrough`; `state_executor.go` transition enablement and history-default routing | `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`, `state_junction_no_way_through_unmatched.sysml`, `state_junction_no_way_through_other_transition_fires.sysml`, `state_junction_no_way_through_deferred.sysml`, `state_junction_dead_branch_not_drawn.sysml`, `state_completion_no_way_through_dropped.sysml`, `state_history_default_no_way_through.sysml`, `state_history_self_transition_default_no_way_through_restores.sysml`, `state_join_no_way_out_disables_last_segment.sysml`; `explore_test.go:TestExploreStaticJunctionBranches`, `:TestExploreJunctionDrawnAsTransitionFires`, `:TestExploreHistoryDefaultThroughJunction`; `replay_test.go:TestReplayRefusedChoiceUndoesTheJunctionDrawBeforeIt`, `:TestReplayRefusedChoiceUndoesTheHistoryDefaultsJunctionDraw`; `robustness_junction_exit_route_test.go:TestRuntimeRobustnessJunctionExitRoute`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead junction beyond choice, cycle, unevaluable guard, join exit failure); `robustness_test.go:region_pseudostate_without_satisfied_guard` | ✅ Faithful | -| Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork follows default entry: its stand-in is entered even without a start, leaving no active child; when no stand-in and no start exist, the region remains inactive. Default entry follows the same rule. A composite completes only after every region reaches a final state; a stand-in with no active child or an inactive region blocks completion. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region with a start does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `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`, `regionStart`; `state_executor.go` `regionComplete`, `stateComplete` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | +| Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork follows default entry: its stand-in is entered even without a start; if no child becomes active, the region counts as complete, as does a region with no stand-in and no start (PSSM *Entering 004*). A later fork branch that activates a region must reach its final state before the owner completes. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region with a start does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. A running do-action still defers completion. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `lower/state_footprint.go` `stateFootprintBuilder.pseudostate` (fork case), `entersRegion`; `lower/state_graph.go` `ownTransitionEffects`; `state_executor.go` `fireForkTransition`, `leaveForFork`, `exitRegionsOf`, `completeIfDone`, `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`; `state_region_entry.go` `forkEntry`, `enterForkBranches`, `enterRegion`, `enterLazily`, `regionStart` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_region_without_entry_completes.sysml` + trace golden, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | | Join pseudostates: each occurrence fires every not-yet-arrived segment it enables and whose region's dispatch chose it, exiting each source, running its effect and recording each in `joinArrived` only after the firing succeeds; when multiple segments fire, they are drawn in `join ` order. At selection, including for each time expiry, probes count only the candidate and prior arrivals, checking the way out only if they complete the join. In signal/change dispatches, same-occurrence peers count only when their regions chose them; firing checks completion against those peers, and if the route is dead, none fire. Timer expiries are separate occurrences even at the same instant; a dead completing segment is disabled, leaving its source's other timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). The join completes when prior arrivals plus segments enabled by the current occurrence and chosen by their regions in that dispatch cover every incoming segment and the outgoing route is available. The current segments are drawn as `join `, arrivals clear and the owner is left after the last segment effect. Completion segments fire only on their own completion; waiting regions do not complete their owner; arrivals clear on region re-entry or when another transition leaves the owner. Snapshots, held images and canonical check state preserve arrivals. PSSM leaves the owner before the final incoming effect; leaving after that effect is the extension's reading recorded in SM34. `TransitionPerformances.kerml` says `private succession [1] transitionLinkSource then [*] effect;`; `StatePerformances.kerml` says `and hence happening during the state performance.` Neither supplies a cross-segment arrival rule; that is the UML/PSSM extension reference | `state_route.go` `joinFiringNow`; `state_executor.go` `fireJoinTransition`, `fireJoinSegment`, `fireJoinIncoming`, `leaveJoinOwner`, `regionWaitingAtJoin`, `setRegionState`, `clearJoinArrivalsOwnedBy`; `snapshot.go`, `held_image_behavior.go`, `check_state.go` preserve `joinArrived` | Conformance fixtures: `state_fork_join_pseudostate`, `state_join_arrival_dropped_when_owner_exited`, `state_join_change_segment_rises_alone`, `state_join_change_segment_waits_before_junction`, `state_join_change_segment_waits_before_junction_metadata`, `state_join_change_segments_rise_together`, `state_join_completion_is_not_a_timers_expiry`, `state_join_completion_segment_waits_for_do_behavior`, `state_join_dead_timer_join_keeps_group_alternative`, `state_join_exit_from_regions_before_owner_exit`, `state_join_from_composite_sources`, `state_join_from_nested_states`, `state_join_no_way_out_disables_last_segment`, `state_join_of_machine_regions_from_nested_source`, `state_join_runs_every_incoming_effect`, `state_join_segment_fires_on_own_signal`, `state_join_segments_arrive_together_on_one_occurrence`, `state_join_segment_not_chosen_does_not_arrive`, `state_join_peer_not_chosen_does_not_block_arrival`, `state_join_segment_reads_its_payload`, `state_join_segment_trigger_unmatched`, `state_join_time_segment_needs_same_occurrence`, `state_join_time_segment_not_taken_by_signal`, `state_join_time_segment_unsynchronized_reads_no_route`, `state_join_time_segment_unsynchronized_reads_no_route_metadata`, `state_join_time_segments_expire_apart`, `state_join_time_segments_expire_together`, `state_join_waits_for_every_segment_enabled`; tests: `pseudostate_test.go:TestForkAndJoinPseudostates`, `:TestJoinWaitsForEveryBranch`, `:TestJoinSegmentFiresOnOwnOccurrence`, `:TestJoinDisarmedSegmentCanArriveAlone`, `:TestJoinFromActiveCompositeSourcesFires`, `:TestJoinCompletionSegmentWaitsForItsSourcesDoBehavior`, `:TestJoinCompletionSegmentWaitsForItsSourcesBody`; `fork_join_test.go:TestForkJoinVisitOrderIsDeterministic`; `choice_test.go:TestCompletionTransitionArrivesAtUnreadyJoin`; `state_change_trigger_test.go:TestChangeTriggerDisarmedSegmentCanWaitForItsPeer`; `join_snapshot_test.go:TestSnapshotRestoresJoinArrivals`, `:TestCloneNilJoinArrivalsIsWritable`; `check_test.go:TestCheckJoinWaitsForSlowestBranch`; `replay_test.go:TestReplayRefusedJoinDrawChangesNothing`, `:TestReplayRefusedJoinDrawUndoesTheSegmentsChoice`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead signal and timer join exits, exit failure) | ✅ 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 ;`) and `lower/state_metadata.go` (`#shallowHistory state`/`#deepHistory state`); `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 | diff --git a/internal/exec/runtime/check_state.go b/internal/exec/runtime/check_state.go index 1a2d4c6342..0e95fc8487 100644 --- a/internal/exec/runtime/check_state.go +++ b/internal/exec/runtime/check_state.go @@ -274,6 +274,16 @@ func (s *stateSpeller) machine(e *StateExecutor) { for _, region := range e.orderedActiveRegions() { fmt.Fprintf(&s.out, " active{%s = %s}", regionKey(region), e.statePath(e.activeConfig.regionStates[region])) } + complete := make([]*ast.StateRegion, 0, len(e.activeConfig.defaultComplete)) + for region, isComplete := range e.activeConfig.defaultComplete { + if isComplete { + complete = append(complete, region) + } + } + sort.Slice(complete, func(i, j int) bool { return regionKey(complete[i]) < regionKey(complete[j]) }) + for _, region := range complete { + fmt.Fprintf(&s.out, " default-complete{%s}", regionKey(region)) + } } for _, state := range e.stateStack { fmt.Fprintf(&s.out, " stack{%s}", e.statePath(state)) diff --git a/internal/exec/runtime/robustness_inactive_region_test.go b/internal/exec/runtime/robustness_inactive_region_test.go index 6351bd16fe..c572262a87 100644 --- a/internal/exec/runtime/robustness_inactive_region_test.go +++ b/internal/exec/runtime/robustness_inactive_region_test.go @@ -22,7 +22,8 @@ func TestRuntimeRobustnessInactiveRegion(t *testing.T) { case "idle", "waiting": t.Errorf("no-entry region state %s was activated", visit) case "finished": - t.Fatal("working completed while both regions were inactive") + return } } + t.Fatal("working did not complete after both regions were left inactive") } diff --git a/internal/exec/runtime/robustness_parallel_wrapper_test.go b/internal/exec/runtime/robustness_parallel_wrapper_test.go index 1af21c0770..c53b349b21 100644 --- a/internal/exec/runtime/robustness_parallel_wrapper_test.go +++ b/internal/exec/runtime/robustness_parallel_wrapper_test.go @@ -30,8 +30,8 @@ func TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive(t *testing.T) if slices.Contains(visits, "A") { t.Errorf("the unstarted body state A was entered: %v", visits) } - if slices.Contains(visits, "finished") { - t.Errorf("S completed while its stand-in body was unstarted: %v", visits) + if !slices.Contains(visits, "finished") { + t.Errorf("S did not complete while its stand-in body was unstarted: %v", visits) } } diff --git a/internal/exec/runtime/snapshot.go b/internal/exec/runtime/snapshot.go index 433043d37c..3223075cce 100644 --- a/internal/exec/runtime/snapshot.go +++ b/internal/exec/runtime/snapshot.go @@ -967,7 +967,11 @@ func cloneConfiguration(config *StateConfiguration) *StateConfiguration { if config == nil { return nil } - return &StateConfiguration{simpleState: config.simpleState, regionStates: maps.Clone(config.regionStates)} + return &StateConfiguration{ + simpleState: config.simpleState, + regionStates: maps.Clone(config.regionStates), + defaultComplete: maps.Clone(config.defaultComplete), + } } func cloneDispatch(dispatch *Dispatch) *Dispatch { diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index db4effa1cc..7c61a2cc21 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -24,7 +24,8 @@ type StateConfiguration struct { simpleState *ast.StateNode // For composite states with regions: map of region → active state in that region - regionStates map[*ast.StateRegion]*ast.StateNode + regionStates map[*ast.StateRegion]*ast.StateNode + defaultComplete map[*ast.StateRegion]bool } // StateExecutor executes state machines using event-driven semantics. @@ -295,7 +296,8 @@ func newStateExecutorOn( breakpointNodes: make(map[ast.Node]bool), dispatchMark: -1, activeConfig: &StateConfiguration{ - regionStates: make(map[*ast.StateRegion]*ast.StateNode), + regionStates: make(map[*ast.StateRegion]*ast.StateNode), + defaultComplete: make(map[*ast.StateRegion]bool), }, entering: make(map[*ast.StateNode]bool), } @@ -798,8 +800,7 @@ func (e *StateExecutor) scheduleCompletionTransitions(state *ast.StateNode) erro return nil } -// completesAtEntry reports whether entering state as the end of an entry path queues -// its completion at once: it has a completion transition and nothing below to enter. +// completesAtEntry reports whether entering a bodyless state queues its completion at once. func (e *StateExecutor) completesAtEntry(state *ast.StateNode) bool { return !e.hasBody(state) && completionCount(e.graph.Transitions[state]) > 0 } @@ -2535,6 +2536,7 @@ func (e *StateExecutor) abandonMachine() []string { e.clearEntryState() e.activeConfig.simpleState = nil e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) + e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) e.stateStack = nil e.completionDue = false // Nothing dispatches on an ended machine: what it queued is discarded. @@ -2634,9 +2636,11 @@ func (e *StateExecutor) completeInto(trans *lower.Transition, fromName string, t return nil } -// regionComplete reports whether region rests at its own completion vertex (not a -// nested composite's `done`), or at its owner once a transition into the owner left it empty. +// regionComplete reports whether region reached its completion vertex, was left empty by default entry, or was left at its owner. func (e *StateExecutor) regionComplete(region *ast.StateRegion) bool { + if e.activeConfig.defaultComplete[region] { + return true + } active, ok := e.activeConfig.regionStates[region] if !ok { return false @@ -2647,6 +2651,19 @@ func (e *StateExecutor) regionComplete(region *ast.StateRegion) bool { return active == e.graph.RegionOwner[region] } +func (e *StateExecutor) allRegionsDefaultComplete(state *ast.StateNode) bool { + regions := e.graph.CompositeStates[state] + if len(regions) == 0 { + return false + } + for _, region := range regions { + if !e.activeConfig.defaultComplete[region] { + return false + } + } + return true +} + // stateComplete reports whether state's body has completed: it is a completion // vertex, its substate rests at its `done`, or its every orthogonal region does. func (e *StateExecutor) stateComplete(state *ast.StateNode) bool { @@ -3307,6 +3324,7 @@ func (e *StateExecutor) fireJoinSegment(trans *lower.Transition, plan *lower.Joi if active, isActive := e.activeConfig.regionStates[region]; isActive { e.recordRegionHistory(region, active) delete(e.activeConfig.regionStates, region) + delete(e.activeConfig.defaultComplete, region) } } if err := e.exitStates(e.exitPath(leaving, plan.Owner, nil)); err != nil { @@ -3346,6 +3364,7 @@ func (e *StateExecutor) joinIncoming(join *ast.PseudostateNode) []*lower.Transit // setRegionState clears arrivals from a region before its active state changes. func (e *StateExecutor) setRegionState(region *ast.StateRegion, state *ast.StateNode) { e.clearJoinArrivalsForRegion(region) + delete(e.activeConfig.defaultComplete, region) e.activeConfig.regionStates[region] = state } @@ -5081,6 +5100,7 @@ func (e *StateExecutor) initialize() (err error) { e.state = StateRunning e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) + e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) e.activeConfig.simpleState = nil return e.enterMachineRegions() } @@ -5319,6 +5339,11 @@ func (e *StateExecutor) enterStateInto(state *ast.StateNode, branches map[*ast.S if err := e.enterRegionsInto(state, regions, branches); err != nil { return err } + if e.allRegionsDefaultComplete(state) { + if err := e.scheduleCompletionTransitions(state); err != nil { + return fmt.Errorf("schedule completion of state %s: %w", state.Name, err) + } + } } // The completion goes into the pool behind those queued by the entries performed @@ -5555,6 +5580,7 @@ func (e *StateExecutor) exitRegionsBelow(owner *ast.StateNode, regions []*ast.St // Clear the entry first: the recursive exit walks the same map, and the // region still pointing at regionState would exit it a second time. delete(e.activeConfig.regionStates, region) + delete(e.activeConfig.defaultComplete, region) bodies = append(bodies, func() error { for current := regionState; current != nil && current != owner; current = e.graph.ParentState[current] { if err := e.exitState(current); err != nil { @@ -5695,6 +5721,7 @@ func (e *StateExecutor) getCurrentState() *ast.StateNode { func (e *StateExecutor) setCurrentState(state *ast.StateNode) { e.activeConfig.simpleState = state e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) // Clear regions + e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) } // StateStack returns a copy of the state stack (active configuration). diff --git a/internal/exec/runtime/state_region_entry.go b/internal/exec/runtime/state_region_entry.go index d9302e58bd..fcb4228bbb 100644 --- a/internal/exec/runtime/state_region_entry.go +++ b/internal/exec/runtime/state_region_entry.go @@ -138,6 +138,9 @@ func (e *StateExecutor) enterRegion(w *regionEntry) error { return err } if entry == nil { + if w.target == nil { + e.activeConfig.defaultComplete[w.region] = true + } return nil } e.setRegionState(w.region, entry) @@ -150,6 +153,10 @@ func (e *StateExecutor) enterRegion(w *regionEntry) error { if branch, ok := e.branchesTo(nil, deepest)[w.region]; ok { e.setRegionState(w.region, branch) } + if w.target == nil && entry == e.graph.RegionState[w.region] && deepest == entry && + len(e.graph.StartOf(entry)) == 0 { + e.activeConfig.defaultComplete[w.region] = true + } return nil } diff --git a/internal/exec/runtime/state_region_transition.go b/internal/exec/runtime/state_region_transition.go index 8ee5cd25c8..e16e440c9d 100644 --- a/internal/exec/runtime/state_region_transition.go +++ b/internal/exec/runtime/state_region_transition.go @@ -385,6 +385,7 @@ func (e *StateExecutor) exitRegionTo(region *ast.StateRegion, stop *ast.StateNod } path := e.regionExitPath(region, stop) delete(e.activeConfig.regionStates, region) + delete(e.activeConfig.defaultComplete, region) if stop == nil { // The region keeps no active state, so it has none to restore either. e.forgetRegionHistory(region) @@ -451,6 +452,7 @@ func (e *StateExecutor) exitRegionOwnerTo(owner, lca *ast.StateNode) error { if active, isActive := e.activeConfig.regionStates[declaring]; isActive && e.isBelowOrEqual(active, current) { e.recordRegionHistory(declaring, active) delete(e.activeConfig.regionStates, declaring) + delete(e.activeConfig.defaultComplete, declaring) } } if err := e.exitState(current); err != nil { @@ -471,6 +473,7 @@ func (e *StateExecutor) leaveTopRegions(trans *lower.Transition, effects []route continue } delete(e.activeConfig.regionStates, region) + delete(e.activeConfig.defaultComplete, region) bodies = append(bodies, func() error { leaving := make([]*ast.StateNode, 0) for current := active; current != nil; current = e.graph.ParentState[current] { @@ -483,6 +486,7 @@ func (e *StateExecutor) leaveTopRegions(trans *lower.Transition, effects []route return err } e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) + e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) e.activeConfig.simpleState = nil if err := e.runEffects(effects, e.descendantChain(nil, target)); err != nil { diff --git a/internal/exec/runtime/state_route.go b/internal/exec/runtime/state_route.go index 871b4449cf..ca04022b95 100644 --- a/internal/exec/runtime/state_route.go +++ b/internal/exec/runtime/state_route.go @@ -3,6 +3,7 @@ package runtime import ( "errors" "fmt" + "maps" "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" @@ -729,8 +730,9 @@ func (e *StateExecutor) exitAhead(states []*ast.StateNode) error { if len(states) == 0 { return nil } - simple, regions := e.activeConfig.simpleState, e.activeConfig.regionStates + simple, regions, complete := e.activeConfig.simpleState, e.activeConfig.regionStates, e.activeConfig.defaultComplete e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode, len(regions)) + e.activeConfig.defaultComplete = maps.Clone(complete) for region, active := range regions { e.activeConfig.regionStates[region] = active } @@ -740,7 +742,7 @@ func (e *StateExecutor) exitAhead(states []*ast.StateNode) error { e.exitingAhead = true err := e.exitStates(states) e.exitingAhead = false - e.activeConfig.simpleState, e.activeConfig.regionStates = simple, regions + e.activeConfig.simpleState, e.activeConfig.regionStates, e.activeConfig.defaultComplete = simple, regions, complete return err } diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json index f913a417eb..d6bcd615d8 100644 --- a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.check.expected.json @@ -1,22 +1,7 @@ { - "verdict": "divergent", - "divergent": { - "log": [ - "\"a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) \"", - "\"a(entry) b(entry) a(entry) c(entry) b(entry) finished(entry) \"", - "\"a(entry) b(entry) b(entry) a(entry) c(entry) finished(entry) \"", - "\"a(entry) b(entry) b(entry) c(entry) a(entry) finished(entry) \"", - "\"a(entry) b(entry) c(entry) a(entry) b(entry) finished(entry) \"", - "\"a(entry) b(entry) c(entry) b(entry) a(entry) finished(entry) \"", - "\"b(entry) a(entry) a(entry) b(entry) c(entry) finished(entry) \"", - "\"b(entry) a(entry) a(entry) c(entry) b(entry) finished(entry) \"", - "\"b(entry) a(entry) b(entry) a(entry) c(entry) finished(entry) \"", - "\"b(entry) a(entry) b(entry) c(entry) a(entry) finished(entry) \"", - "\"b(entry) a(entry) c(entry) a(entry) b(entry) finished(entry) \"", - "\"b(entry) a(entry) c(entry) b(entry) a(entry) finished(entry) \"" - ] - }, + "verdict": "no violation, exhaustive", "agreed": { - "finalState": "finished" + "finalState": "finished", + "log": "\"finished(entry) \"" } } diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden deleted file mode 100644 index 9e4bf2ea81..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.declared.trace.golden +++ /dev/null @@ -1,79 +0,0 @@ -exit: init -enter: working -transition: init -> working (event: accept Default) -exit: working -enter: init -transition: working -> init (event: accept Back) -exit: init -choice fork split: next split->a(effect), split->b(effect) (unordered; took split->a(effect) first) -enter: working -enter: a (entry action) -stmt action body - stmt assign log - eval feature log -> "" - eval literal "a(entry) " -> "a(entry) " - eval operator + -> "a(entry) " -enter: b (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) " - eval literal "b(entry) " -> "b(entry) " - eval operator + -> "a(entry) b(entry) " -transition: init -> split -exit: a -enter: done -transition: a -> done (event: accept FinishA) -exit: b -enter: done -transition: b -> done (event: accept FinishB) -choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) -exit: done -exit: done -exit: working -enter: init -transition: working -> init (event: accept Back) -exit: init -choice fork all: next all->a(effect), all->b(effect), all->c(effect) (unordered; took all->a(effect) first) -enter: working -enter: a (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) " - eval literal "a(entry) " -> "a(entry) " - eval operator + -> "a(entry) b(entry) a(entry) " -choice fork all: next all->b(effect), all->c(effect) (unordered; took all->b(effect) first) -enter: b (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) a(entry) " - eval literal "b(entry) " -> "b(entry) " - eval operator + -> "a(entry) b(entry) a(entry) b(entry) " -enter: c (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) a(entry) b(entry) " - eval literal "c(entry) " -> "c(entry) " - eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) " -transition: init -> all -exit: a -enter: done -transition: a -> done (event: accept FinishA) -exit: b -enter: done -transition: b -> done (event: accept FinishB) -exit: c -enter: done -transition: c -> done (event: accept FinishC) -choice exiting working: next left.done(exit), right.done(exit), third.done(exit) (unordered; took left.done(exit) first) -exit: done -choice exiting working: next right.done(exit), third.done(exit) (unordered; took right.done(exit) first) -exit: done -exit: done -exit: working -enter: finished (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) a(entry) b(entry) c(entry) " - eval literal "finished(entry) " -> "finished(entry) " - eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) " -transition: working -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json index e935776bf6..b6e665400f 100644 --- a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.expected.json @@ -12,56 +12,7 @@ {"signal": "FinishB"}, {"signal": "FinishC"} ], - "outcomes": [ - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) a(entry) c(entry) b(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) b(entry) a(entry) c(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) b(entry) c(entry) a(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) c(entry) a(entry) b(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "a(entry) b(entry) c(entry) b(entry) a(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) a(entry) b(entry) c(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) a(entry) c(entry) b(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) b(entry) a(entry) c(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) b(entry) c(entry) a(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) c(entry) a(entry) b(entry) finished(entry) "}} - }, - { - "finalState": "finished", - "outputs": {"log": {"type": "String", "value": "b(entry) a(entry) c(entry) b(entry) a(entry) finished(entry) "}} - } - ], - "admissible": "Regions of a parallel state entered on one occurrence: each is entered, in which order is open", + "finalState": "finished", + "outputs": {"log": {"type": "String", "value": "finished(entry) "}}, "trace": true } diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden deleted file mode 100644 index 4ce90069fd..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.seed-1.trace.golden +++ /dev/null @@ -1,79 +0,0 @@ -exit: init -enter: working -transition: init -> working (event: accept Default) -exit: working -enter: init -transition: working -> init (event: accept Back) -exit: init -choice fork split: next split->a(effect), split->b(effect) (unordered; took split->b(effect) first) -enter: working -enter: b (entry action) -stmt action body - stmt assign log - eval feature log -> "" - eval literal "b(entry) " -> "b(entry) " - eval operator + -> "b(entry) " -enter: a (entry action) -stmt action body - stmt assign log - eval feature log -> "b(entry) " - eval literal "a(entry) " -> "a(entry) " - eval operator + -> "b(entry) a(entry) " -transition: init -> split -exit: a -enter: done -transition: a -> done (event: accept FinishA) -exit: b -enter: done -transition: b -> done (event: accept FinishB) -choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) -exit: done -exit: done -exit: working -enter: init -transition: working -> init (event: accept Back) -exit: init -choice fork all: next all->a(effect), all->b(effect), all->c(effect) (unordered; took all->c(effect) first) -enter: working -enter: c (entry action) -stmt action body - stmt assign log - eval feature log -> "b(entry) a(entry) " - eval literal "c(entry) " -> "c(entry) " - eval operator + -> "b(entry) a(entry) c(entry) " -choice fork all: next all->a(effect), all->b(effect) (unordered; took all->a(effect) first) -enter: a (entry action) -stmt action body - stmt assign log - eval feature log -> "b(entry) a(entry) c(entry) " - eval literal "a(entry) " -> "a(entry) " - eval operator + -> "b(entry) a(entry) c(entry) a(entry) " -enter: b (entry action) -stmt action body - stmt assign log - eval feature log -> "b(entry) a(entry) c(entry) a(entry) " - eval literal "b(entry) " -> "b(entry) " - eval operator + -> "b(entry) a(entry) c(entry) a(entry) b(entry) " -transition: init -> all -exit: a -enter: done -transition: a -> done (event: accept FinishA) -exit: b -enter: done -transition: b -> done (event: accept FinishB) -exit: c -enter: done -transition: c -> done (event: accept FinishC) -choice exiting working: next left.done(exit), right.done(exit), third.done(exit) (unordered; took third.done(exit) first) -exit: done -choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) -exit: done -exit: done -exit: working -enter: finished (entry action) -stmt action body - stmt assign log - eval feature log -> "b(entry) a(entry) c(entry) a(entry) b(entry) " - eval literal "finished(entry) " -> "finished(entry) " - eval operator + -> "b(entry) a(entry) c(entry) a(entry) b(entry) finished(entry) " -transition: working -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden index 9e4bf2ea81..ceb26ef86f 100644 --- a/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_fork_only_region_entered_by_default.trace.golden @@ -2,78 +2,10 @@ exit: init enter: working transition: init -> working (event: accept Default) exit: working -enter: init -transition: working -> init (event: accept Back) -exit: init -choice fork split: next split->a(effect), split->b(effect) (unordered; took split->a(effect) first) -enter: working -enter: a (entry action) -stmt action body - stmt assign log - eval feature log -> "" - eval literal "a(entry) " -> "a(entry) " - eval operator + -> "a(entry) " -enter: b (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) " - eval literal "b(entry) " -> "b(entry) " - eval operator + -> "a(entry) b(entry) " -transition: init -> split -exit: a -enter: done -transition: a -> done (event: accept FinishA) -exit: b -enter: done -transition: b -> done (event: accept FinishB) -choice exiting working: next left.done(exit), right.done(exit) (unordered; took left.done(exit) first) -exit: done -exit: done -exit: working -enter: init -transition: working -> init (event: accept Back) -exit: init -choice fork all: next all->a(effect), all->b(effect), all->c(effect) (unordered; took all->a(effect) first) -enter: working -enter: a (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) " - eval literal "a(entry) " -> "a(entry) " - eval operator + -> "a(entry) b(entry) a(entry) " -choice fork all: next all->b(effect), all->c(effect) (unordered; took all->b(effect) first) -enter: b (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) a(entry) " - eval literal "b(entry) " -> "b(entry) " - eval operator + -> "a(entry) b(entry) a(entry) b(entry) " -enter: c (entry action) -stmt action body - stmt assign log - eval feature log -> "a(entry) b(entry) a(entry) b(entry) " - eval literal "c(entry) " -> "c(entry) " - eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) " -transition: init -> all -exit: a -enter: done -transition: a -> done (event: accept FinishA) -exit: b -enter: done -transition: b -> done (event: accept FinishB) -exit: c -enter: done -transition: c -> done (event: accept FinishC) -choice exiting working: next left.done(exit), right.done(exit), third.done(exit) (unordered; took left.done(exit) first) -exit: done -choice exiting working: next right.done(exit), third.done(exit) (unordered; took right.done(exit) first) -exit: done -exit: done -exit: working enter: finished (entry action) stmt action body stmt assign log - eval feature log -> "a(entry) b(entry) a(entry) b(entry) c(entry) " + eval feature log -> "" eval literal "finished(entry) " -> "finished(entry) " - eval operator + -> "a(entry) b(entry) a(entry) b(entry) c(entry) finished(entry) " + eval operator + -> "finished(entry) " transition: working -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.expected.json new file mode 100644 index 0000000000..2c71894aaa --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.expected.json @@ -0,0 +1,8 @@ +{ + "type": "state", + "finalState": "done", + "outputs": { + "log": {"type": "String", "value": "S1(entry);S1(exit);"} + }, + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.sysml b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.sysml new file mode 100644 index 0000000000..5a292087ef --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.sysml @@ -0,0 +1,18 @@ +package ParallelRegionCompletion { + private import ScalarValues::*; + + state Machine { + attribute log : String = ""; + entry; then S1; + // PSSM Entering 004: a region without an initial pseudostate completes on default entry. + state S1 parallel { + entry action { assign log := log + "S1(entry);"; } + exit action { assign log := log + "S1(exit);"; } + state r { + state S1_1 { entry action { assign log := log + "S1.1(entry);"; } } + transition first S1_1 then done; + } + } + transition first S1 then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.trace.golden new file mode 100644 index 0000000000..5ea5057ff1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_completes.trace.golden @@ -0,0 +1,8 @@ +exit: S1 (exit action) +stmt action body + stmt assign log + eval feature log -> "S1(entry);" + eval literal "S1(exit);" -> "S1(exit);" + eval operator + -> "S1(entry);S1(exit);" +enter: done +transition: S1 -> done diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json index d224a888b3..16eb9068ab 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.check.expected.json @@ -1,12 +1,7 @@ { - "verdict": "divergent", - "divergent": { - "log": [ - "\"A(entry) A(entry) B(entry) outside(entry) \"", - "\"A(entry) B(entry) A(entry) outside(entry) \"" - ] - }, + "verdict": "no violation, exhaustive", "agreed": { - "finalState": "outside" + "finalState": "outside", + "log": "\"A(entry) outside(entry) \"" } } diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden deleted file mode 100644 index 8bdad6063b..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.declared.trace.golden +++ /dev/null @@ -1,49 +0,0 @@ -exit: idle -enter: working -enter: A (entry action) -stmt action body - stmt assign log - eval feature log -> "" - eval literal "A(entry) " -> "A(entry) " - eval operator + -> "A(entry) " -transition: idle -> working (event: accept Go) -exit: A -enter: done -transition: A -> done (event: accept FinishA) -exit: done -exit: working -enter: idle -transition: working -> idle (event: accept Back) -exit: idle -enter: working -choice entering working: next A(entry), B(entry) (unordered; took A(entry) first) -enter: A (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) " - eval literal "A(entry) " -> "A(entry) " - eval operator + -> "A(entry) A(entry) " -enter: B (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) A(entry) " - eval literal "B(entry) " -> "B(entry) " - eval operator + -> "A(entry) A(entry) B(entry) " -transition: idle -> B (event: accept Direct) -exit: A -enter: done -transition: A -> done (event: accept FinishA) -exit: B -enter: done -transition: B -> done (event: accept FinishB) -choice exiting working: next a.done(exit), b.done(exit) (unordered; took a.done(exit) first) -exit: done -exit: done -exit: working -enter: outside (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) A(entry) B(entry) " - eval literal "outside(entry) " -> "outside(entry) " - eval operator + -> "A(entry) A(entry) B(entry) outside(entry) " -transition: working -> outside diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json index fd6e640037..77faec7afd 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.expected.json @@ -8,20 +8,9 @@ {"signal": "FinishA"}, {"signal": "FinishB"} ], - "outcomes": [ - { - "finalState": "outside", - "outputs": { - "log": {"type": "String", "value": "A(entry) A(entry) B(entry) outside(entry) "} - } - }, - { - "finalState": "outside", - "outputs": { - "log": {"type": "String", "value": "A(entry) B(entry) A(entry) outside(entry) "} - } - } - ], - "admissible": "Regions of a parallel state entered on one occurrence: each is entered, in which order is open", + "finalState": "outside", + "outputs": { + "log": {"type": "String", "value": "A(entry) outside(entry) "} + }, "trace": true } diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden deleted file mode 100644 index b0ca3c499f..0000000000 --- a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.seed-1.trace.golden +++ /dev/null @@ -1,49 +0,0 @@ -exit: idle -enter: working -enter: A (entry action) -stmt action body - stmt assign log - eval feature log -> "" - eval literal "A(entry) " -> "A(entry) " - eval operator + -> "A(entry) " -transition: idle -> working (event: accept Go) -exit: A -enter: done -transition: A -> done (event: accept FinishA) -exit: done -exit: working -enter: idle -transition: working -> idle (event: accept Back) -exit: idle -enter: working -choice entering working: next A(entry), B(entry) (unordered; took B(entry) first) -enter: B (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) " - eval literal "B(entry) " -> "B(entry) " - eval operator + -> "A(entry) B(entry) " -enter: A (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) B(entry) " - eval literal "A(entry) " -> "A(entry) " - eval operator + -> "A(entry) B(entry) A(entry) " -transition: idle -> B (event: accept Direct) -exit: A -enter: done -transition: A -> done (event: accept FinishA) -exit: B -enter: done -transition: B -> done (event: accept FinishB) -choice exiting working: next a.done(exit), b.done(exit) (unordered; took a.done(exit) first) -exit: done -exit: done -exit: working -enter: outside (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) B(entry) A(entry) " - eval literal "outside(entry) " -> "outside(entry) " - eval operator + -> "A(entry) B(entry) A(entry) outside(entry) " -transition: working -> outside diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden index 8bdad6063b..7477351412 100644 --- a/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_parallel_region_without_entry_inactive.trace.golden @@ -12,38 +12,10 @@ enter: done transition: A -> done (event: accept FinishA) exit: done exit: working -enter: idle -transition: working -> idle (event: accept Back) -exit: idle -enter: working -choice entering working: next A(entry), B(entry) (unordered; took A(entry) first) -enter: A (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) " - eval literal "A(entry) " -> "A(entry) " - eval operator + -> "A(entry) A(entry) " -enter: B (entry action) -stmt action body - stmt assign log - eval feature log -> "A(entry) A(entry) " - eval literal "B(entry) " -> "B(entry) " - eval operator + -> "A(entry) A(entry) B(entry) " -transition: idle -> B (event: accept Direct) -exit: A -enter: done -transition: A -> done (event: accept FinishA) -exit: B -enter: done -transition: B -> done (event: accept FinishB) -choice exiting working: next a.done(exit), b.done(exit) (unordered; took a.done(exit) first) -exit: done -exit: done -exit: working enter: outside (entry action) stmt action body stmt assign log - eval feature log -> "A(entry) A(entry) B(entry) " + eval feature log -> "A(entry) " eval literal "outside(entry) " -> "outside(entry) " - eval operator + -> "A(entry) A(entry) B(entry) outside(entry) " + eval operator + -> "A(entry) outside(entry) " transition: working -> outside From 742792eaf689c2d0983e0ccba350915355c67601 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:52:53 +0000 Subject: [PATCH 13/19] fix(runtime): enter a region without a start through an empty history Co-Authored-By: jason.han --- .../census-states-events.changed.md | 2 +- docs/project/spec-compliance.md | 2 +- internal/exec/runtime/state_executor.go | 5 ++-- ..._first_entry_inactive_region.expected.json | 12 ++++++++++ ..._history_first_entry_inactive_region.sysml | 24 +++++++++++++++++++ ...y_first_entry_inactive_region.trace.golden | 21 ++++++++++++++++ 6 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.trace.golden diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index c1676ce9d5..3e5f27220a 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ - **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. -- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. Terminate pseudostates now terminate the whole state machine. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. A first entry through history uses the same default, while an owner with no default entry remains an error. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 96ddc7886d..18342e7fa7 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -972,7 +972,7 @@ nothing above. | Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork follows default entry: its stand-in is entered even without a start; if no child becomes active, the region counts as complete, as does a region with no stand-in and no start (PSSM *Entering 004*). A later fork branch that activates a region must reach its final state before the owner completes. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region with a start does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. A running do-action still defers completion. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `lower/state_footprint.go` `stateFootprintBuilder.pseudostate` (fork case), `entersRegion`; `lower/state_graph.go` `ownTransitionEffects`; `state_executor.go` `fireForkTransition`, `leaveForFork`, `exitRegionsOf`, `completeIfDone`, `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`; `state_region_entry.go` `forkEntry`, `enterForkBranches`, `enterRegion`, `enterLazily`, `regionStart` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_region_without_entry_completes.sysml` + trace golden, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | | Join pseudostates: each occurrence fires every not-yet-arrived segment it enables and whose region's dispatch chose it, exiting each source, running its effect and recording each in `joinArrived` only after the firing succeeds; when multiple segments fire, they are drawn in `join ` order. At selection, including for each time expiry, probes count only the candidate and prior arrivals, checking the way out only if they complete the join. In signal/change dispatches, same-occurrence peers count only when their regions chose them; firing checks completion against those peers, and if the route is dead, none fire. Timer expiries are separate occurrences even at the same instant; a dead completing segment is disabled, leaving its source's other timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). The join completes when prior arrivals plus segments enabled by the current occurrence and chosen by their regions in that dispatch cover every incoming segment and the outgoing route is available. The current segments are drawn as `join `, arrivals clear and the owner is left after the last segment effect. Completion segments fire only on their own completion; waiting regions do not complete their owner; arrivals clear on region re-entry or when another transition leaves the owner. Snapshots, held images and canonical check state preserve arrivals. PSSM leaves the owner before the final incoming effect; leaving after that effect is the extension's reading recorded in SM34. `TransitionPerformances.kerml` says `private succession [1] transitionLinkSource then [*] effect;`; `StatePerformances.kerml` says `and hence happening during the state performance.` Neither supplies a cross-segment arrival rule; that is the UML/PSSM extension reference | `state_route.go` `joinFiringNow`; `state_executor.go` `fireJoinTransition`, `fireJoinSegment`, `fireJoinIncoming`, `leaveJoinOwner`, `regionWaitingAtJoin`, `setRegionState`, `clearJoinArrivalsOwnedBy`; `snapshot.go`, `held_image_behavior.go`, `check_state.go` preserve `joinArrived` | Conformance fixtures: `state_fork_join_pseudostate`, `state_join_arrival_dropped_when_owner_exited`, `state_join_change_segment_rises_alone`, `state_join_change_segment_waits_before_junction`, `state_join_change_segment_waits_before_junction_metadata`, `state_join_change_segments_rise_together`, `state_join_completion_is_not_a_timers_expiry`, `state_join_completion_segment_waits_for_do_behavior`, `state_join_dead_timer_join_keeps_group_alternative`, `state_join_exit_from_regions_before_owner_exit`, `state_join_from_composite_sources`, `state_join_from_nested_states`, `state_join_no_way_out_disables_last_segment`, `state_join_of_machine_regions_from_nested_source`, `state_join_runs_every_incoming_effect`, `state_join_segment_fires_on_own_signal`, `state_join_segments_arrive_together_on_one_occurrence`, `state_join_segment_not_chosen_does_not_arrive`, `state_join_peer_not_chosen_does_not_block_arrival`, `state_join_segment_reads_its_payload`, `state_join_segment_trigger_unmatched`, `state_join_time_segment_needs_same_occurrence`, `state_join_time_segment_not_taken_by_signal`, `state_join_time_segment_unsynchronized_reads_no_route`, `state_join_time_segment_unsynchronized_reads_no_route_metadata`, `state_join_time_segments_expire_apart`, `state_join_time_segments_expire_together`, `state_join_waits_for_every_segment_enabled`; tests: `pseudostate_test.go:TestForkAndJoinPseudostates`, `:TestJoinWaitsForEveryBranch`, `:TestJoinSegmentFiresOnOwnOccurrence`, `:TestJoinDisarmedSegmentCanArriveAlone`, `:TestJoinFromActiveCompositeSourcesFires`, `:TestJoinCompletionSegmentWaitsForItsSourcesDoBehavior`, `:TestJoinCompletionSegmentWaitsForItsSourcesBody`; `fork_join_test.go:TestForkJoinVisitOrderIsDeterministic`; `choice_test.go:TestCompletionTransitionArrivesAtUnreadyJoin`; `state_change_trigger_test.go:TestChangeTriggerDisarmedSegmentCanWaitForItsPeer`; `join_snapshot_test.go:TestSnapshotRestoresJoinArrivals`, `:TestCloneNilJoinArrivalsIsWritable`; `check_test.go:TestCheckJoinWaitsForSlowestBranch`; `replay_test.go:TestReplayRefusedJoinDrawChangesNothing`, `:TestReplayRefusedJoinDrawUndoesTheSegmentsChoice`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead signal and timer join exits, exit failure) | ✅ 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 ;`) and `lower/state_metadata.go` (`#shallowHistory state`/`#deepHistory state`); `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 | +| History pseudostates (shallow and deep). A region's last active configuration is recorded and restored; a region or body left at `done` leaves no history. With no recorded configuration, history takes its own outgoing transition if present, otherwise it performs the owner's default entry through its entry transition or region defaults. A region without a stand-in and start is a valid empty default; its composite owner can be entered through history and the region remains inactive. A top-level machine history or owner with no default entry at all yields typed `ErrHistoryWithoutEntry` naming the history and owner | `parser/behavior.go` parseStateMember; `lower/state_metadata.go`; `state_executor.go` `historyEntry`, `hasDefaultEntry`, `historyRecorded`, `deepestRecorded`, `exitState`, `recordChildHistory`, `recordRegionHistory`, `forgetRegionHistory`; `errors.go` `ErrHistoryWithoutEntry`; `state_region_transition.go` `leaveRegion`, `exitRegionTo`; `lower/state_graph.go` `PseudostateOwner` | `state_history_first_entry_inactive_region.sysml` + trace golden, `state_history_revisit.sysml` + trace golden, `state_shallow_history.sysml`, `state_deep_history.sysml`; `history_test.go` history restoration tests; `robustness_test.go: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 | | A transition between two regions of one composite state exits its source only: KerML `StatePerformances::StateTransitionPerformance` orders `private succession [*] guard then [1] transitionLinkSource.exit`, so the composite state is neither exited nor re-entered, the source's region is left without an active state, and the regions holding neither endpoint keep theirs. A target nested inside the target region's own composite state moves that inner region, exiting the state it was running. A region whose target is nested inside a composite state it is not running records that composite state as its active one. A source active in a region nested deeper than its target's region leaves its region set up to the level the two share, exiting the composite state holding its own region. The level is found by walking outward through the region declaring the region owner's nearest region-declared ancestor, so a region owned by a plain substate is not missed. A target outside the composite state still exits it and its regions — each state on the way out exactly once, the region a state is active in being cleared before that state is exited, since a region's active state may be nested below it — and a nested non-orthogonal transition still exits up to the endpoints' least common ancestor | `state_region_transition.go` `fireTransitionInRegion`, `concurrentRegionsFor`, `enclosingRegion`, `siblingRegionContaining`, `moveBetweenRegions`, `exitRegionTo`, `leaveRegion`, `isBelowOrEqual`; `state_executor.go` `getLCA` (nested and outward transitions only) | `state_transition_cross_region.sysml` + trace golden, `state_transition_cross_region_third_region.sysml`, `state_transition_cross_region_nested_target.sysml`, `state_transition_cross_region_inactive_wrapper.sysml`, `state_transition_cross_region_deep_source.sysml`, `state_transition_cross_region_substate_owner.sysml`, `state_transition_leave_composite_substate_region.sysml`, `state_transition_sibling_region.sysml`, `state_region_cross_pseudostate.sysml` + trace golden, `cross_region_transition_test.go:TestCrossRegionTransitionExitsSourceOnly`, `:TestCrossRegionTransitionIntoNestedTargetExitsTheAbandonedState`, `:TestCrossRegionTransitionIntoInactiveCompositeRecordsTheEnteredState`, `:TestCrossRegionTransitionFromDeeperRegionExitsUpToTheSharedLevel`, `:TestCrossRegionTransitionFromARegionOwnedByASubstate`, `:TestNestedTransitionExitsUpToTheLCA`, `:TestTransitionOutOfCompositeStateExitsEveryRegion`, `:TestTransitionOutOfCompositeStateExitsNestedStatesOnce`, `region_pseudostate_test.go:TestRegionPseudostateIntoSiblingRegionExitsSourceOnly`, `robustness_test.go:state_cross_region_transitions_ping_pong` | ✅ Faithful | diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index 7c61a2cc21..6051ae4f40 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -2995,14 +2995,15 @@ func (e *StateExecutor) historyEntry(hist *ast.PseudostateNode, owner *ast.State } // hasDefaultEntry reports whether entering state with no branch chosen has a -// state to start in: an entry transition of its body, or of each of its regions. +// valid default configuration, including regions left empty by default entry. func (e *StateExecutor) hasDefaultEntry(state *ast.StateNode) bool { regions, orthogonal := e.graph.CompositeStates[state] if !orthogonal { return len(e.graph.StartOf(state)) > 0 } for _, region := range regions { - if e.graph.RegionState[region] == nil && len(e.graph.StartOf(region)) == 0 { + standIn := e.graph.RegionState[region] + if standIn != nil && len(e.graph.CompositeStates[standIn]) > 0 && len(e.graph.StartOf(region)) == 0 { return false } } diff --git a/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.expected.json b/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.expected.json new file mode 100644 index 0000000000..4a0ce11463 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.expected.json @@ -0,0 +1,12 @@ +{ + "type": "state", + "events": [ + {"signal": "Enter"}, + {"signal": "Go"} + ], + "finalState": "finished", + "outputs": { + "log": {"type": "String", "value": "A1(entry);finished(entry);"} + }, + "trace": true +} diff --git a/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.sysml b/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.sysml new file mode 100644 index 0000000000..96f0ed60c9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.sysml @@ -0,0 +1,24 @@ +package HistoryFirstEntryInactiveRegion { + attribute def Enter; + attribute def Go; + + state Machine { + attribute log : String = ""; + entry; then idle; + state idle; + state P parallel { + history resume; + state a { + entry; then A1; + state A1 { entry { assign log := log + "A1(entry);"; } } + transition first A1 accept Go then done; + } + state b { + state B1 { entry { assign log := log + "B1(entry);"; } } + } + } + state finished { entry { assign log := log + "finished(entry);"; } } + transition first idle accept Enter then P::resume; + transition first P then finished; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.trace.golden b/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.trace.golden new file mode 100644 index 0000000000..c5bb9dd149 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_history_first_entry_inactive_region.trace.golden @@ -0,0 +1,21 @@ +exit: idle +enter: P +enter: A1 (entry action) +stmt action body + stmt assign log + eval feature log -> "" + eval literal "A1(entry);" -> "A1(entry);" + eval operator + -> "A1(entry);" +transition: idle -> P (event: accept Enter) +exit: A1 +enter: done +transition: A1 -> done (event: accept Go) +exit: done +exit: P +enter: finished (entry action) +stmt action body + stmt assign log + eval feature log -> "A1(entry);" + eval literal "finished(entry);" -> "finished(entry);" + eval operator + -> "A1(entry);finished(entry);" +transition: P -> finished From 8c807a1baa69718a9c9c4ba3796419144c8081ea Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:02:20 +0000 Subject: [PATCH 14/19] fix(migrate): keep opaque body whitespace in textual representations Co-Authored-By: jason.han --- .../census-states-events.changed.md | 2 +- internal/translate/migrate/behavior.go | 2 +- internal/translate/migrate/values.go | 20 +++++- tests/migrate/opaque_migration_test.go | 4 +- tests/migrate/opaque_textual_rep_test.go | 55 +++++++++++++--- tests/migrate/owner_reads_test.go | 2 +- tests/migrate/states_test.go | 2 +- .../testdata/xmi/acquisition.golden.sysml | 15 +++-- .../testdata/xmi/calc_context.golden.sysml | 2 +- .../testdata/xmi/diagram_edges.golden.sysml | 2 +- .../xmi/diagram_edges.layout.golden.sysml | 2 +- .../testdata/xmi/diagrams.golden.sysml | 6 +- .../testdata/xmi/empty_behaviors.golden.sysml | 10 +-- .../migrate/testdata/xmi/layout.golden.sysml | 2 +- .../testdata/xmi/layout.layout.golden.sysml | 2 +- tests/migrate/testdata/xmi/meter.golden.sysml | 63 +++++++++---------- .../testdata/xmi/montecarlo_case.golden.sysml | 4 +- .../testdata/xmi/nested_context.golden.sysml | 2 +- .../testdata/xmi/object_flows.golden.sysml | 43 +++++++------ tests/migrate/testdata/xmi/plant.golden.sysml | 24 +++---- .../testdata/xmi/plant_states.golden.sysml | 4 +- .../migrate/testdata/xmi/reactor.golden.sysml | 48 +++++++------- .../xmi/script_guarded_reads.golden.sysml | 14 ++--- .../testdata/xmi/script_locals.golden.sysml | 11 ++-- .../xmi/script_unset_reads.golden.sysml | 11 ++-- .../testdata/xmi/station_points.golden.sysml | 48 +++++++------- .../xmi/submachine_params.golden.sysml | 2 +- 27 files changed, 220 insertions(+), 182 deletions(-) diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index 3e5f27220a..71ffb0e5be 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ -- **Preserve UML opaque body text during migration.** Each body and matching language now appear as a non-executed textual representation, while supported body translations remain executable. +- **Preserve UML opaque body text during migration.** Each raw body and matching language now appear as a non-executed textual representation with whitespace preserved, while supported body translations continue to use the trimmed first body and remain executable. - **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. A first entry through history uses the same default, while an owner with no default entry remains an error. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/internal/translate/migrate/behavior.go b/internal/translate/migrate/behavior.go index bc8a2627cb..cb0807abf6 100644 --- a/internal/translate/migrate/behavior.go +++ b/internal/translate/migrate/behavior.go @@ -1025,7 +1025,7 @@ func (m *migration) opaqueBehaviorBody(e, scope *sysmlv1.Element) { func (m *migration) textualReps(e *sysmlv1.Element) { for _, pair := range opaqueBodyPairs(e) { if pair.body != "" && pair.language != "" { - m.w.lines(prefixFirst("rep language "+stringLiteral(pair.language)+" ", commentLines(pair.body))) + m.w.lines(prefixFirst("rep language "+stringLiteral(pair.language)+" ", opaqueRepresentationCommentLines(pair.body))) } } } diff --git a/internal/translate/migrate/values.go b/internal/translate/migrate/values.go index 37bcf10673..c676d5eb25 100644 --- a/internal/translate/migrate/values.go +++ b/internal/translate/migrate/values.go @@ -376,11 +376,18 @@ func opaqueTexts(v *sysmlv1.Element, role string) []string { if len(owned) > 0 { texts := make([]string, len(owned)) for i, e := range owned { - texts[i] = strings.TrimSpace(e.Text) + texts[i] = e.Text + if role == "language" { + texts[i] = strings.TrimSpace(texts[i]) + } } return texts } - if text := strings.TrimSpace(v.Attrs[role]); text != "" { + text := v.Attrs[role] + if role == "language" { + text = strings.TrimSpace(text) + } + if text != "" { return []string{text} } return nil @@ -388,7 +395,7 @@ func opaqueTexts(v *sysmlv1.Element, role string) []string { func opaqueBodiesEmpty(v *sysmlv1.Element) bool { for _, pair := range opaqueBodyPairs(v) { - if pair.body != "" { + if strings.TrimSpace(pair.body) != "" { return false } } @@ -1267,3 +1274,10 @@ func commentLines(text string) []string { out = append(out, " */") return out } + +func opaqueRepresentationCommentLines(text string) []string { + text = strings.ReplaceAll(text, "*/", "* /") + text = strings.ReplaceAll(strings.ReplaceAll(text, "\r\n", "\n"), "\r", "\n") + comment, _ := source.CommentText(text, "") + return strings.Split(comment, "\n") +} diff --git a/tests/migrate/opaque_migration_test.go b/tests/migrate/opaque_migration_test.go index a991ff1c67..92379f2162 100644 --- a/tests/migrate/opaque_migration_test.go +++ b/tests/migrate/opaque_migration_test.go @@ -392,7 +392,7 @@ func TestTranslatedOutputPinsFeedTheirFlows(t *testing.T) { wantNote(t, r, "_shout", migrate.Approximated, "the JavaScript body is translated to v2; the console print println(…) is left out, as it writes to the tool's console and changes nothing of the model") wantLine(t, r.Notation, "assign ticks := ticks + 1;") wantLine(t, r.Notation, "action shout {") - wantLine(t, r.Notation, `rep language "JavaScript" /* println("done"); */`) + wantLine(t, r.Notation, `rep language "JavaScript" /* println("done");*/`) s := session(t, r) meta(t, s, "%instantiate Meter") @@ -435,7 +435,7 @@ func TestNonScalarFeaturesAndScriptLiterals(t *testing.T) { `/* default value not migrated: {JavaScript} Math.sqrt(total) — the types at "Math.sqrt(total)" disagree: the expression is a Real, not the Boolean wanted */`, "in limit : ScalarValues::Integer[1] default = count + 1;", "/* guard not migrated: [{JavaScript} ready and enabled] — the text \"and\" is not expression syntax: text follows the expression */", - `rep language "JavaScript" /* flag = ready and enabled */`, + `rep language "JavaScript" /* flag = ready and enabled*/`, "assign flag := ready and enabled;", } { wantLine(t, r.Notation, line) diff --git a/tests/migrate/opaque_textual_rep_test.go b/tests/migrate/opaque_textual_rep_test.go index 2e597ac9f3..9c36f4a5e2 100644 --- a/tests/migrate/opaque_textual_rep_test.go +++ b/tests/migrate/opaque_textual_rep_test.go @@ -4,6 +4,9 @@ import ( "strings" "testing" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" "github.com/Open-MBEE/OpenSysML/internal/translate/migrate" ) @@ -22,14 +25,14 @@ func opaqueActionDocument(t *testing.T, body string) *migrate.Result { func TestOpaqueActionTextualRepresentations(t *testing.T) { t.Run("translated", func(t *testing.T) { r := opaqueActionDocument(t, `JavaScriptx = 1;`) - wantLine(t, r.Notation, `rep language "JavaScript" /* x = 1; */`) + wantLine(t, r.Notation, `rep language "JavaScript" /* x = 1;*/`) wantLine(t, r.Notation, "assign x := 1;") wantClean(t, "t.sysml", r) }) t.Run("untranslated", func(t *testing.T) { r := opaqueActionDocument(t, `Javawork();`) - wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantLine(t, r.Notation, `rep language "Java" /* work();*/`) wantNoLine(t, r.Notation, "body not migrated") wantNote(t, r, "_opaque", migrate.Approximated, "the body is kept as a textual representation, which is not executed") wantClean(t, "t.sysml", r) @@ -45,8 +48,8 @@ func TestOpaqueActionTextualRepresentations(t *testing.T) { t.Run("pairs by index", func(t *testing.T) { r := opaqueActionDocument(t, `Javafirst();Pythonsecond()`) - wantLine(t, r.Notation, `rep language "Java" /* first(); */`) - wantLine(t, r.Notation, `rep language "Python" /* second() */`) + wantLine(t, r.Notation, `rep language "Java" /* first();*/`) + wantLine(t, r.Notation, `rep language "Python" /* second()*/`) wantNoLine(t, r.Notation, "body not migrated") wantClean(t, "t.sysml", r) }) @@ -60,7 +63,7 @@ func TestOpaqueActionTextualRepresentations(t *testing.T) {
`, "") - wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantLine(t, r.Notation, `rep language "Java" /* work();*/`) wantClean(t, "t.sysml", r) }) @@ -74,7 +77,7 @@ func TestOpaqueActionTextualRepresentations(t *testing.T) { t.Run("comment terminator", func(t *testing.T) { r := opaqueActionDocument(t, `Pythonprint("*/")`) - wantLine(t, r.Notation, `rep language "Python" /* print("* /") */`) + wantLine(t, r.Notation, `rep language "Python" /* print("* /")*/`) wantClean(t, "t.sysml", r) }) } @@ -87,7 +90,7 @@ func TestOpaqueBehaviorTextualRepresentations(t *testing.T) { work();
`, "") wantLine(t, r.Notation, "action def Work {") - wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantLine(t, r.Notation, `rep language "Java" /* work();*/`) wantNoLine(t, r.Notation, "body not migrated") wantNote(t, r, "_behavior", migrate.Approximated, "the body is kept as a textual representation, which is not executed") wantClean(t, "t.sysml", r) @@ -99,7 +102,7 @@ func TestOpaqueBehaviorTextualRepresentations(t *testing.T) { JavaScript 1 + 2
`, "") - wantLine(t, r.Notation, `rep language "JavaScript" /* 1 + 2 */`) + wantLine(t, r.Notation, `rep language "JavaScript" /* 1 + 2*/`) rep := strings.Index(string(r.Notation), `rep language "JavaScript"`) result := strings.LastIndex(string(r.Notation), "1 + 2") if rep < 0 || result < 0 || rep >= result { @@ -130,7 +133,41 @@ func TestOpaqueBehaviorTextualRepresentations(t *testing.T) { work();
`, ``) - wantLine(t, r.Notation, `rep language "Java" /* work(); */`) + wantLine(t, r.Notation, `rep language "Java" /* work();*/`) wantClean(t, "t.sysml", r) }) } + +func TestOpaqueRepresentationBodyRoundTrip(t *testing.T) { + for _, body := range []struct { + name string + text string + }{ + {name: "leading indentation and trailing spaces", text: " command\n argument \n"}, + {name: "leading blank line", text: "\n command\n argument \n"}, + } { + t.Run(body.name, func(t *testing.T) { + r := opaqueActionDocument(t, `Java`+body.text+``) + sf := source.New("migrated.sysml", r.Notation) + p := parser.New(sf) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("migrated notation did not parse: %v", p.Diagnostics) + } + var bodies []string + ast.Inspect(root, func(n ast.Node) bool { + if rep, ok := n.(*ast.TextualRepresentation); ok { + bodies = append(bodies, source.CommentBody(sf.Text(rep.BodySpan))) + } + return true + }) + // source.CommentBody drops the comment delimiters, opener whitespace through its first line terminator, and each continuation line's indentation, `*`, and one following space. + if len(bodies) != 1 { + t.Fatalf("found %d textual representations, want one", len(bodies)) + } + if bodies[0] != body.text { + t.Errorf("parsed representation body = %q, want %q\n%s", bodies[0], body.text, r.Notation) + } + }) + } +} diff --git a/tests/migrate/owner_reads_test.go b/tests/migrate/owner_reads_test.go index 5873760b57..206c620d84 100644 --- a/tests/migrate/owner_reads_test.go +++ b/tests/migrate/owner_reads_test.go @@ -331,7 +331,7 @@ func TestLocalsABodyDeclaresAreNoReadsOfTheOwner(t *testing.T) { r := migrateDocument(t, localsModel, ``) for _, line := range []string{ "action def Compute {", - `rep language "JavaScript" /* let count = 1; count += 1; */`, + `rep language "JavaScript" /* let count = 1; count += 1;*/`, "action tally {", "attribute n : ScalarValues::Integer;", "assign count := n + 1;", diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index cbbc23ab11..1d09b85b35 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -33,7 +33,7 @@ func TestStateMachineCrossRegionTransitionsAndPseudostates(t *testing.T) { "transition first Idle accept Resume then Work.steps.Run;", "transition first Pause accept Resume", "do action log {", - `rep language "JavaScript" /* println("resuming at " + count); */`, + `rep language "JavaScript" /* println("resuming at " + count);*/`, "then Work.steps.last;", "transition first Idle accept Enter then Cell.warmStart;", "transition first Cell.spent then Idle;", diff --git a/tests/migrate/testdata/xmi/acquisition.golden.sysml b/tests/migrate/testdata/xmi/acquisition.golden.sysml index 4d1185e1d5..395716ab74 100644 --- a/tests/migrate/testdata/xmi/acquisition.golden.sysml +++ b/tests/migrate/testdata/xmi/acquisition.golden.sysml @@ -44,13 +44,13 @@ part def Observatory { } first stamp9 then 'start timer'; action 'start timer' { - rep language "JavaScript" /* Time_Acq_Total = simtime; */ + rep language "JavaScript" /* Time_Acq_Total = simtime;*/ assign Time_Acq_Total := localClock.currentTime; } first 'start timer' then seed; action seed { in Retries : ScalarValues::Integer[1] = 2; - rep language "JavaScript" /* GS_Found = Retries < 1; */ + rep language "JavaScript" /* GS_Found = Retries < 1;*/ assign tcs.GS_Found := Retries < 1; } first seed then stamp; @@ -63,8 +63,7 @@ part def Observatory { first stamp then 'first attempt'; action 'first attempt' { rep language "JavaScript" /* i = 1; - * GS_Found = false; - */ + * GS_Found = false;*/ assign tcs.i := 1; assign tcs.GS_Found := false; } @@ -120,7 +119,7 @@ part def Observatory { } first stamp4 then 'guide star found'; action 'guide star found' { - rep language "JavaScript" /* GS_Found = true; */ + rep language "JavaScript" /* GS_Found = true;*/ assign tcs.GS_Found := true; } first 'guide star found' then stamp2; @@ -134,7 +133,7 @@ part def Observatory { } first stamp2 then merge4; action 'next attempt' { - rep language "JavaScript" /* i += 1; */ + rep language "JavaScript" /* i += 1;*/ assign tcs.i := tcs.i + 1; } first 'next attempt' then 'merge'; @@ -143,7 +142,7 @@ part def Observatory { } first stamp7 then abort; action abort { - rep language "JavaScript" /* GS_Found = false; */ + rep language "JavaScript" /* GS_Found = false;*/ assign tcs.GS_Found := false; } first abort then stamp8; @@ -157,7 +156,7 @@ part def Observatory { first fork2 then merge3; first fork2 then merge2; action 'stop timer' { - rep language "JavaScript" /* Time_Acq_Total = simtime - Time_Acq_Total; */ + rep language "JavaScript" /* Time_Acq_Total = simtime - Time_Acq_Total;*/ assign Time_Acq_Total := localClock.currentTime - Time_Acq_Total; } first 'stop timer' then stamp10; diff --git a/tests/migrate/testdata/xmi/calc_context.golden.sysml b/tests/migrate/testdata/xmi/calc_context.golden.sysml index 307042d786..620acbf8ed 100644 --- a/tests/migrate/testdata/xmi/calc_context.golden.sysml +++ b/tests/migrate/testdata/xmi/calc_context.golden.sysml @@ -4,7 +4,7 @@ part def Tank { in ref context : Tank[1]; in factor : ScalarValues::Real[1]; out result : ScalarValues::Real[1]; - rep language "English" /* level * factor */ + rep language "English" /* level * factor*/ context.level * factor } } diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml index 9002b34214..694baa5f70 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml @@ -86,7 +86,7 @@ package Behavior { state Idle; state Running { do action spin { - rep language "English" /* keep turning */ + rep language "English" /* keep turning*/ } } state Halted; diff --git a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml index e4169bfecb..52e0bac087 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml @@ -114,7 +114,7 @@ package Behavior { state Idle; state Running { do action spin { - rep language "English" /* keep turning */ + rep language "English" /* keep turning*/ } } state Halted; diff --git a/tests/migrate/testdata/xmi/diagrams.golden.sysml b/tests/migrate/testdata/xmi/diagrams.golden.sysml index 0c1d67e23a..188fa9d42d 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.sysml +++ b/tests/migrate/testdata/xmi/diagrams.golden.sysml @@ -33,7 +33,7 @@ package Structure { } succession 'start to ignite' first start then ignite; action ignite { - rep language "JavaScript" /* ignited = true; */ + rep language "JavaScript" /* ignited = true;*/ attribute ignited : ScalarValues::Boolean; assign ignited := true; } @@ -48,7 +48,7 @@ package Structure { expose Engine; render Views::asTextualNotation; } - rep language "JavaScript" /* ignited = false; */ + rep language "JavaScript" /* ignited = false;*/ first start then body; action body { attribute ignited : ScalarValues::Boolean; @@ -94,7 +94,7 @@ package Behavior { } succession 'start to start engine' first start then 'start engine'; action 'start engine' { - rep language "JavaScript" /* running = true; */ + rep language "JavaScript" /* running = true;*/ attribute running : ScalarValues::Boolean; assign running := true; } diff --git a/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml b/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml index c98d755a9a..72242ecd6a 100644 --- a/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml +++ b/tests/migrate/testdata/xmi/empty_behaviors.golden.sysml @@ -19,7 +19,7 @@ occurrence def Gate { } } exit action flush { - rep language "Fortran" /* CALL FLUSH(count) */ + rep language "Fortran" /* CALL FLUSH(count)*/ } } state Busy { @@ -38,13 +38,13 @@ occurrence def Gate { metadata MigrationMetadata::StandIn about 'fork'; } do action poll { - rep language "Fortran" /* CALL POLL(count) */ + rep language "Fortran" /* CALL POLL(count)*/ } exit action close; } state Cool { entry action vent { - rep language "Fortran" /* CALL VENT(count) */ + rep language "Fortran" /* CALL VENT(count)*/ } do action wait; exit action seal { @@ -73,7 +73,7 @@ occurrence def Gate { then Cool; transition first Cool accept Go do action effect { - rep language "Fortran" /* CALL WARM(count) */ + rep language "Fortran" /* CALL WARM(count)*/ } then Warm; transition first Warm @@ -89,7 +89,7 @@ occurrence def Gate { then Off; transition first Off do action effect { - rep language "Fortran" /* CALL RESTART(count) */ + rep language "Fortran" /* CALL RESTART(count)*/ } then Idle; transition first Off accept Go diff --git a/tests/migrate/testdata/xmi/layout.golden.sysml b/tests/migrate/testdata/xmi/layout.golden.sysml index f29ab16b5f..acc888abdf 100644 --- a/tests/migrate/testdata/xmi/layout.golden.sysml +++ b/tests/migrate/testdata/xmi/layout.golden.sysml @@ -30,7 +30,7 @@ package Behavior { action def Drive { succession 'start to step' first start then 'step'; action 'step' { - rep language "JavaScript" /* going = true; */ + rep language "JavaScript" /* going = true;*/ attribute going : ScalarValues::Boolean; assign going := true; } diff --git a/tests/migrate/testdata/xmi/layout.layout.golden.sysml b/tests/migrate/testdata/xmi/layout.layout.golden.sysml index 6473fad7fe..2d124e047c 100644 --- a/tests/migrate/testdata/xmi/layout.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/layout.layout.golden.sysml @@ -38,7 +38,7 @@ package Behavior { action def Drive { succession 'start to step' first start then 'step'; action 'step' { - rep language "JavaScript" /* going = true; */ + rep language "JavaScript" /* going = true;*/ attribute going : ScalarValues::Boolean; assign going := true; } diff --git a/tests/migrate/testdata/xmi/meter.golden.sysml b/tests/migrate/testdata/xmi/meter.golden.sysml index 7d2e413c6d..f1f290389d 100644 --- a/tests/migrate/testdata/xmi/meter.golden.sysml +++ b/tests/migrate/testdata/xmi/meter.golden.sysml @@ -59,18 +59,18 @@ part def Meter { action sense { in x : ScalarValues::Real[1] = 1.5; out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = x * 2; */ + rep language "JavaScript" /* y = x * 2;*/ assign y := x * 2; } action record { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* total = v + 1; peak = Math.max(v); */ + rep language "JavaScript" /* total = v + 1; peak = Math.max(v);*/ assign total := v + 1; assign peak := v; } first record then split; action split { - rep language "Java" /* half = ticks / 2; ratio = total / 2; floored = Math.floor(total) / 8; rounded = Math.round(total) / 8; */ + rep language "Java" /* half = ticks / 2; ratio = total / 2; floored = Math.floor(total) / 8; rounded = Math.round(total) / 8;*/ assign half := OpenSysMLMathFunctions::quotient(ticks, 2); assign ratio := total / 2; assign floored := RealFunctions::floor(total) / 8; @@ -78,12 +78,12 @@ part def Meter { } first split then quarterly; action quarterly { - rep language "Java 1.8.0_202" /* quarter = ticks / 4; */ + rep language "Java 1.8.0_202" /* quarter = ticks / 4;*/ assign quarter := OpenSysMLMathFunctions::quotient(ticks, 4); } first quarterly then cc; action cc { - rep language "JavaCC" /* eighth = ticks / 8; */ + rep language "JavaCC" /* eighth = ticks / 8;*/ assign eighth := ticks / 8; } first cc then judge; @@ -91,20 +91,19 @@ part def Meter { in ok : ScalarValues::Boolean[1]; in n : ScalarValues::Integer[1] = RealFunctions::floor(total); in m : ScalarValues::Integer[1] = 2; - rep language "JavaScript" /* count = n + m; */ + rep language "JavaScript" /* count = n + m;*/ assign count := n + m; } first judge then log; action log { rep language "JavaScript" /* print("total so far: " + (total + 1)); * ticks = ticks + 1; - * println(ticks); - */ + * println(ticks);*/ assign ticks := ticks + 1; } first log then shout; action shout { - rep language "JavaScript" /* println("done"); */ + rep language "JavaScript" /* println("done");*/ } first shout then final; action final terminate; @@ -115,24 +114,24 @@ part def Meter { first start then idle; action idle { out z : ScalarValues::Real[1]; - rep language "JavaScript" /* total = total * 1; */ + rep language "JavaScript" /* total = total * 1;*/ assign total := total * 1; } first idle then sink; action sink { in w : ScalarValues::Real[0..1]; - rep language "JavaScript" /* total = w; */ + rep language "JavaScript" /* total = w;*/ if w->SequenceFunctions::notEmpty() { assign total := w; } } first sink then dark; action dark { out q : ScalarValues::Real[1]; - rep language "Python" /* q = 1 */ + rep language "Python" /* q = 1*/ } first dark then drain; action drain { in w : ScalarValues::Real[0..1]; - rep language "JavaScript" /* total = w; */ + rep language "JavaScript" /* total = w;*/ if w->SequenceFunctions::notEmpty() { assign total := w; } } first drain then final; @@ -144,43 +143,43 @@ part def Meter { action flip { first start then bump; action bump { - rep language "JavaScript" /* count = mode + 1; */ + rep language "JavaScript" /* count = mode + 1;*/ } first bump then pick; action pick { - rep language "JavaScript" /* count = mode; */ + rep language "JavaScript" /* count = mode;*/ } first pick then match; action match { - rep language "JavaScript" /* mode = dial; */ + rep language "JavaScript" /* mode = dial;*/ } first match then widen; action widen { - rep language "JavaScript" /* dial = hand; */ + rep language "JavaScript" /* dial = hand;*/ assign dial := hand; } first widen then narrow; action narrow { - rep language "JavaScript" /* hand = dial; */ + rep language "JavaScript" /* hand = dial;*/ } first narrow then huge; action huge { - rep language "JavaScript" /* count = 9007199254740993; */ + rep language "JavaScript" /* count = 9007199254740993;*/ } first huge then smile; action smile { - rep language "JavaScript" /* label = "\uD83D\uDE00"; count = 9007199254740991; */ + rep language "JavaScript" /* label = "\uD83D\uDE00"; count = 9007199254740991;*/ assign label := "😀"; assign count := 9007199254740991; } first smile then same; action same { - rep language "Java" /* lit = label.equals("\uD83D\uDE00"); */ + rep language "Java" /* lit = label.equals("\uD83D\uDE00");*/ assign lit := label == "😀"; } first same then 'alias'; action 'alias' { - rep language "Java" /* lit = label == "\uD83D\uDE00"; */ + rep language "Java" /* lit = label == "\uD83D\uDE00";*/ } first 'alias' then final; action final terminate; @@ -193,12 +192,12 @@ part def Meter { in limit : ScalarValues::Integer[1] default = count + 1; first start then fuse; action fuse { - rep language "JavaScript" /* flag = ready and enabled */ + rep language "JavaScript" /* flag = ready and enabled*/ } /* guard not migrated: [{JavaScript} ready and enabled] — the text "and" is not expression syntax: text follows the expression */ first fuse then latch; action latch { - rep language "JavaScript" /* flag = ready && enabled */ + rep language "JavaScript" /* flag = ready && enabled*/ assign flag := ready and enabled; } first latch then final; @@ -208,38 +207,38 @@ part def Meter { action sweep { first start then spread; action spread { - rep language "JavaScript" /* total = cells.reading + 1; */ + rep language "JavaScript" /* total = cells.reading + 1;*/ } first spread then reset; action reset { - rep language "JavaScript" /* cells.reading = 1; */ + rep language "JavaScript" /* cells.reading = 1;*/ } first reset then peak; action peak { - rep language "JavaScript" /* total = java.util.Collections.max(cells.reading); */ + rep language "JavaScript" /* total = java.util.Collections.max(cells.reading);*/ assign total := cells.reading->ControlFunctions::reduce { in x; in y; RealFunctions::max(x, y) }; } first peak then calib; action calib { - rep language "JavaScript" /* reading = 2; */ + rep language "JavaScript" /* reading = 2;*/ } first calib then span; action span { - rep language "JavaScript" /* total = reading; */ + rep language "JavaScript" /* total = reading;*/ } first span then ticking; action ticking : Gauge::Tick; first ticking then tally; action tally { - rep language "JavaScript" /* total = banks.level + 1; */ + rep language "JavaScript" /* total = banks.level + 1;*/ } first tally then charge; action charge { - rep language "JavaScript" /* level = 1; */ + rep language "JavaScript" /* level = 1;*/ } first charge then settle; action settle { - rep language "JavaScript" /* level = 0; */ + rep language "JavaScript" /* level = 0;*/ } first settle then final; action final terminate; diff --git a/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml b/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml index 77ebd94976..467f06c4af 100644 --- a/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml +++ b/tests/migrate/testdata/xmi/montecarlo_case.golden.sysml @@ -7,7 +7,7 @@ part def Sensor { perform action settle { first start then 'start timer'; action 'start timer' { - rep language "JavaScript" /* settleTime = simtime; */ + rep language "JavaScript" /* settleTime = simtime;*/ assign settleTime := localClock.currentTime; } first 'start timer' then wait; @@ -16,7 +16,7 @@ part def Sensor { action settle; first settle then 'stop timer'; action 'stop timer' { - rep language "JavaScript" /* settleTime = simtime - settleTime; */ + rep language "JavaScript" /* settleTime = simtime - settleTime;*/ assign settleTime := localClock.currentTime - settleTime; } first 'stop timer' then final; diff --git a/tests/migrate/testdata/xmi/nested_context.golden.sysml b/tests/migrate/testdata/xmi/nested_context.golden.sysml index 8003e04913..d06fad3b1d 100644 --- a/tests/migrate/testdata/xmi/nested_context.golden.sysml +++ b/tests/migrate/testdata/xmi/nested_context.golden.sysml @@ -52,7 +52,7 @@ part def Host { action direct { first start then 'write state data'; action 'write state data' { - rep language "JavaScript Rhino" /* k = 2 */ + rep language "JavaScript Rhino" /* k = 2*/ assign 'aO Sequencer'.k := 2; } first 'write state data' then 'decide'; diff --git a/tests/migrate/testdata/xmi/object_flows.golden.sysml b/tests/migrate/testdata/xmi/object_flows.golden.sysml index 3abca35311..3e6acf4437 100644 --- a/tests/migrate/testdata/xmi/object_flows.golden.sysml +++ b/tests/migrate/testdata/xmi/object_flows.golden.sysml @@ -7,12 +7,12 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 2.5; */ + rep language "JavaScript" /* y = 2.5;*/ assign y := 2.5; } action consume { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* a = v; */ + rep language "JavaScript" /* a = v;*/ assign a := v; } first consume then final; @@ -24,12 +24,12 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 4.0; */ + rep language "JavaScript" /* y = 4.0;*/ assign y := 4.0; } action consume { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* a = v; */ + rep language "JavaScript" /* a = v;*/ assign a := v; } first consume then final; @@ -42,7 +42,7 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 3.0; */ + rep language "JavaScript" /* y = 3.0;*/ assign y := 3.0; } fork copy { @@ -52,13 +52,13 @@ part def Pipeline { } action left { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* a = v; */ + rep language "JavaScript" /* a = v;*/ assign a := v; } first left then both; action right { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* b = v * 2; */ + rep language "JavaScript" /* b = v * 2;*/ assign b := v * 2; } first right then both; @@ -74,17 +74,17 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 5.0; */ + rep language "JavaScript" /* y = 5.0;*/ assign y := 5.0; } action left { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* a = v; */ + rep language "JavaScript" /* a = v;*/ assign a := v; } action right { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* b = v + 1; */ + rep language "JavaScript" /* b = v + 1;*/ assign b := v + 1; } succession flow of ScalarValues::Real from produce.y to left.v; @@ -97,11 +97,11 @@ part def Pipeline { first 'start both' then warm; action produce { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 7.0; */ + rep language "JavaScript" /* y = 7.0;*/ assign y := 7.0; } action warm { - rep language "JavaScript" /* n = n + 1; */ + rep language "JavaScript" /* n = n + 1;*/ assign n := n + 1; } first warm then ready; @@ -111,7 +111,7 @@ part def Pipeline { } action consume { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* a = v; */ + rep language "JavaScript" /* a = v;*/ assign a := v; } first consume then final; @@ -127,12 +127,12 @@ part def Pipeline { first which if not on then low; action high { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 9.0; */ + rep language "JavaScript" /* y = 9.0;*/ assign y := 9.0; } action low { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 1.0; */ + rep language "JavaScript" /* y = 1.0;*/ assign y := 1.0; } merge pick { @@ -142,7 +142,7 @@ part def Pipeline { } action consume { in v : ScalarValues::Real[1]; - rep language "JavaScript" /* a = v; */ + rep language "JavaScript" /* a = v;*/ assign a := v; } first consume then final; @@ -156,7 +156,7 @@ part def Pipeline { first start then zero; action zero { out y : ScalarValues::Integer[1]; - rep language "JavaScript" /* y = 0; */ + rep language "JavaScript" /* y = 0;*/ assign y := 0; } merge again { @@ -168,8 +168,7 @@ part def Pipeline { in x : ScalarValues::Integer[1]; out y : ScalarValues::Integer[1]; rep language "JavaScript" /* y = x + 1; - * n = y; - */ + * n = y;*/ assign y := x + 1; assign n := y; } @@ -194,12 +193,12 @@ part def Pipeline { first both then high; action low { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 1.5; */ + rep language "JavaScript" /* y = 1.5;*/ assign y := 1.5; } action high { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 8.5; */ + rep language "JavaScript" /* y = 8.5;*/ assign y := 8.5; } join gather { @@ -222,7 +221,7 @@ part def Pipeline { first start then produce; action produce { out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = 6.0; */ + rep language "JavaScript" /* y = 6.0;*/ assign y := 6.0; } first produce then feed; diff --git a/tests/migrate/testdata/xmi/plant.golden.sysml b/tests/migrate/testdata/xmi/plant.golden.sysml index 4f08f0f529..5dd7fc7cd2 100644 --- a/tests/migrate/testdata/xmi/plant.golden.sysml +++ b/tests/migrate/testdata/xmi/plant.golden.sysml @@ -33,61 +33,61 @@ part def Plant { action fill { first start then 'open valve'; action 'open valve' { - rep language "JavaScript" /* open = true; */ + rep language "JavaScript" /* open = true;*/ assign tank.valve.open := true; } first 'open valve' then 'fill tank'; action 'fill tank' { - rep language "JavaScript" /* volume = volume * 2 + (valve.open ? 1 : 0); */ + rep language "JavaScript" /* volume = volume * 2 + (valve.open ? 1 : 0);*/ assign tank.volume := tank.volume * 2 + (if tank.valve.open ? 1 else 0); } first 'fill tank' then torn; action torn { - rep language "JavaScript" /* volume = 0; */ + rep language "JavaScript" /* volume = 0;*/ } /* guard not migrated: [{JavaScript} on] — the name "on" resolves to nothing readable: nothing visible from Plant::Fill:: is called on; its source 'torn' is in the partitions 'Tank' (this.tank) and 'Pump' (this.pump), which represent different objects, so names resolve through no partition */ first torn then prime; action prime { - rep language "JavaScript" /* on = true; */ + rep language "JavaScript" /* on = true;*/ assign pump.on := true; } first prime then twice; action twice { - rep language "JavaScript" /* volume = volume + 1; */ + rep language "JavaScript" /* volume = volume + 1;*/ assign tank.volume := tank.volume + 1; } first twice then mine; action mine { - rep language "JavaScript" /* this.level = 1; */ + rep language "JavaScript" /* this.level = 1;*/ } first mine then 'count run'; action 'count run' { - rep language "JavaScript" /* runs += 1; */ + rep language "JavaScript" /* runs += 1;*/ assign runs := runs + 1; } first 'count run' then 'note level'; action 'note level' { - rep language "JavaScript" /* level = tank.volume; */ + rep language "JavaScript" /* level = tank.volume;*/ assign level := tank.volume; } first 'note level' then lost; action lost { - rep language "JavaScript" /* level = level + 1; */ + rep language "JavaScript" /* level = level + 1;*/ assign level := level + 1; } first lost then free; action free { - rep language "JavaScript" /* level = level + tank.volume; */ + rep language "JavaScript" /* level = level + tank.volume;*/ assign level := level + tank.volume; } first free then arm; action arm { - rep language "JavaScript" /* this.status = true; */ + rep language "JavaScript" /* this.status = true;*/ assign site.control.rack.controller.status := true; } first arm then light; action light { - rep language "JavaScript" /* lit = true; */ + rep language "JavaScript" /* lit = true;*/ assign site.control.rack.controller.led.lit := true; } first light then final; diff --git a/tests/migrate/testdata/xmi/plant_states.golden.sysml b/tests/migrate/testdata/xmi/plant_states.golden.sysml index bed64ac13d..0c2f15dd09 100644 --- a/tests/migrate/testdata/xmi/plant_states.golden.sysml +++ b/tests/migrate/testdata/xmi/plant_states.golden.sysml @@ -19,7 +19,7 @@ part def Plant { state Idle; state Work parallel { exit action tally { - rep language "JavaScript" /* count = count + 1; */ + rep language "JavaScript" /* count = count + 1;*/ assign context.count := context.count + 1; } state steps { @@ -62,7 +62,7 @@ part def Plant { transition first Work accept Hold then Pause; transition first Pause accept Resume do action log { - rep language "JavaScript" /* println("resuming at " + count); */ + rep language "JavaScript" /* println("resuming at " + count);*/ } then Work.steps.last; transition first Idle accept Enter then Cell.warmStart; diff --git a/tests/migrate/testdata/xmi/reactor.golden.sysml b/tests/migrate/testdata/xmi/reactor.golden.sysml index 124d3902f2..1c7447690f 100644 --- a/tests/migrate/testdata/xmi/reactor.golden.sysml +++ b/tests/migrate/testdata/xmi/reactor.golden.sysml @@ -8,22 +8,22 @@ calc def Energy { in P : ScalarValues::Real[1]; in dt : ScalarValues::Real[1]; out result : ScalarValues::Real[1]; - rep language "JavaScript" /* Math.max(E_0 - P * dt, 0) */ + rep language "JavaScript" /* Math.max(E_0 - P * dt, 0)*/ RealFunctions::max(E_0 - P * dt, 0) } action def Scale { in x : ScalarValues::Real[1]; out y : ScalarValues::Real[1]; - rep language "JavaScript" /* x = x * 2; y = x; */ + rep language "JavaScript" /* x = x * 2; y = x;*/ } action def Shift { in a : ScalarValues::Real[1]; out b : ScalarValues::Real[1]; - rep language "JavaScript" /* const step = 1; a = a + step; b = a; */ + rep language "JavaScript" /* const step = 1; a = a + step; b = a;*/ } action def Double { inout n : ScalarValues::Real[1]; - rep language "JavaScript" /* const k = 2; n = n * k; */ + rep language "JavaScript" /* const k = 2; n = n * k;*/ first start then body; action body { attribute k : ScalarValues::Integer; @@ -35,7 +35,7 @@ action def Double { action def Delay { in t_sim : ScalarValues::Real[1]; out d : ScalarValues::Real[1]; - rep language "JavaScript" /* d = t_sim * 2; */ + rep language "JavaScript" /* d = t_sim * 2;*/ first start then body; action body { assign d := t_sim * 2; @@ -45,31 +45,31 @@ action def Delay { calc def Floor { in x : ScalarValues::Real[1]; out whole : ScalarValues::Integer[1]; - rep language "JavaScript" /* Math.floor(x) */ + rep language "JavaScript" /* Math.floor(x)*/ RealFunctions::floor(x) } calc def Ceil { in x : ScalarValues::Real[1]; out whole : ScalarValues::Integer[1]; - rep language "JavaScript" /* Math.ceil(x) */ + rep language "JavaScript" /* Math.ceil(x)*/ OpenSysMLMathFunctions::ceiling(x) } calc def Round { in x : ScalarValues::Real[1]; out whole : ScalarValues::Real[1]; - rep language "JavaScript" /* Math.round(x) */ + rep language "JavaScript" /* Math.round(x)*/ RealFunctions::floor(x + 0.5) } action def Root { in x : ScalarValues::Real[1]; out ok : ScalarValues::Boolean[1]; - rep language "JavaScript" /* Math.sqrt(x) */ + rep language "JavaScript" /* Math.sqrt(x)*/ } action def Split { in x : ScalarValues::Real[1]; out lo : ScalarValues::Real[1]; out hi : ScalarValues::Real[1]; - rep language "JavaScript" /* x / 2 */ + rep language "JavaScript" /* x / 2*/ } part def Gauge { attribute t_sim : ScalarValues::Real default = 4.0; @@ -83,68 +83,68 @@ part def Reactor { action cycle { first start then mark; action mark { - rep language "JavaScript" /* started = t_sim; */ + rep language "JavaScript" /* started = t_sim;*/ assign started := localClock.currentTime; } first mark then 'loop'; action 'loop' { - rep language "JavaScript" /* for (var k = 0; k < 3; k++) { power = power + 1; } */ + rep language "JavaScript" /* for (var k = 0; k < 3; k++) { power = power + 1; }*/ } first 'loop' then alloc; action alloc { - rep language "JavaScript" /* power = new Number(4); */ + rep language "JavaScript" /* power = new Number(4);*/ } first alloc then trim; action trim { - rep language "JavaScript" /* label = label.trim(); */ + rep language "JavaScript" /* label = label.trim();*/ } first trim then self2; action self2 { - rep language "JavaScript" /* power = this.owner.power; */ + rep language "JavaScript" /* power = this.owner.power;*/ } first self2 then stray; action stray { - rep language "JavaScript" /* power = nowhere + 1; */ + rep language "JavaScript" /* power = nowhere + 1;*/ } first stray then mixed; action mixed { - rep language "JavaScript" /* power = label + 1; */ + rep language "JavaScript" /* power = label + 1;*/ } first mixed then python; action python { - rep language "Python" /* power = power + 1 */ + rep language "Python" /* power = power + 1*/ assign power := power + 1; } first python then partial; action partial { - rep language "JavaScript" /* power = power + 1; label = label.trim(); */ + rep language "JavaScript" /* power = power + 1; label = label.trim();*/ } first partial then shadow; action shadow { - rep language "JavaScript" /* var power = 1; power += 1; */ + rep language "JavaScript" /* var power = 1; power += 1;*/ } first shadow then pinned; action pinned { in x : ScalarValues::Real[1] = 1.0; - rep language "JavaScript" /* var x = 1; x += 1; */ + rep language "JavaScript" /* var x = 1; x += 1;*/ } first pinned then relay; action relay { in x : ScalarValues::Real[1] = 1.0; out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = x * 2; x = y; */ + rep language "JavaScript" /* y = x * 2; x = y;*/ } first relay then gain; action gain { in x : ScalarValues::Real[1] = 1.5; out y : ScalarValues::Real[1]; - rep language "JavaScript" /* y = x * power; */ + rep language "JavaScript" /* y = x * power;*/ assign y := x * power; } first gain then bare; action bare { in raw[1] = 1.0; - rep language "JavaScript" /* power = raw.level; */ + rep language "JavaScript" /* power = raw.level;*/ } first bare then final; action final terminate; diff --git a/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml b/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml index 34e8041f8c..357c9e2c7c 100644 --- a/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml +++ b/tests/migrate/testdata/xmi/script_guarded_reads.golden.sysml @@ -6,15 +6,14 @@ part def Executive { action idle { out z : ScalarValues::Real[1]; out y : ScalarValues::Real[1]; - rep language "JavaScript" /* tdiff = tdiff * 1; */ + rep language "JavaScript" /* tdiff = tdiff * 1;*/ assign tdiff := tdiff * 1; } first idle then clamp; action clamp { in w : ScalarValues::Real[0..1]; rep language "JavaScript" /* tMax = w; - * tdiff = tMax * 2; - */ + * tdiff = tMax * 2;*/ if w->SequenceFunctions::notEmpty() { assign tMax := w; } assign tdiff := tMax * 2; } @@ -23,8 +22,7 @@ part def Executive { in w : ScalarValues::Real[0..1]; rep language "JavaScript" /* tMax = 1; * tMax = w; - * tdiff = tMax * 2; - */ + * tdiff = tMax * 2;*/ assign tMax := 1; if w->SequenceFunctions::notEmpty() { assign tMax := w; } assign tdiff := tMax * 2; @@ -35,8 +33,7 @@ part def Executive { in v : ScalarValues::Real[0..1]; rep language "JavaScript" /* tMax = w; * tMax = v; - * tdiff = tMax * 2; - */ + * tdiff = tMax * 2;*/ if w->SequenceFunctions::notEmpty() { assign tMax := w; } if v->SequenceFunctions::notEmpty() { assign tMax := v; } assign tdiff := tMax * 2; @@ -47,8 +44,7 @@ part def Executive { in v : ScalarValues::Real[0..1]; rep language "JavaScript" /* tMax = w; * tdiff = tMax * 2; - * tMax = v; - */ + * tMax = v;*/ if w->SequenceFunctions::notEmpty() { assign tMax := w; } assign tdiff := tMax * 2; if v->SequenceFunctions::notEmpty() { assign tMax := v; } diff --git a/tests/migrate/testdata/xmi/script_locals.golden.sysml b/tests/migrate/testdata/xmi/script_locals.golden.sysml index 6f2d84e230..703f8beb61 100644 --- a/tests/migrate/testdata/xmi/script_locals.golden.sysml +++ b/tests/migrate/testdata/xmi/script_locals.golden.sysml @@ -12,7 +12,7 @@ part def Executive { state def 'Executive Behavior' { in ref context : Executive[1]; entry action stamp { - rep language "JavaScript" /* t0 = simtime; */ + rep language "JavaScript" /* t0 = simtime;*/ assign context.t0 := localClock.currentTime; } then Aligning; @@ -26,8 +26,7 @@ part def Executive { * tdiffOld = tdiff; * tdiff = (t1 - t0); * tStep = tdiff - tdiffOld; - * print("step " + tStep); - */ + * print("step " + tStep);*/ attribute t1 : ScalarValues::Real; assign t1 := localClock.currentTime; attribute tdiffOld : ScalarValues::Real; @@ -39,15 +38,13 @@ part def Executive { transition first Phasing accept after 2.0 [SI::s] do action javaDiff { rep language "Java" /* tdiffOld = tdiff; - * tdiff = tdiff + 1.0; - */ + * tdiff = tdiff + 1.0;*/ } then Counting; transition first Counting do action readFirst { rep language "JavaScript" /* steps = steps + stepsOld; - * stepsOld = steps; - */ + * stepsOld = steps;*/ } then Idle; } diff --git a/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml b/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml index 90bf7fecb8..830cd858b0 100644 --- a/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml +++ b/tests/migrate/testdata/xmi/script_unset_reads.golden.sysml @@ -12,7 +12,7 @@ part def Executive { state def 'Executive Behavior' { in ref context : Executive[1]; entry action stamp { - rep language "JavaScript" /* t0 = simtime; */ + rep language "JavaScript" /* t0 = simtime;*/ assign context.t0 := localClock.currentTime; } then Aligning; @@ -25,8 +25,7 @@ part def Executive { rep language "JavaScript" /* t1 = simtime; * tdiffOld = tdiff; * tdiff = t1 - t0; - * tStep = tdiff - tdiffOld; - */ + * tStep = tdiff - tdiffOld;*/ attribute t1 : ScalarValues::Real; assign t1 := localClock.currentTime; attribute tdiffOld : ScalarValues::Real; @@ -38,8 +37,7 @@ part def Executive { transition first Phasing accept after 2.0 [SI::s] do action reset { rep language "JavaScript" /* tdiff = 0; - * tStep = tdiff + steps; - */ + * tStep = tdiff + steps;*/ assign context.tdiff := 0; assign context.tStep := context.tdiff + context.steps; } @@ -47,8 +45,7 @@ part def Executive { transition first Counting do action count { rep language "JavaScript" /* steps++; - * tdiff += steps; - */ + * tdiff += steps;*/ assign context.steps := context.steps + 1; assign context.tdiff := context.tdiff + context.steps; } diff --git a/tests/migrate/testdata/xmi/station_points.golden.sysml b/tests/migrate/testdata/xmi/station_points.golden.sysml index 6315150fe3..aa5d346cd5 100644 --- a/tests/migrate/testdata/xmi/station_points.golden.sysml +++ b/tests/migrate/testdata/xmi/station_points.golden.sysml @@ -18,11 +18,11 @@ occurrence def Station { state Idle; state Work parallel { entry action { - rep language "JavaScript" /* trace = trace * 100 + 11; */ + rep language "JavaScript" /* trace = trace * 100 + 11;*/ assign context.trace := context.trace * 100 + 11; } exit action { - rep language "JavaScript" /* trace = trace * 100 + 12; */ + rep language "JavaScript" /* trace = trace * 100 + 12;*/ assign context.trace := context.trace * 100 + 12; } state steps { @@ -32,17 +32,17 @@ occurrence def Station { entry; then Prep; state Prep { entry action { - rep language "JavaScript" /* trace = trace * 100 + 13; */ + rep language "JavaScript" /* trace = trace * 100 + 13;*/ assign context.trace := context.trace * 100 + 13; } } state Run parallel { entry action { - rep language "JavaScript" /* trace = trace * 100 + 14; */ + rep language "JavaScript" /* trace = trace * 100 + 14;*/ assign context.trace := context.trace * 100 + 14; } exit action { - rep language "JavaScript" /* trace = trace * 100 + 15; */ + rep language "JavaScript" /* trace = trace * 100 + 15;*/ assign context.trace := context.trace * 100 + 15; } state pace { @@ -52,19 +52,19 @@ occurrence def Station { state Slow; state Fast { entry action { - rep language "JavaScript" /* trace = trace * 100 + 17; */ + rep language "JavaScript" /* trace = trace * 100 + 17;*/ assign context.trace := context.trace * 100 + 17; } } transition first Deep do action { - rep language "JavaScript" /* trace = trace * 100 + 21; */ + rep language "JavaScript" /* trace = trace * 100 + 21;*/ assign context.trace := context.trace * 100 + 21; } then Fast; transition first Fast accept Back do action { - rep language "JavaScript" /* trace = trace * 100 + 22; */ + rep language "JavaScript" /* trace = trace * 100 + 22;*/ assign context.trace := context.trace * 100 + 22; } then Out; @@ -74,20 +74,20 @@ occurrence def Station { transition first Prep accept Next then Run; transition first Start do action { - rep language "JavaScript" /* trace = trace * 100 + 23; */ + rep language "JavaScript" /* trace = trace * 100 + 23;*/ assign context.trace := context.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 { - rep language "JavaScript" /* trace = trace * 100 + 24; */ + rep language "JavaScript" /* trace = trace * 100 + 24;*/ assign context.trace := context.trace * 100 + 24; } then Leave; transition first steps.Run.pace.Out do action { - rep language "JavaScript" /* trace = trace * 100 + 25; */ + rep language "JavaScript" /* trace = trace * 100 + 25;*/ assign context.trace := context.trace * 100 + 25; } then Prep; @@ -95,12 +95,12 @@ occurrence def Station { } state Sync { entry action { - rep language "JavaScript" /* trace = trace * 100 + 31; */ + rep language "JavaScript" /* trace = trace * 100 + 31;*/ assign context.trace := context.trace * 100 + 31; } then regions; exit action { - rep language "JavaScript" /* trace = trace * 100 + 32; */ + rep language "JavaScript" /* trace = trace * 100 + 32;*/ assign context.trace := context.trace * 100 + 32; } fork Both; @@ -111,11 +111,11 @@ occurrence def Station { state A0; state A1 { entry action { - rep language "JavaScript" /* trace = trace * 100 + 33; */ + rep language "JavaScript" /* trace = trace * 100 + 33;*/ assign context.trace := context.trace * 100 + 33; } exit action { - rep language "JavaScript" /* trace = trace * 100 + 34; */ + rep language "JavaScript" /* trace = trace * 100 + 34;*/ assign context.trace := context.trace * 100 + 34; } } @@ -126,11 +126,11 @@ occurrence def Station { state B0; state B1 { entry action { - rep language "JavaScript" /* trace = trace * 100 + 35; */ + rep language "JavaScript" /* trace = trace * 100 + 35;*/ assign context.trace := context.trace * 100 + 35; } exit action { - rep language "JavaScript" /* trace = trace * 100 + 36; */ + rep language "JavaScript" /* trace = trace * 100 + 36;*/ assign context.trace := context.trace * 100 + 36; } } @@ -141,14 +141,14 @@ occurrence def Station { transition first Both then regions.a.A1; transition first regions.a.A1 do action { - rep language "JavaScript" /* trace = trace * 100 + 37; */ + rep language "JavaScript" /* trace = trace * 100 + 37;*/ assign context.trace := context.trace * 100 + 37; } then Gather; transition first Both then regions.b.B1; transition first regions.b.B1 do action { - rep language "JavaScript" /* trace = trace * 100 + 38; */ + rep language "JavaScript" /* trace = trace * 100 + 38;*/ assign context.trace := context.trace * 100 + 38; } then Gather; @@ -156,7 +156,7 @@ occurrence def Station { } transition first Idle accept Go do action { - rep language "JavaScript" /* trace = trace * 100 + 41; */ + rep language "JavaScript" /* trace = trace * 100 + 41;*/ assign context.trace := context.trace * 100 + 41; } then Work.steps.Start; @@ -164,7 +164,7 @@ occurrence def Station { transition first Idle accept Enter then Work; transition first Idle accept Dive do action { - rep language "JavaScript" /* trace = trace * 100 + 42; */ + rep language "JavaScript" /* trace = trace * 100 + 42;*/ assign context.trace := context.trace * 100 + 42; } then Work.steps.Run.pace.Deep; @@ -172,13 +172,13 @@ occurrence def Station { /* 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.steps.Leave do action { - rep language "JavaScript" /* trace = trace * 100 + 43; */ + rep language "JavaScript" /* trace = trace * 100 + 43;*/ assign context.trace := context.trace * 100 + 43; } then Idle; transition first Work accept Stop do action { - rep language "JavaScript" /* trace = trace * 100 + 44; */ + rep language "JavaScript" /* trace = trace * 100 + 44;*/ assign context.trace := context.trace * 100 + 44; } then Idle; @@ -186,7 +186,7 @@ occurrence def Station { transition first Idle accept Split then Sync.Both; transition first Sync.Gather do action { - rep language "JavaScript" /* trace = trace * 100 + 46; */ + rep language "JavaScript" /* trace = trace * 100 + 46;*/ assign context.trace := context.trace * 100 + 46; } then Idle; diff --git a/tests/migrate/testdata/xmi/submachine_params.golden.sysml b/tests/migrate/testdata/xmi/submachine_params.golden.sysml index 0d7a18d012..cc78da86f2 100644 --- a/tests/migrate/testdata/xmi/submachine_params.golden.sysml +++ b/tests/migrate/testdata/xmi/submachine_params.golden.sysml @@ -9,7 +9,7 @@ part def Plant { in x[1]; in ref :>> context = Line::context; entry action tally { - rep language "JavaScript" /* count = count + 1; */ + rep language "JavaScript" /* count = count + 1;*/ assign context.count := context.count + 1; } } From 86bd9a1101b05b34de3db14ca6ca0534f9f8ea9d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:09:09 +0000 Subject: [PATCH 15/19] fix(runtime): wait for a region stand-in's do action before completing its owner Co-Authored-By: jason.han --- .../census-states-events.changed.md | 2 +- docs/project/spec-compliance.md | 2 +- .../robustness_inactive_region_test.go | 61 +++++++++++++++---- internal/exec/runtime/state_executor.go | 22 ++++++- ..._do_action_defers_completion.expected.json | 8 +++ ...stand_in_do_action_defers_completion.sysml | 25 ++++++++ ...n_do_action_defers_completion.trace.golden | 26 ++++++++ 7 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.sysml create mode 100644 internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.trace.golden diff --git a/changes/unreleased/census-states-events.changed.md b/changes/unreleased/census-states-events.changed.md index 71ffb0e5be..d287a6f1b6 100644 --- a/changes/unreleased/census-states-events.changed.md +++ b/changes/unreleased/census-states-events.changed.md @@ -1,3 +1,3 @@ - **Preserve UML opaque body text during migration.** Each raw body and matching language now appear as a non-executed textual representation with whitespace preserved, while supported body translations continue to use the trimmed first body and remain executable. -- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. A first entry through history uses the same default, while an owner with no default entry remains an error. Terminate pseudostates now terminate the whole state machine. +- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a stand-in's running do action defers that completion until it ends. A region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. A first entry through history uses the same default, while an owner with no default entry remains an error. Terminate pseudostates now terminate the whole state machine. - **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 18342e7fa7..0fb769dbdd 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -901,7 +901,7 @@ nothing above. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| -| On default entry, a region whose stand-in has no active child is complete; a region with no stand-in and no start is also complete while inactive. A region activated later by a transition or fork branch must reach its final state. A composite whose regions are all complete by default completes immediately (PSSM *Entering 004*); a running do-action still defers completion | `state_region_entry.go` `regionStart`, `enterRegion`; `state_executor.go` `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`, `scheduleCompletionTransitions` | `state_parallel_region_without_entry_completes.sysml` (+ trace golden; PSSM *Entering 004*), `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens); `lower/fork_plan_test.go` | ✅ Faithful | +| On default entry, a region whose stand-in has no active child is complete; a region with no stand-in and no start is also complete while inactive. A region activated later by a transition or fork branch must reach its final state. A composite whose regions are all complete by default completes immediately (PSSM *Entering 004*). A stand-in's running do-action keeps its default-complete region incomplete until it ends; the owner is then scheduled if every region is complete | `state_region_entry.go` `regionStart`, `enterRegion`; `state_executor.go` `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`, `scheduleCompletionTransitions`, `settleDoActions` | `state_parallel_region_without_entry_completes.sysml` (+ trace golden; PSSM *Entering 004*), `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens); `lower/fork_plan_test.go` | ✅ Faithful | | A named succession or transition endpoint that resolves to a non-vertex or non-action node is reported during analysis; positional, implied, unresolved and flow ends remain unchanged | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go`; `passes/action_endpoint.go` `ActionEndpointPass`; `lower/action_nodes.go` `ActionNodes` and `ActionEndpointAccepted`, including lazy inherited action-node collection | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestActionEndpointPassReportsResolvedNonNodes`, `:TestActionEndpointPassAcceptsLoweredNodes`, `:TestActionEndpointPassDiagnosticAgreesWithLowering`, `:TestActionEndpointPassInheritedNodes`, `:TestActionEndpointPassIgnoresFeatureChainsThroughNonNodes`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`; `lower/action_inherited_test.go`; `runtime/testdata/conformance/action_inherited_endpoint.sysml` | ⚠️ Approximate (a feature chain rooted in a part usage is legal notation and stays silent during validation, but execution still reports the existing lowering error because invoking an action through that chain is not implemented) | | Initial state identification | `lower/state_graph.go:ToStateGraph`; `state_executor.go:686` initialize | `state_simple.sysml` | ✅ Faithful | | A succession out of a state body's own entry subaction names the state it starts in (`entry; then off;`), which is how a machine designates where it starts | `lower/entry_transition.go` `addEntryTransition`, `UnconditionalStart`; `lower/state_graph.go` collectTransitions SuccessionEdge case + `isEntrySubaction` | `lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState`, `lower/transition_source_test.go:TestToStateGraph_UnguardedEntryTransitionIsInitial`, `state_entry_succession_initial.sysml` conformance, `tests/parser/testdata/parse/behavior_exhibit_state_body.golden` | ✅ Faithful | diff --git a/internal/exec/runtime/robustness_inactive_region_test.go b/internal/exec/runtime/robustness_inactive_region_test.go index c572262a87..d88e924875 100644 --- a/internal/exec/runtime/robustness_inactive_region_test.go +++ b/internal/exec/runtime/robustness_inactive_region_test.go @@ -3,7 +3,8 @@ package runtime import "testing" func TestRuntimeRobustnessInactiveRegion(t *testing.T) { - _, visits, err := executeStateSource(t, "Machine", `package test { + t.Run("empty regions complete without activating inner states", func(t *testing.T) { + _, visits, err := executeStateSource(t, "Machine", `package test { state Machine { entry; then working; state working parallel { @@ -14,16 +15,52 @@ func TestRuntimeRobustnessInactiveRegion(t *testing.T) { transition first working then finished; } }`) - if err != nil { - t.Fatalf("execute state machine: %v", err) - } - for _, visit := range visits { - switch visit { - case "idle", "waiting": - t.Errorf("no-entry region state %s was activated", visit) - case "finished": - return + if err != nil { + t.Fatalf("execute state machine: %v", err) } - } - t.Fatal("working did not complete after both regions were left inactive") + finished := false + for _, visit := range visits { + switch visit { + case "idle", "waiting": + t.Errorf("no-entry region state %s was activated", visit) + case "finished": + finished = true + } + } + if !finished { + t.Fatal("working did not complete after both regions were left inactive") + } + }) + + t.Run("stand-in do action waits for a signal before owner completion", func(t *testing.T) { + _, visits, err := executeStateSource(t, "Machine", `package test { + state Machine { + entry; then working; + state working parallel { + state standIn { + do action wait { + first start; + then action receive accept Never; + then done; + } + state inner; + } + } + state finished; + transition first working then finished; + } + attribute def Never; + }`) + if err != nil { + t.Fatalf("execute state machine: %v", err) + } + for _, visit := range visits { + switch visit { + case "inner": + t.Error("unstarted inner state was activated") + case "finished": + t.Error("owner completed while its stand-in do action awaited a signal") + } + } + }) } diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index 6051ae4f40..cd1f3440e8 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -2639,7 +2639,8 @@ func (e *StateExecutor) completeInto(trans *lower.Transition, fromName string, t // regionComplete reports whether region reached its completion vertex, was left empty by default entry, or was left at its owner. func (e *StateExecutor) regionComplete(region *ast.StateRegion) bool { if e.activeConfig.defaultComplete[region] { - return true + standIn := e.graph.RegionState[region] + return standIn == nil || !e.hasRunningDoAction(standIn) } active, ok := e.activeConfig.regionStates[region] if !ok { @@ -2657,7 +2658,7 @@ func (e *StateExecutor) allRegionsDefaultComplete(state *ast.StateNode) bool { return false } for _, region := range regions { - if !e.activeConfig.defaultComplete[region] { + if !e.activeConfig.defaultComplete[region] || !e.regionComplete(region) { return false } } @@ -4475,7 +4476,7 @@ func doBehaviorDescription(state *ast.StateNode) string { } // settleDoActions drops the do behaviors that have finished and schedules the -// completion of their states; a do action whose state was exited is already gone. +// completion of their states and newly completed owners. func (e *StateExecutor) settleDoActions() error { finished := make([]*ast.StateNode, 0, len(e.doActions)) kept := e.doActions[:0] @@ -4494,6 +4495,7 @@ func (e *StateExecutor) settleDoActions() error { // A state completes once its do behavior has finished and its body, where it // runs one, has reached `done`; completeIfDone schedules the latter case, and a // body the move has yet to enter completes nothing. + scheduledOwners := make(map[*ast.StateNode]struct{}, len(finished)) for _, state := range finished { if e.bodyAhead(state) || (e.bodyRunning(state) && !e.stateComplete(state)) { continue @@ -4501,6 +4503,20 @@ func (e *StateExecutor) settleDoActions() error { if err := e.scheduleCompletionTransitions(state); err != nil { return fmt.Errorf("schedule completion of state %s: %w", state.Name, err) } + if region := e.RegionOf(state); region != nil && e.graph.RegionState[region] == state { + owner := e.graph.RegionOwner[region] + if owner != nil && e.stateComplete(owner) { + if _, ok := scheduledOwners[owner]; !ok { + scheduledOwners[owner] = struct{}{} + if err := e.scheduleCompletionTransitions(owner); err != nil { + return fmt.Errorf("schedule completion of state %s: %w", owner.Name, err) + } + } + } + } + if err := e.completeIfDone(state); err != nil { + return fmt.Errorf("complete after do behavior of state %s: %w", state.Name, err) + } } return nil } diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.expected.json b/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.expected.json new file mode 100644 index 0000000000..3bad1c5645 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.expected.json @@ -0,0 +1,8 @@ +{ + "type": "state", + "trace": true, + "finalState": "finished", + "outputs": { + "log": {"type": "String", "value": "do1;do2;S(exit);finished(entry);"} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.sysml b/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.sysml new file mode 100644 index 0000000000..d6f0293474 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.sysml @@ -0,0 +1,25 @@ +package Test { + state Machine { + attribute log : String = ""; + + entry; then S; + state S parallel { + exit { assign log := log + "S(exit);"; } + state R { + do action work { + first start; + then action firstStep { assign log := log + "do1;"; } + then action secondStep { assign log := log + "do2;"; } + then done; + } + state A { + entry { assign log := log + "A(entry);"; } + } + } + } + state finished { + entry { assign log := log + "finished(entry);"; } + } + transition first S then finished; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.trace.golden b/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.trace.golden new file mode 100644 index 0000000000..3c24f9bacd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_parallel_stand_in_do_action_defers_completion.trace.golden @@ -0,0 +1,26 @@ +do: R +stmt action body +enter action node: state behavior work + stmt assign log + eval feature log -> "" + eval literal "do1;" -> "do1;" + eval operator + -> "do1;" +do: R + stmt assign log + eval feature log -> "do1;" + eval literal "do2;" -> "do2;" + eval operator + -> "do1;do2;" +leave action node: state behavior work +exit: S (exit action) +stmt action body + stmt assign log + eval feature log -> "do1;do2;" + eval literal "S(exit);" -> "S(exit);" + eval operator + -> "do1;do2;S(exit);" +enter: finished (entry action) +stmt action body + stmt assign log + eval feature log -> "do1;do2;S(exit);" + eval literal "finished(entry);" -> "finished(entry);" + eval operator + -> "do1;do2;S(exit);finished(entry);" +transition: S -> finished From 21fc6727bb576e232623256d637615d2794c11a0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:24:29 +0000 Subject: [PATCH 16/19] test(migrate): align diagram output with raw opaque bodies Co-Authored-By: jason.han --- internal/translate/migrate/diagrams_internal_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/translate/migrate/diagrams_internal_test.go b/internal/translate/migrate/diagrams_internal_test.go index 1d7c21acf2..0230f1e1ff 100644 --- a/internal/translate/migrate/diagrams_internal_test.go +++ b/internal/translate/migrate/diagrams_internal_test.go @@ -151,7 +151,7 @@ func TestDiagramViews(t *testing.T) {
`, diagram("_d", "Shutting", "_shutting", "SysML Activity Diagram", "_pump"), - []string{"action def Shut {\n view Shutting {\n expose Sys::Pump;\n render Views::asTextualNotation;\n }\n rep language \"JavaScript\" /* 1; */"}, Mapped, + []string{"action def Shut {\n view Shutting {\n expose Sys::Pump;\n render Views::asTextualNotation;\n }\n rep language \"JavaScript\" /* 1;*/"}, Mapped, "its owner OpaqueBehavior Valve::Shutting is written as the body of action def Valve::Shut, whose method it is"}, {"a diagram owned by an action node is written in the body of the node's activity", ` From 1260c1c6d76a6c4443709b3d564461302c1ec089 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:35:52 +0000 Subject: [PATCH 17/19] fix(migrate): skip textual representations for whitespace-only opaque bodies Co-Authored-By: jason.han --- internal/translate/migrate/behavior.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/translate/migrate/behavior.go b/internal/translate/migrate/behavior.go index cb0807abf6..7849b9f571 100644 --- a/internal/translate/migrate/behavior.go +++ b/internal/translate/migrate/behavior.go @@ -1024,7 +1024,7 @@ func (m *migration) opaqueBehaviorBody(e, scope *sysmlv1.Element) { func (m *migration) textualReps(e *sysmlv1.Element) { for _, pair := range opaqueBodyPairs(e) { - if pair.body != "" && pair.language != "" { + if strings.TrimSpace(pair.body) != "" && pair.language != "" { m.w.lines(prefixFirst("rep language "+stringLiteral(pair.language)+" ", opaqueRepresentationCommentLines(pair.body))) } } From 99fbff8f6d826dbe3e19702944a42e6957d6ad1f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:33:00 +0000 Subject: [PATCH 18/19] fix(census): keep develop's use-case actor verdicts Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 2 + ...sml-v1-transformation-census-baseline.json | 42 ++++++++++++------- .../project/sysml-v1-transformation-census.md | 26 ++++++------ 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index da8fd7a8cd..70f759420d 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -912,6 +912,8 @@ nothing above. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| +| A nested default-entry junction belongs to a separate transition out of the composite's `entry` action and is read when that transition is taken, after the outer effect and composite entry; availability checks only the incoming transition's own route (SM32); if no branch is enabled, the run fails with a typed error naming the junction (SM46) | `state_executor.go` `startIn`, `resolveEntryRoute`, `continueEntryRoute` | `state_entry_transition_junction_no_way_fails.sysml`, `state_entry_transition_junction_reads_outer_effect.sysml` | ✅ Faithful | +| History is recorded per region, so a transition that starts inside its own composite's region restores that composite and its inner configuration rather than the region's initial state; a region left with no active state has nothing to restore; a region or body left at `done` leaves no history | `state_executor.go` `exitState`, `recordChildHistory`, `recordRegionHistory`, `forgetRegionHistory`; `state_region_transition.go` `leaveRegion`, `exitRegionTo` | `history_test.go:TestDeepHistoryRestoresARegionLeftFromInsideItsCompositeState`, `:TestHistoryRestoresOrthogonalRegions`, `state_deep_history_region_composite.sysml`, `state_history_revisit.sysml` | ✅ Faithful | | On default entry, a region whose stand-in has no active child is complete; a region with no stand-in and no start is also complete while inactive. A region activated later by a transition or fork branch must reach its final state. A composite whose regions are all complete by default completes immediately (PSSM *Entering 004*). A stand-in's running do-action keeps its default-complete region incomplete until it ends; the owner is then scheduled if every region is complete | `state_region_entry.go` `regionStart`, `enterRegion`; `state_executor.go` `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`, `scheduleCompletionTransitions`, `settleDoActions` | `state_parallel_region_without_entry_completes.sysml` (+ trace golden; PSSM *Entering 004*), `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens); `lower/fork_plan_test.go` | ✅ Faithful | | A named succession or transition endpoint that resolves to a non-vertex or non-action node is reported during analysis; positional, implied, unresolved and flow ends remain unchanged | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go`; `passes/action_endpoint.go` `ActionEndpointPass`; `lower/action_nodes.go` `ActionNodes` and `ActionEndpointAccepted`, including lazy inherited action-node collection | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestActionEndpointPassReportsResolvedNonNodes`, `:TestActionEndpointPassAcceptsLoweredNodes`, `:TestActionEndpointPassDiagnosticAgreesWithLowering`, `:TestActionEndpointPassInheritedNodes`, `:TestActionEndpointPassIgnoresFeatureChainsThroughNonNodes`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`; `lower/action_inherited_test.go`; `runtime/testdata/conformance/action_inherited_endpoint.sysml` | ⚠️ Approximate (a feature chain rooted in a part usage is legal notation and stays silent during validation, but execution still reports the existing lowering error because invoking an action through that chain is not implemented) | | Initial state identification | `lower/state_graph.go:ToStateGraph`; `state_executor.go:686` initialize | `state_simple.sysml` | ✅ Faithful | diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index dd8b391050..597ce8324b 100644 --- a/docs/project/sysml-v1-transformation-census-baseline.json +++ b/docs/project/sysml-v1-transformation-census-baseline.json @@ -98,6 +98,10 @@ "pssm": 0, "fixtures": 1 }, + "uml:Actor": { + "pssm": 0, + "fixtures": 8 + }, "uml:AddStructuralFeatureValueAction": { "pssm": 525, "fixtures": 18 @@ -21069,7 +21073,7 @@ ], "operations": [], "ocl": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "status": "faithful", + "status": "approximate", "implementation": [ "internal/translate/migrate/usecase.go:actorLink", "internal/translate/migrate/classify.go:classifyClass" @@ -21077,8 +21081,10 @@ "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "an actor is written as a part usage, not a part def: the actor lines of a use case diagram are connections, which join usages", + "scope": [ + "uml:Actor" + ] }, { "name": "IncludeFeatureTyping_Mapping", @@ -21145,16 +21151,18 @@ "type" ], "ocl": "sha256:719917ab2ec0a4e8566e0084ac378f0182dd8f3ed98522ab6c780ed6a5001349", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorLink", + "internal/translate/migrate/usecase.go:connectActor", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage", + "scope": [ + "uml:Actor" + ] }, { "name": "UseCaseActorMembership_Mapping", @@ -21171,16 +21179,18 @@ "ownedMemberParameter" ], "ocl": "sha256:350dd57e7124d89991e86930a62206324b4913cc22f5dfbc212c7fb53353cc02", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorLink", + "internal/translate/migrate/usecase.go:connectActor", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage", + "scope": [ + "uml:Actor" + ] }, { "name": "UseCaseActor_Mapping", @@ -21198,16 +21208,18 @@ "ownedRelationship" ], "ocl": "sha256:a3c87cbca08a5c2087397677178ad9e15063e3bad7ab80893904b89e3c79001c", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorLink", + "internal/translate/migrate/usecase.go:connectActor", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage", + "scope": [ + "uml:Actor" + ] }, { "name": "UseCaseEmptySubjectReferenceUsage_Mapping", diff --git a/docs/project/sysml-v1-transformation-census.md b/docs/project/sysml-v1-transformation-census.md index eb5ecaa7f8..69d1d030bf 100644 --- a/docs/project/sysml-v1-transformation-census.md +++ b/docs/project/sysml-v1-transformation-census.md @@ -34,7 +34,7 @@ that build one relationship of a target — share the verdict of the construct t -**Census:** 783 mapping classes — 533 ✅ faithful, 13 ⚠️ approximate, 42 ❌ not implemented, 195 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. +**Census:** 783 mapping classes — 529 ✅ faithful, 17 ⚠️ approximate, 42 ❌ not implemented, 195 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. | Package | Mappings | ✅ | ⚠️ | ❌ | ⛔ | 🚧 | ❔ | |---|---|---|---|---|---|---|---| @@ -65,9 +65,9 @@ that build one relationship of a target — share the verdict of the construct t | UML4SysML::SimpleClassifiers | 22 | 20 | 0 | 0 | 2 | 0 | 0 | | UML4SysML::StateMachines | 26 | 25 | 1 | 0 | 0 | 0 | 0 | | UML4SysML::StructuredClassifiers | 39 | 15 | 0 | 0 | 24 | 0 | 0 | -| UML4SysML::UseCases | 14 | 10 | 0 | 4 | 0 | 0 | 0 | +| UML4SysML::UseCases | 14 | 6 | 4 | 4 | 0 | 0 | 0 | | UML4SysML::Values | 33 | 14 | 0 | 3 | 16 | 0 | 0 | -| **Total** | 783 | 533 | 13 | 42 | 195 | 0 | 0 | +| **Total** | 783 | 529 | 17 | 42 | 195 | 0 | 0 | ## Census rows @@ -948,12 +948,12 @@ that build one relationship of a target — share the verdict of the construct t | OMG mapping | v1 source | v2 target | Implementation | Test | Status | Reason | |---|---|---|---|---|---|---| -| `Actor_Mapping` | `Actor` | `PartDefinition` | `internal/translate/migrate/usecase.go:actorLink`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | +| `Actor_Mapping` | `Actor` | `PartDefinition` | `internal/translate/migrate/usecase.go:actorLink`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an actor is written as a part usage, not a part def: the actor lines of a use case diagram are connections, which join usages | | `IncludeFeatureTyping_Mapping` | `Include` | `FeatureTyping` | `internal/translate/migrate/usecase.go:include` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `Include_Mapping` | `Include` | `IncludeUseCaseUsage` | `internal/translate/migrate/usecase.go:include` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `UseCaseActorFeatureTyping_Mapping` | `Property` | `FeatureTyping` | `internal/translate/migrate/usecase.go:actorLink`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `UseCaseActorMembership_Mapping` | `Property` | `ActorMembership` | `internal/translate/migrate/usecase.go:actorLink`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `UseCaseActor_Mapping` | `Property` | `PartUsage` | `internal/translate/migrate/usecase.go:actorLink`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | +| `UseCaseActorFeatureTyping_Mapping` | `Property` | `FeatureTyping` | `internal/translate/migrate/usecase.go:connectActor`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | +| `UseCaseActorMembership_Mapping` | `Property` | `ActorMembership` | `internal/translate/migrate/usecase.go:connectActor`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | +| `UseCaseActor_Mapping` | `Property` | `PartUsage` | `internal/translate/migrate/usecase.go:connectActor`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | | `UseCaseEmptySubjectReferenceUsage_Mapping` | `UseCase` | `ReferenceUsage` | — | — | ❌ not implemented | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | | `UseCaseObjectiveMembership_Mapping` | `UseCase` | `ObjectiveMembership` | — | — | ❌ not implemented | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | | `UseCaseObjectiveRequirementUsage_Mapping` | `UseCase` | `RequirementUsage` | — | — | ❌ not implemented | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | @@ -1101,9 +1101,11 @@ SysML profile namespace. The counts are elements affected, not effort. | 5 | 9: `RICOAFeatureValueOperatorExpressionFeatureValue_Mapping`, `RICOAFeatureValueOperatorExpressionFeature_Mapping`, `RICOAFeatureValueOperatorExpression_Mapping`, `RICOAFeatureValueOperatorFeatureReferenceExpression_Mapping`, `RICOAFeatureValueOperatorMembership_Mapping`, `RICOAFeatureValueOperatorParameterMembership_Mapping`, `RICOAFeatureValue_Mapping`, `RICOAOutputPin_Mapping`, `ReadIsClassifiedObjectAction_Mapping` | ❌ not implemented | `uml:ReadIsClassifiedObjectAction` | 12 | 0 | 12 | a ReadIsClassifiedObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | | 6 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 12 | 12 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | | 7 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 9 | 11 | a decider loop whose parts exchange values over pins the migrator writes is written as a v2 loop (the loops.xmi tested-first and post-tested fixtures): its loop variables and an `ended` flag as private features, its parts performing in the loop — test before body when tested first, else after — and its results read from the variables at the end; no corpus loop takes the loop form: PSSM's 2 fall back because their body outputs are structured-node output pins the migrator does not write, and TMT's 1 names no decider, so each keeps the body-once form and stays approximated per element; candidate erratum: the mapping gives only an ActionUsage and leaves its details undefined, so a loop's test and iteration are lost | -| 8 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | -| 9 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | -| 10 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | -| 11 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 12 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | +| 8 | 1: `Actor_Mapping` | ⚠️ approximate | `uml:Actor` | 0 | 8 | 8 | an actor is written as a part usage, not a part def: the actor lines of a use case diagram are connections, which join usages | +| 9 | 3: `UseCaseActorFeatureTyping_Mapping`, `UseCaseActorMembership_Mapping`, `UseCaseActor_Mapping` | ⚠️ approximate | `uml:Actor` | 0 | 8 | 8 | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | +| 10 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | +| 11 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | +| 12 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | +| 13 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 14 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | From 8f0eaf46b7636d4e2f78d91e10a7ec0f3a383cec Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:43:20 +0000 Subject: [PATCH 19/19] docs(compliance): keep develop's state rows alongside the region entry changes Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 70f759420d..b859a70a81 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -912,9 +912,6 @@ nothing above. | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| -| A nested default-entry junction belongs to a separate transition out of the composite's `entry` action and is read when that transition is taken, after the outer effect and composite entry; availability checks only the incoming transition's own route (SM32); if no branch is enabled, the run fails with a typed error naming the junction (SM46) | `state_executor.go` `startIn`, `resolveEntryRoute`, `continueEntryRoute` | `state_entry_transition_junction_no_way_fails.sysml`, `state_entry_transition_junction_reads_outer_effect.sysml` | ✅ Faithful | -| History is recorded per region, so a transition that starts inside its own composite's region restores that composite and its inner configuration rather than the region's initial state; a region left with no active state has nothing to restore; a region or body left at `done` leaves no history | `state_executor.go` `exitState`, `recordChildHistory`, `recordRegionHistory`, `forgetRegionHistory`; `state_region_transition.go` `leaveRegion`, `exitRegionTo` | `history_test.go:TestDeepHistoryRestoresARegionLeftFromInsideItsCompositeState`, `:TestHistoryRestoresOrthogonalRegions`, `state_deep_history_region_composite.sysml`, `state_history_revisit.sysml` | ✅ Faithful | -| On default entry, a region whose stand-in has no active child is complete; a region with no stand-in and no start is also complete while inactive. A region activated later by a transition or fork branch must reach its final state. A composite whose regions are all complete by default completes immediately (PSSM *Entering 004*). A stand-in's running do-action keeps its default-complete region incomplete until it ends; the owner is then scheduled if every region is complete | `state_region_entry.go` `regionStart`, `enterRegion`; `state_executor.go` `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`, `scheduleCompletionTransitions`, `settleDoActions` | `state_parallel_region_without_entry_completes.sysml` (+ trace golden; PSSM *Entering 004*), `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml`, `state_fork_only_region_entered_by_default.sysml`, `state_nested_fork_starts_outer_region_inactive.sysml`, `state_history_restores_inactive_parallel_region.sysml` (+ trace goldens); `lower/fork_plan_test.go` | ✅ Faithful | | A named succession or transition endpoint that resolves to a non-vertex or non-action node is reported during analysis; positional, implied, unresolved and flow ends remain unchanged | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go`; `passes/action_endpoint.go` `ActionEndpointPass`; `lower/action_nodes.go` `ActionNodes` and `ActionEndpointAccepted`, including lazy inherited action-node collection | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestActionEndpointPassReportsResolvedNonNodes`, `:TestActionEndpointPassAcceptsLoweredNodes`, `:TestActionEndpointPassDiagnosticAgreesWithLowering`, `:TestActionEndpointPassInheritedNodes`, `:TestActionEndpointPassIgnoresFeatureChainsThroughNonNodes`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`; `lower/action_inherited_test.go`; `runtime/testdata/conformance/action_inherited_endpoint.sysml` | ⚠️ Approximate (a feature chain rooted in a part usage is legal notation and stays silent during validation, but execution still reports the existing lowering error because invoking an action through that chain is not implemented) | | Initial state identification | `lower/state_graph.go:ToStateGraph`; `state_executor.go:686` initialize | `state_simple.sysml` | ✅ Faithful | | A succession out of a state body's own entry subaction names the state it starts in (`entry; then off;`), which is how a machine designates where it starts | `lower/entry_transition.go` `addEntryTransition`, `UnconditionalStart`; `lower/state_graph.go` collectTransitions SuccessionEdge case + `isEntrySubaction` | `lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState`, `lower/transition_source_test.go:TestToStateGraph_UnguardedEntryTransitionIsInitial`, `state_entry_succession_initial.sysml` conformance, `tests/parser/testdata/parse/behavior_exhibit_state_body.golden` | ✅ Faithful | @@ -983,12 +980,13 @@ nothing above. | Choice pseudostates: the guards are read on arrival, after the incoming segment's effect has run, against the data as it then stands; several enabled branches are the existing transition choice point (`ChoiceTaken` at the choice, enumerated by `explore`), an unguarded branch is the else branch, and no enabled branch is the typed `ErrChoiceWithoutBranch` naming the choice | `state_route.go` resolveRoute (resolves a route up to its first choice), travel (exits the states every branch leaves, runs the segments' effects, then resolves the choice), resolveChoice, pickBranch, enabledBranches (guards in declaration order); `errors.go` `ErrChoiceWithoutBranch` | `state_choice_pseudostate.sysml`, `state_region_choice.sysml`, `state_choice_after_incoming_effect`, `state_choice_dynamic_conflict` (+ trace goldens), `explore_test.go:TestExploreDynamicChoiceBranches`, `robustness_test.go:state_choice_without_an_enabled_branch`, `:state_do_body_accept_yields_to_an_open_choice`, `:state_do_body_accept_runs_before_the_choice_reads` | ✅ Faithful | | A succession (`succession first s then a;`) names its endpoints the way a transition does, so it reaches a nested, region-local or qualified vertex, and a pseudostate as well as a state | `resolve/transition.go` `ResolveEndpoint`; `resolve/edge.go` and `resolve/document.go` route succession ends; `passes/state_transition.go`; `lower/state_graph.go` `collectTransitions` (UsageSuccession and SuccessionEdge cases) → `(*StateGraph).endpointVertex`/`endpointState`, over the same `lower.EndpointResolver` a transition's endpoints resolve through | `passes/succession_endpoint_test.go:TestResolvedStateEndpointNotVertexIsReported`, `:TestStateSuccessionEndpointSpellingsAcceptVertices`, `lower/state_graph_nested_test.go:TestSuccessionReachesAPseudostate`, `:TestSuccessionQualifiedTargetNamesTheVertexItQualifies`, `:TestToStateGraph_EntrySuccessionNamesInitialState` | ✅ Faithful (previously matched the endpoint's last name segment against a flat state list, which reached no pseudostate and could bind a same-named vertex of another state) | | Two regions may declare same-named pseudostates, and each is a vertex of its own | `lower/state_graph.go` `StateGraph.Pseudostates` (declaration-ordered slice, not keyed by name), `addPseudostate` | `lower/state_graph_nested_test.go:TestSameNamedPseudostatesInSiblingRegionsAreBothCollected` | ✅ Faithful | -| Junction pseudostates: the route through a junction is resolved statically before the incoming effect; every outgoing guard is read against the current data, several enabled branches are a recorded transition choice, and an unguarded branch is the default when none of the guards holds. The settled route continues through further junctions but stays open at the first choice; a history default is resolved the same way. If a route has no way through, its compound transition is not enabled. The occurrence may go to another enabled transition, be deferred or be discarded as unmatched; a completion with no route is dropped. For a join, selection checks the way out using only the candidate and prior arrivals. Signal/change dispatches count same-occurrence peers only when their regions chose them; firing checks completion against those peers, and a dead route fires none. Timer expiries are separate occurrences even when due together; a dead completing segment is disabled, leaving its source's timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). A history default route disables the transition only when its source is outside the history owner. Only `errNoWayThrough` disables: cycles, unevaluable guards and binding failures still fail as the transition fires. Dead branches whose continuation has no way through are filtered from a junction draw; one surviving branch is followed without a draw. Choice remains dynamic, and a dead junction reached beyond a choice remains a run error. UML 2.5.1 §14.2.3.8.1 (compound transition) and §14.2.3.7 (junction) are the extension's references. KerML `ControlPerformances::DecisionPerformance` declares `feature outgoingHBLink: HappensBefore[1]`; it does not specify no-way-through or unmatched-event behavior | `state_route.go` `resolveRoute`, `routeAvailable`, `followOut`, `enabledBranches`, `junctionDraw`, `travel`, `travelResolving`, `settleDraws`, `pickBranch`, `errNoWayThrough`; `state_executor.go` transition enablement and history-default routing | `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`, `state_junction_no_way_through_unmatched.sysml`, `state_junction_no_way_through_other_transition_fires.sysml`, `state_junction_no_way_through_deferred.sysml`, `state_junction_dead_branch_not_drawn.sysml`, `state_completion_no_way_through_dropped.sysml`, `state_history_default_no_way_through.sysml`, `state_history_self_transition_default_no_way_through_restores.sysml`, `state_join_no_way_out_disables_last_segment.sysml`; `explore_test.go:TestExploreStaticJunctionBranches`, `:TestExploreJunctionDrawnAsTransitionFires`, `:TestExploreHistoryDefaultThroughJunction`; `replay_test.go:TestReplayRefusedChoiceUndoesTheJunctionDrawBeforeIt`, `:TestReplayRefusedChoiceUndoesTheHistoryDefaultsJunctionDraw`; `robustness_junction_exit_route_test.go:TestRuntimeRobustnessJunctionExitRoute`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead junction beyond choice, cycle, unevaluable guard, join exit failure); `robustness_test.go:region_pseudostate_without_satisfied_guard` | ✅ Faithful | -| Fork pseudostates enter one composite state's orthogonal regions, with at least two unguarded, untriggered branches and one target per region (UML 2.5.1 §14.2.3.5). A branch bypasses the named region's initial transition and enters every state on the way to its target. A region omitted by the fork follows default entry: its stand-in is entered even without a start; if no child becomes active, the region counts as complete, as does a region with no stand-in and no start (PSSM *Entering 004*). A later fork branch that activates a region must reach its final state before the owner completes. Entering through a fork exits the source configuration down to the ancestor shared by the source and composite, runs branch effects in region order and enters each branch's target; an omitted region with a start does not start ahead of the branches. Fork effects read and write the declaring state's attributes, and branches ending at `done` complete the composite or machine. A running do-action still defers completion. The static footprint retains dependencies of regions entered by branches or by their defaults | `lower/fork_plan.go` `ForkPlan`, `planFork`, `forkOwner`, `enclosingRegion`, `ForkStarted`; `lower/state_footprint.go` `stateFootprintBuilder.pseudostate` (fork case), `entersRegion`; `lower/state_graph.go` `ownTransitionEffects`; `state_executor.go` `fireForkTransition`, `leaveForFork`, `exitRegionsOf`, `completeIfDone`, `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`; `state_region_entry.go` `forkEntry`, `enterForkBranches`, `enterRegion`, `enterLazily`, `regionStart` | `lower/fork_plan_test.go` (`TestToStateGraph_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `:TestToStateGraph_ForkEntersRegionsWithoutInitial`); `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`, `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_region_without_entry_completes.sysml` + trace golden, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden; `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_branch_with_a_trigger` | ✅ Faithful | +| Junction pseudostates: the route through a junction is resolved statically before the incoming effect; every outgoing guard is read against the current data, several enabled branches are a recorded transition choice, and an unguarded branch is the default when none of the guards holds. The settled route continues through further junctions but stays open at the first choice; a history default is resolved the same way. Availability checks only the incoming transition's own route (SM32): if that route has no way through, its compound transition is not enabled. A nested default-entry junction belongs to a separate transition out of the composite's `entry` action and is read when that transition is taken, after the outer effect and composite entry; if no branch is enabled, the run fails with a typed error naming the junction (SM46), as covered by `state_entry_transition_junction_no_way_fails.sysml` and `state_entry_transition_junction_reads_outer_effect.sysml`. The occurrence may go to another enabled transition, be deferred or be discarded as unmatched; a completion with no route is dropped. For a join, selection checks the way out using only the candidate and prior arrivals. Signal/change dispatches count same-occurrence peers only when their regions chose them; firing checks completion against those peers, and a dead route fires none. Timer expiries are separate occurrences even when due together; a dead completing segment is disabled, leaving its source's timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). A history default route disables the transition only when its source is outside the history owner. Only `errNoWayThrough` disables: cycles, unevaluable guards and binding failures still fail as the transition fires. Dead branches whose continuation has no way through are filtered from a junction draw; one surviving branch is followed without a draw. Choice remains dynamic, and a dead junction reached beyond a choice remains a run error. UML 2.5.1 §14.2.3.8.1 (compound transition) and §14.2.3.7 (junction) are the extension's references. KerML `ControlPerformances::DecisionPerformance` declares `feature outgoingHBLink: HappensBefore[1]`; it does not specify no-way-through or unmatched-event behavior | `state_route.go` `resolveRoute`, `routeAvailable`, `followOut`, `enabledBranches`, `junctionDraw`, `travel`, `travelResolving`, `settleDraws`, `pickBranch`, `errNoWayThrough`; `state_executor.go` `startIn`, `resolveEntryRoute`, `continueEntryRoute`, transition enablement and history-default routing | `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`, `state_junction_no_way_through_unmatched.sysml`, `state_junction_no_way_through_other_transition_fires.sysml`, `state_junction_no_way_through_deferred.sysml`, `state_junction_dead_branch_not_drawn.sysml`, `state_completion_no_way_through_dropped.sysml`, `state_history_default_no_way_through.sysml`, `state_history_self_transition_default_no_way_through_restores.sysml`, `state_join_no_way_out_disables_last_segment.sysml`, `state_entry_transition_junction_no_way_fails.sysml`, `state_entry_transition_junction_reads_outer_effect.sysml`; `explore_test.go:TestExploreStaticJunctionBranches`, `:TestExploreJunctionDrawnAsTransitionFires`, `:TestExploreHistoryDefaultThroughJunction`; `replay_test.go:TestReplayRefusedChoiceUndoesTheJunctionDrawBeforeIt`, `:TestReplayRefusedChoiceUndoesTheHistoryDefaultsJunctionDraw`; `robustness_junction_exit_route_test.go:TestRuntimeRobustnessJunctionExitRoute`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead junction beyond choice, cycle, unevaluable guard, join exit failure); `robustness_test.go:region_pseudostate_without_satisfied_guard` | ✅ Faithful | +| 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) may enter the composite with that no-start region inactive; a top-level machine region without a start and a region whose stand-in is parallel are 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`; `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_RegionWithoutInitialOrForkRemainsInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByDefaultLeavesItInactive`, `:TestToStateGraph_ForkOnlyRegionEnteredByAnEnclosingEntryLeavesItInactive`, `:TestToStateGraph_ForkOmissionLeavesRegionInactive`, `:TestToStateGraph_NestedForkLeavesUnenteredRegionsInactive`, `: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_branch_with_a_trigger` | ✅ Faithful | +| On default entry, a composite region without a start enters its stand-in without activating a child, or remains inactive when it has no stand-in. Either form is complete by default, so a composite whose regions are all default-complete completes immediately (PSSM *Entering 004*); a region activated later by a transition or fork branch must reach a final state before its owner completes. A stand-in's running do-action defers completion until it ends. | `state_region_entry.go` `enterRegion`, `regionStart`, `enterLazily`; `state_executor.go` `allRegionsDefaultComplete`, `regionComplete`, `stateComplete`, `scheduleCompletionTransitions`, `settleDoActions`; `lower/state_graph.go` `ToStateGraph` | `lower/state_graph_nested_test.go:TestToStateGraph_ParallelWithoutInitialFailsClearly`; `robustness_test.go:parallel_state_region_without_initial`, `:parallel_state_region_itself_parallel`; `robustness_inactive_region_test.go:TestRuntimeRobustnessInactiveRegion`; `robustness_parallel_wrapper_test.go:TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`; `state_parallel_region_without_entry_completes.sysml` + trace golden (PSSM *Entering 004*), `state_parallel_region_without_entry_inactive.sysml` + trace golden, `state_parallel_stateless_region_with_behaviors.sysml`, `state_parallel_stand_in_do_action_defers_completion.sysml` + trace golden, `state_fork_only_region_entered_by_default.sysml` + trace golden, `state_nested_fork_starts_outer_region_inactive.sysml` + trace golden | ✅ Faithful | | Join pseudostates: each occurrence fires every not-yet-arrived segment it enables and whose region's dispatch chose it, exiting each source, running its effect and recording each in `joinArrived` only after the firing succeeds; when multiple segments fire, they are drawn in `join ` order. At selection, including for each time expiry, probes count only the candidate and prior arrivals, checking the way out only if they complete the join. In signal/change dispatches, same-occurrence peers count only when their regions chose them; firing checks completion against those peers, and if the route is dead, none fire. Timer expiries are separate occurrences even at the same instant; a dead completing segment is disabled, leaving its source's other timer-group alternatives available (`state_join_peer_not_chosen_does_not_block_arrival`, `state_join_time_segments_expire_together`, `state_join_dead_timer_join_keeps_group_alternative`). The join completes when prior arrivals plus segments enabled by the current occurrence and chosen by their regions in that dispatch cover every incoming segment and the outgoing route is available. The current segments are drawn as `join `, arrivals clear and the owner is left after the last segment effect. Completion segments fire only on their own completion; waiting regions do not complete their owner; arrivals clear on region re-entry or when another transition leaves the owner. Snapshots, held images and canonical check state preserve arrivals. PSSM leaves the owner before the final incoming effect; leaving after that effect is the extension's reading recorded in SM34. `TransitionPerformances.kerml` says `private succession [1] transitionLinkSource then [*] effect;`; `StatePerformances.kerml` says `and hence happening during the state performance.` Neither supplies a cross-segment arrival rule; that is the UML/PSSM extension reference | `state_route.go` `joinFiringNow`; `state_executor.go` `fireJoinTransition`, `fireJoinSegment`, `fireJoinIncoming`, `leaveJoinOwner`, `regionWaitingAtJoin`, `setRegionState`, `clearJoinArrivalsOwnedBy`; `snapshot.go`, `held_image_behavior.go`, `check_state.go` preserve `joinArrived` | Conformance fixtures: `state_fork_join_pseudostate`, `state_join_arrival_dropped_when_owner_exited`, `state_join_change_segment_rises_alone`, `state_join_change_segment_waits_before_junction`, `state_join_change_segment_waits_before_junction_metadata`, `state_join_change_segments_rise_together`, `state_join_completion_is_not_a_timers_expiry`, `state_join_completion_segment_waits_for_do_behavior`, `state_join_dead_timer_join_keeps_group_alternative`, `state_join_exit_from_regions_before_owner_exit`, `state_join_from_composite_sources`, `state_join_from_nested_states`, `state_join_no_way_out_disables_last_segment`, `state_join_of_machine_regions_from_nested_source`, `state_join_runs_every_incoming_effect`, `state_join_segment_fires_on_own_signal`, `state_join_segments_arrive_together_on_one_occurrence`, `state_join_segment_not_chosen_does_not_arrive`, `state_join_peer_not_chosen_does_not_block_arrival`, `state_join_segment_reads_its_payload`, `state_join_segment_trigger_unmatched`, `state_join_time_segment_needs_same_occurrence`, `state_join_time_segment_not_taken_by_signal`, `state_join_time_segment_unsynchronized_reads_no_route`, `state_join_time_segment_unsynchronized_reads_no_route_metadata`, `state_join_time_segments_expire_apart`, `state_join_time_segments_expire_together`, `state_join_waits_for_every_segment_enabled`; tests: `pseudostate_test.go:TestForkAndJoinPseudostates`, `:TestJoinWaitsForEveryBranch`, `:TestJoinSegmentFiresOnOwnOccurrence`, `:TestJoinDisarmedSegmentCanArriveAlone`, `:TestJoinFromActiveCompositeSourcesFires`, `:TestJoinCompletionSegmentWaitsForItsSourcesDoBehavior`, `:TestJoinCompletionSegmentWaitsForItsSourcesBody`; `fork_join_test.go:TestForkJoinVisitOrderIsDeterministic`; `choice_test.go:TestCompletionTransitionArrivesAtUnreadyJoin`; `state_change_trigger_test.go:TestChangeTriggerDisarmedSegmentCanWaitForItsPeer`; `join_snapshot_test.go:TestSnapshotRestoresJoinArrivals`, `:TestCloneNilJoinArrivalsIsWritable`; `check_test.go:TestCheckJoinWaitsForSlowestBranch`; `replay_test.go:TestReplayRefusedJoinDrawChangesNothing`, `:TestReplayRefusedJoinDrawUndoesTheSegmentsChoice`; `robustness_junction_join_test.go:TestRuntimeRobustnessJunctionJoin` (dead signal and timer join exits, exit failure) | ✅ 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 | | A segment of a compound transition that continues inside a region of the parallel state the transition enters runs its effect on that region's entry front: after the parallel state's entry, before its own target's entry, and interleaved with the sibling regions' entry units, since nothing in `StatePerformances.kerml` orders one region's `middle` performances against another's ([the derivation](behavior-semantic-oracle.md#effects-on-the-way-into-the-regions-of-a-parallel-state-each-precedes-its-own-targets-entry-the-regions-interleave)); a segment before the parallel state is entered runs where it did. Availability checks only the incoming transition's own route (SM32). A nested default entry is a separate transition out of the composite's `entry` action: its junction is read when that transition is taken, after the outer effect and composite entry; a dead route fails with a typed run error naming the junction (SM46). For a draw, nested default-entry targets are alternatives: any available target suffices, while every region of a composite target must have an available default entry; `state_entry_route_dead_alternative.sysml` covers a viable alternative beside a dead nested route. The conformance cases `state_entry_transition_junction_no_way_fails.sysml` and `state_entry_transition_junction_reads_outer_effect.sysml` cover the timing and failure | `runtime/state_route.go` `routeAvailable`, `resolveRoute`, `followOut`; `runtime/state_executor.go` `startIn`, `resolveEntryRoute`, `continueEntryRoute`; `runtime/state_region_entry.go` `enterRegionsInto`, `enterRegion` (`runEntryRouteEffects`); `runtime/state_run_to_completion.go` `holdEntry`/`performHeld` (a held cascade keeps its region's pending effects) | conformance `state_junction_inside_orthogonal_region.sysml` (explored: three orders; + trace, seed, declared and check goldens), `state_entry_transition_junction_no_way_fails.sysml`, `state_entry_transition_junction_reads_outer_effect.sysml`, `state_entry_route_dead_alternative.sysml` | ✅ Faithful | -| History pseudostates (shallow and deep). A region's last active configuration is recorded and restored; a region or body left at `done` leaves no history. With no recorded configuration, history takes its own outgoing transition if present, otherwise it performs the owner's default entry through its entry transition or region defaults. A region without a stand-in and start is a valid empty default; its composite owner can be entered through history and the region remains inactive. A top-level machine history or owner with no default entry at all yields typed `ErrHistoryWithoutEntry` naming the history and owner | `parser/behavior.go` parseStateMember; `lower/state_metadata.go`; `state_executor.go` `historyEntry`, `hasDefaultEntry`, `historyRecorded`, `deepestRecorded`, `exitState`, `recordChildHistory`, `recordRegionHistory`, `forgetRegionHistory`; `errors.go` `ErrHistoryWithoutEntry`; `state_region_transition.go` `leaveRegion`, `exitRegionTo`; `lower/state_graph.go` `PseudostateOwner` | `state_history_first_entry_inactive_region.sysml` + trace golden, `state_history_revisit.sysml` + trace golden, `state_shallow_history.sysml`, `state_deep_history.sysml`; `history_test.go` history restoration tests; `robustness_test.go:history_without_record_default_or_entry` | ✅ 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 or the same region defaults 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 ;`) and `lower/state_metadata.go` (`#shallowHistory state`/`#deepHistory state`); `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`, `state_history_first_entry_inactive_region.sysml` + trace golden, `state_history_restores_inactive_parallel_region.sysml` + trace golden` | ✅ 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 | | A transition between two regions of one composite state exits its source only: KerML `StatePerformances::StateTransitionPerformance` orders `private succession [*] guard then [1] transitionLinkSource.exit`, so the composite state is neither exited nor re-entered, the source's region is left without an active state, and the regions holding neither endpoint keep theirs. A target nested inside the target region's own composite state moves that inner region, exiting the state it was running. A region whose target is nested inside a composite state it is not running records that composite state as its active one. A source active in a region nested deeper than its target's region leaves its region set up to the level the two share, exiting the composite state holding its own region. The level is found by walking outward through the region declaring the region owner's nearest region-declared ancestor, so a region owned by a plain substate is not missed. A target outside the composite state still exits it and its regions — each state on the way out exactly once, the region a state is active in being cleared before that state is exited, since a region's active state may be nested below it — and a nested non-orthogonal transition still exits up to the endpoints' least common ancestor | `state_region_transition.go` `fireTransitionInRegion`, `concurrentRegionsFor`, `enclosingRegion`, `siblingRegionContaining`, `moveBetweenRegions`, `exitRegionTo`, `leaveRegion`, `isBelowOrEqual`; `state_executor.go` `getLCA` (nested and outward transitions only) | `state_transition_cross_region.sysml` + trace golden, `state_transition_cross_region_third_region.sysml`, `state_transition_cross_region_nested_target.sysml`, `state_transition_cross_region_inactive_wrapper.sysml`, `state_transition_cross_region_deep_source.sysml`, `state_transition_cross_region_substate_owner.sysml`, `state_transition_leave_composite_substate_region.sysml`, `state_transition_sibling_region.sysml`, `state_region_cross_pseudostate.sysml` + trace golden, `cross_region_transition_test.go:TestCrossRegionTransitionExitsSourceOnly`, `:TestCrossRegionTransitionIntoNestedTargetExitsTheAbandonedState`, `:TestCrossRegionTransitionIntoInactiveCompositeRecordsTheEnteredState`, `:TestCrossRegionTransitionFromDeeperRegionExitsUpToTheSharedLevel`, `:TestCrossRegionTransitionFromARegionOwnedByASubstate`, `:TestNestedTransitionExitsUpToTheLCA`, `:TestTransitionOutOfCompositeStateExitsEveryRegion`, `:TestTransitionOutOfCompositeStateExitsNestedStatesOnce`, `region_pseudostate_test.go:TestRegionPseudostateIntoSiblingRegionExitsSourceOnly`, `robustness_test.go:state_cross_region_transitions_ping_pong` | ✅ Faithful |