From b79122d84f323e488d1d5a50e89e1fc87c74088e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:24:00 +0000 Subject: [PATCH 1/2] migrate: find item flow ends through the parts and blocks a tool names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An «ItemFlow» whose source and target are the parts on the realizing connector's ends, the blocks typing them, or the connector's owner at its own port — as Cameo writes a flow drawn between parts — now flows between the ports those ends name instead of being left as a comment. lower: a view, viewpoint, rendering or metadata member of a state def is not state content, so a usage typed by it inherits past it instead of failing to lower, which left migrated state diagrams empty. Co-Authored-By: jason.han --- .../migrate-item-flow-part-ends.fixed.md | 1 + .../state-def-views-inherited.fixed.md | 1 + docs/reference/sysml-v1-migration.md | 2 +- internal/ir/lower/state_inheritance.go | 5 +- internal/ir/lower/state_inherited_test.go | 39 +++++++++++ internal/translate/migrate/migrate.go | 33 +++++++-- tests/migrate/relations_test.go | 70 +++++++++++++++++++ 7 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 changes/unreleased/migrate-item-flow-part-ends.fixed.md create mode 100644 changes/unreleased/state-def-views-inherited.fixed.md diff --git a/changes/unreleased/migrate-item-flow-part-ends.fixed.md b/changes/unreleased/migrate-item-flow-part-ends.fixed.md new file mode 100644 index 0000000000..3e21c6ece1 --- /dev/null +++ b/changes/unreleased/migrate-item-flow-part-ends.fixed.md @@ -0,0 +1 @@ +- **A migrated item flow finds its ends through the parts and blocks a tool names.** An «ItemFlow» whose `informationSource` and `informationTarget` are the parts on the realizing connector's ends, the blocks typing them, or the connector's owner at its own port — as Cameo writes a flow drawn between parts — is now written as the `flow` between the ports those ends name. It was left as a comment, since only the connector ends' roles themselves were accepted; the report's note for a flow naming neither end now says what was tried. diff --git a/changes/unreleased/state-def-views-inherited.fixed.md b/changes/unreleased/state-def-views-inherited.fixed.md new file mode 100644 index 0000000000..ada4d474f9 --- /dev/null +++ b/changes/unreleased/state-def-views-inherited.fixed.md @@ -0,0 +1 @@ +- **A state usage inherits a definition that declares a view.** A `view`, `viewpoint`, `rendering` or `metadata` annotation in a state def body — what a migrated state machine diagram writes into its state def — made every usage typed by it fail to lower, so a state diagram showing such a usage rendered empty and its layout annotations warned that no node is drawn. Such members are not state content and are now passed over, like a comment or a nested definition. diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 917ee958c3..1ac8bd2cc1 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -174,7 +174,7 @@ ties each of its mapping classes to the code that carries it out (or records why | Property or port sharing the name of an inherited feature without redefining it | `:>>` the inherited feature when both are the same kind of usage; otherwise the collision is reported and left | approximated | | Connector, nested ends | `connect a.b to c.d` | mapped | | «BindingConnector» | `bind`, or `binding name bind` when named | mapped | -| InformationFlow / «ItemFlow» over a connector | `flow of Item from a.x to b.y` | mapped | +| InformationFlow / «ItemFlow» over a connector | `flow of Item from a.x to b.y`, from the end standing for the flow's source to the one standing for its target: the end whose role it is, whose path names it or a part typed by it (a flow a tool draws between parts or blocks), or the connector's own port when it is the connector's owner | mapped | | «Satisfy» | `satisfy requirement … by …` in the satisfying usage's owner | mapped | | «Verify» from a test case | `verify` in the verification def | mapped | | «DeriveReqt» | `connection … :> RequirementDerivation::Derivation` | mapped | diff --git a/internal/ir/lower/state_inheritance.go b/internal/ir/lower/state_inheritance.go index 2b9f816c2b..b6947e6775 100644 --- a/internal/ir/lower/state_inheritance.go +++ b/internal/ir/lower/state_inheritance.go @@ -426,12 +426,13 @@ func loweredElsewhere(member ast.Node) bool { switch n := member.(type) { case *ast.Comment, *ast.Documentation, *ast.TextualRepresentation, *ast.SuccessionEdge, *ast.TransitionEdge, *ast.TransitionMember, - *ast.InitialNode, *ast.FinalNode, + *ast.InitialNode, *ast.FinalNode, *ast.PrefixMetadata, *ast.Definition, *ast.Package, *ast.ErrorNode: return true case *ast.Usage: switch n.Kind { - case ast.UsageSuccession, ast.UsageTransition, ast.UsagePort: + case ast.UsageSuccession, ast.UsageTransition, ast.UsagePort, + ast.UsageView, ast.UsageViewpoint, ast.UsageRendering, ast.UsageViewRendering, ast.UsageMetadata: return true } } diff --git a/internal/ir/lower/state_inherited_test.go b/internal/ir/lower/state_inherited_test.go index 8d691d229c..c070cd8001 100644 --- a/internal/ir/lower/state_inherited_test.go +++ b/internal/ir/lower/state_inherited_test.go @@ -462,3 +462,42 @@ func inheritedTarget(t *testing.T, graph *StateGraph, usage *ast.StateNode, name t.Fatalf("no transition under %s ends at %s", usage.Name, name) return nil } + +// A view, a viewpoint, a rendering or a metadata annotation a state definition +// declares is not state content, so a usage of the definition inherits its +// substates past them. +func TestToStateGraphTypedStateUsageSkipsInheritedViewsAndMetadata(t *testing.T) { + graph := stateGraphOf(t, ` + package test { + metadata def Note; + state def Inner { + view V : StandardViewDefinitions::StateTransitionView { expose Inner; render Views::asInterconnectionDiagram; } + viewpoint P; + rendering R; + @Note; + metadata Note about i1; + entry; then i1; + state i1; + state i2; + } + state def Machine { + entry; then nested; + state nested : Inner; + } + } + `, "Machine") + + nested := stateNamed(graph, "nested") + if nested == nil { + t.Fatal("typed state usage was not collected") + } + for _, name := range []string{"i1", "i2"} { + child := stateNamed(graph, name) + if child == nil { + t.Fatalf("inherited substate %s was not collected", name) + } + if graph.ParentState[child] != nested { + t.Fatalf("parent of %s = %v, want nested", name, graph.ParentState[child]) + } + } +} diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index d7b1351b82..1c4f831539 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -3031,7 +3031,7 @@ func (m *migration) connector(c *sysmlv1.Element) { m.add(c, Mapped, target, note) m.stereotypeComments(c) for _, f := range m.flows[c] { - m.itemFlow(f, c.Owned("end"), paths) + m.itemFlow(f, c, segs, paths) } } @@ -3137,8 +3137,10 @@ func segmentWord(i, n int) string { // itemFlow writes an item flow realized by a connector as a flow between the // flow properties its ends carry for each conveyed classifier, from the end -// whose role is the flow's source to the end whose role is its target. -func (m *migration) itemFlow(f *sysmlv1.Element, ends []*sysmlv1.Element, paths []string) { +// standing for the flow's source to the end standing for its target: the end +// whose role is it, or, as a tool writes a flow between parts or blocks, whose +// path names it or a part typed by it. +func (m *migration) itemFlow(f, c *sysmlv1.Element, segs [][]*sysmlv1.Element, paths []string) { conveyed := m.model.Refs(f, "conveyed") missing := m.dangling(f, "conveyed") if len(conveyed) == 0 { @@ -3152,14 +3154,17 @@ func (m *migration) itemFlow(f *sysmlv1.Element, ends []*sysmlv1.Element, paths return } from, to := paths[0], paths[1] + owner := c.Parent switch { - case m.model.Ref(ends[0], "role") == src && m.model.Ref(ends[1], "role") == dst: - case m.model.Ref(ends[1], "role") == src && m.model.Ref(ends[0], "role") == dst: + case m.endStandsFor(segs[0], owner, src) && m.endStandsFor(segs[1], owner, dst): + case m.endStandsFor(segs[1], owner, src) && m.endStandsFor(segs[0], owner, dst): from, to = paths[1], paths[0] + segs = [][]*sysmlv1.Element{segs[1], segs[0]} default: - m.flowDone(f, nil, []string{"the item flow's source and target are not the roles of the ends of realizing connector " + describe(ends[0].Parent)}) + m.flowDone(f, nil, []string{"the item flow's source and target are not the ends of realizing connector " + describe(c) + ", nor the parts or blocks on its ends' paths"}) return } + src, dst = segs[0][len(segs[0])-1], segs[1][len(segs[1])-1] var written, notes []string if missing != "" { notes = append(notes, missing) @@ -3182,6 +3187,22 @@ func (m *migration) itemFlow(f *sysmlv1.Element, ends []*sysmlv1.Element, paths m.flowDone(f, written, notes) } +// endStandsFor reports whether a connector end's path stands for el: its role, +// a part on its path, or the block typing one of them, which is how a tool +// writes an item flow between parts or between the blocks typing them; an end +// at a port of the connector's owner stands for the owner itself. +func (m *migration) endStandsFor(segs []*sysmlv1.Element, owner, el *sysmlv1.Element) bool { + if len(segs) == 1 && segs[0].Type == "Port" && owner != nil && owner == el { + return true + } + for _, s := range segs { + if s == el || m.model.Ref(s, "type") == el { + return true + } + } + return false +} + // flowDone records one realizing connector's result for f and reports the flow // once the last of them is in. func (m *migration) flowDone(f *sysmlv1.Element, written, notes []string) { diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index dc020f20f0..1e98373c9d 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1557,3 +1557,73 @@ func TestOpaqueExpressionsMayNameImportedMembers(t *testing.T) { wantNote(t, r, "_gear", migrate.Approximated, "private visibility is not written: v2 lets nothing outside the package reach a private member") wantClean(t, "imports.sysml", r) } + +// partFlowModel is a system whose item flow names the blocks typing the parts on +// the realizing connector's ends, as Cameo writes a flow drawn between parts, and +// a second connector from the system's own port whose flow names the system. +const partFlowModel = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + +const partFlowApplications = ` + + + + + + + + + + + + + + ` + +// An item flow whose source and target are the parts on the realizing +// connector's ends, the blocks typing them, or the connector's owner at its +// own port, flows between the ports those ends name; one naming an element on +// neither end is left as a comment. +func TestItemFlowEndsMayBeThePartsOrBlocksOnTheConnector(t *testing.T) { + r := migrateDocument(t, partFlowModel, partFlowApplications) + wantLine(t, r.Notation, "flow of Fuel from pump.outlet.fuelOut to engine.inlet.fuelIn;") + wantLine(t, r.Notation, "flow of Fuel from pump.outlet.fuelOut to supply.fuelIn;") + for _, id := range []string{"_if", "_if2"} { + if es := entriesFor(r, id); len(es) != 1 || es[0].Verdict != migrate.Mapped { + t.Errorf("entries for %s = %+v, want one mapped", id, es) + } + } + wantNote(t, r, "_if3", migrate.Unmapped, "the item flow's source and target are not the ends of realizing connector") + wantNote(t, r, "_if3", migrate.Unmapped, "nor the parts or blocks on its ends' paths") + if diags := errors(t, "t.sysml", r.Notation); len(diags) > 0 { + t.Errorf("migrated notation has errors: %v", diags) + } +} From 79f848f264e50324cb0a749ac4d9a871cabb644b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:48:59 +0000 Subject: [PATCH 2/2] migrate: an item flow whose ends stand for both connector ends is not given a direction Two same-typed parts at same-typed ports let a flow from the block to the port type stand for either end; only the ends' roles may then tell them apart, and when they cannot the flow is left as a comment instead of taking the connector's end order. lower: a parallel state def's views, viewpoints, renderings and metadata are no regions, so a usage typed by it lowers as an ordinary one does. Co-Authored-By: jason.han --- internal/ir/lower/state_graph.go | 5 ++- internal/ir/lower/state_inherited_test.go | 32 +++++++++++++++ internal/translate/migrate/migrate.go | 16 +++++++- tests/migrate/relations_test.go | 49 +++++++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/internal/ir/lower/state_graph.go b/internal/ir/lower/state_graph.go index 79d8987ad9..a0a3668a9f 100644 --- a/internal/ir/lower/state_graph.go +++ b/internal/ir/lower/state_graph.go @@ -1111,11 +1111,12 @@ func parallelOwnedMember(member ast.Node) bool { *ast.EntryMember, *ast.DoMember, *ast.ExitMember, *ast.PseudostateNode, *ast.SuccessionEdge, *ast.TransitionEdge, *ast.TransitionMember, - *ast.Definition, *ast.Package, *ast.ErrorNode: + *ast.PrefixMetadata, *ast.Definition, *ast.Package, *ast.ErrorNode: return true case *ast.Usage: switch n.Kind { - case ast.UsageAttribute, ast.UsageItem, ast.UsagePort, ast.UsageSuccession, ast.UsageMetadata: + case ast.UsageAttribute, ast.UsageItem, ast.UsagePort, ast.UsageSuccession, ast.UsageMetadata, + ast.UsageView, ast.UsageViewpoint, ast.UsageRendering, ast.UsageViewRendering: return true } return IsTerminateUsage(n) diff --git a/internal/ir/lower/state_inherited_test.go b/internal/ir/lower/state_inherited_test.go index c070cd8001..b49e423d9a 100644 --- a/internal/ir/lower/state_inherited_test.go +++ b/internal/ir/lower/state_inherited_test.go @@ -501,3 +501,35 @@ func TestToStateGraphTypedStateUsageSkipsInheritedViewsAndMetadata(t *testing.T) } } } + +// A parallel state definition's views and metadata are no regions, so a usage +// typed by it still lowers to its regions. +func TestToStateGraphTypedParallelStateUsageSkipsInheritedViewsAndMetadata(t *testing.T) { + graph := stateGraphOf(t, ` + package test { + metadata def Note; + state def Inner parallel { + view V : StandardViewDefinitions::StateTransitionView { expose Inner; render Views::asInterconnectionDiagram; } + viewpoint P; + rendering R; + @Note; + state left { entry; then idle; state idle; } + state right { entry; then busy; state busy; } + } + state def Machine { + entry; then nested; + state nested : Inner; + } + } + `, "Machine") + + nested := stateNamed(graph, "nested") + if nested == nil { + t.Fatal("typed parallel state usage was not collected") + } + for _, name := range []string{"idle", "busy"} { + if stateNamed(graph, name) == nil { + t.Fatalf("inherited region substate %s was not collected", name) + } + } +} diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 1c4f831539..6824338ed3 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -3155,11 +3155,23 @@ func (m *migration) itemFlow(f, c *sysmlv1.Element, segs [][]*sysmlv1.Element, p } from, to := paths[0], paths[1] owner := c.Parent + forward := m.endStandsFor(segs[0], owner, src) && m.endStandsFor(segs[1], owner, dst) + reverse := m.endStandsFor(segs[1], owner, src) && m.endStandsFor(segs[0], owner, dst) + ambiguous := forward && reverse + if ambiguous { + // Both ends stand for both; only the ends' own roles can still tell them apart. + ends := c.Owned("end") + forward = m.model.Ref(ends[0], "role") == src && m.model.Ref(ends[1], "role") == dst + reverse = m.model.Ref(ends[1], "role") == src && m.model.Ref(ends[0], "role") == dst + } switch { - case m.endStandsFor(segs[0], owner, src) && m.endStandsFor(segs[1], owner, dst): - case m.endStandsFor(segs[1], owner, src) && m.endStandsFor(segs[0], owner, dst): + case forward && !reverse: + case reverse && !forward: from, to = paths[1], paths[0] segs = [][]*sysmlv1.Element{segs[1], segs[0]} + case ambiguous: + m.flowDone(f, nil, []string{"the item flow's source and target each stand for both ends of realizing connector " + describe(c) + ", so its direction cannot be told"}) + return default: m.flowDone(f, nil, []string{"the item flow's source and target are not the ends of realizing connector " + describe(c) + ", nor the parts or blocks on its ends' paths"}) return diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index 1e98373c9d..80507585e3 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1627,3 +1627,52 @@ func TestItemFlowEndsMayBeThePartsOrBlocksOnTheConnector(t *testing.T) { t.Errorf("migrated notation has errors: %v", diags) } } + +// twinFlowModel joins two parts of one block at ports of one type, so a flow +// from the block to the port type stands for either end; one from one port to +// the other is told apart by the ends' roles. +const twinFlowModel = ` + + + + + + + + + + + + + + + + + + + + + ` + +const twinFlowApplications = ` + + + + + + + + ` + +// An item flow whose source and target each stand for both ends of the +// connector is not given the connector's end order as its direction: it is +// left as a comment, while one the parts tell apart still follows them. +func TestItemFlowWithEndsStandingForBothIsNotMigrated(t *testing.T) { + r := migrateDocument(t, twinFlowModel, twinFlowApplications) + wantNote(t, r, "_if", migrate.Unmapped, "each stand for both ends of realizing connector") + wantLine(t, r.Notation, "flow of Fuel from left.outlet.fuel to right.outlet.fuel;") + if es := entriesFor(r, "_if2"); len(es) != 1 || es[0].Verdict != migrate.Mapped { + t.Errorf("entries for _if2 = %+v, want one mapped", es) + } + wantClean(t, "t.sysml", r) +}