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_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_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..b49e423d9a 100644
--- a/internal/ir/lower/state_inherited_test.go
+++ b/internal/ir/lower/state_inherited_test.go
@@ -462,3 +462,74 @@ 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])
+ }
+ }
+}
+
+// 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 d7b1351b82..6824338ed3 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,29 @@ func (m *migration) itemFlow(f *sysmlv1.Element, ends []*sysmlv1.Element, paths
return
}
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.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 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 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 +3199,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..80507585e3 100644
--- a/tests/migrate/relations_test.go
+++ b/tests/migrate/relations_test.go
@@ -1557,3 +1557,122 @@ 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)
+ }
+}
+
+// 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)
+}