Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/migrate-item-flow-part-ends.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changes/unreleased/state-def-views-inherited.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion docs/reference/sysml-v1-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
5 changes: 3 additions & 2 deletions internal/ir/lower/state_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions internal/ir/lower/state_inheritance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
return true
}
}
Expand Down
71 changes: 71 additions & 0 deletions internal/ir/lower/state_inherited_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
45 changes: 39 additions & 6 deletions internal/translate/migrate/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
119 changes: 119 additions & 0 deletions tests/migrate/relations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<packagedElement xmi:type="uml:Class" xmi:id="_fuel" name="Fuel"/>
<packagedElement xmi:type="uml:Class" xmi:id="_out_if" name="Outlet">
<ownedAttribute xmi:type="uml:Property" xmi:id="_out_fuel" name="fuelOut" type="_fuel"/>
</packagedElement>
<packagedElement xmi:type="uml:Class" xmi:id="_in_if" name="Inlet">
<ownedAttribute xmi:type="uml:Property" xmi:id="_in_fuel" name="fuelIn" type="_fuel"/>
</packagedElement>
<packagedElement xmi:type="uml:Class" xmi:id="_pump" name="Pump">
<ownedAttribute xmi:type="uml:Port" xmi:id="_pump_out" name="outlet" type="_out_if" aggregation="composite"/>
</packagedElement>
<packagedElement xmi:type="uml:Class" xmi:id="_engine" name="Engine">
<ownedAttribute xmi:type="uml:Port" xmi:id="_engine_in" name="inlet" type="_in_if" aggregation="composite"/>
</packagedElement>
<packagedElement xmi:type="uml:Class" xmi:id="_sys" name="System">
<ownedAttribute xmi:type="uml:Port" xmi:id="_sys_in" name="supply" type="_in_if" aggregation="composite"/>
<ownedAttribute xmi:type="uml:Property" xmi:id="_p" name="pump" type="_pump" aggregation="composite"/>
<ownedAttribute xmi:type="uml:Property" xmi:id="_e" name="engine" type="_engine" aggregation="composite"/>
<ownedConnector xmi:type="uml:Connector" xmi:id="_conn">
<end xmi:type="uml:ConnectorEnd" xmi:id="_e1" partWithPort="_e" role="_engine_in"/>
<end xmi:type="uml:ConnectorEnd" xmi:id="_e2" partWithPort="_p" role="_pump_out"/>
</ownedConnector>
<ownedConnector xmi:type="uml:Connector" xmi:id="_conn2">
<end xmi:type="uml:ConnectorEnd" xmi:id="_e3" partWithPort="_p" role="_pump_out"/>
<end xmi:type="uml:ConnectorEnd" xmi:id="_e4" role="_sys_in"/>
</ownedConnector>
</packagedElement>
<packagedElement xmi:type="uml:InformationFlow" xmi:id="_if" informationSource="_pump" informationTarget="_engine" conveyed="_fuel" realizingConnector="_conn"/>
<packagedElement xmi:type="uml:InformationFlow" xmi:id="_if2" informationSource="_p" informationTarget="_sys" conveyed="_fuel" realizingConnector="_conn2"/>
<packagedElement xmi:type="uml:InformationFlow" xmi:id="_if3" informationSource="_engine" informationTarget="_sys" conveyed="_fuel" realizingConnector="_conn2"/>`

const partFlowApplications = `
<sysml:Block xmi:id="_s1" base_Class="_sys"/>
<sysml:Block xmi:id="_s2" base_Class="_fuel"/>
<sysml:Block xmi:id="_s3" base_Class="_pump"/>
<sysml:Block xmi:id="_s4" base_Class="_engine"/>
<sysml:InterfaceBlock xmi:id="_s5" base_Class="_out_if"/>
<sysml:InterfaceBlock xmi:id="_s6" base_Class="_in_if"/>
<sysml:FlowProperty xmi:id="_s7" base_Property="_out_fuel" direction="out"/>
<sysml:FlowProperty xmi:id="_s8" base_Property="_in_fuel" direction="in"/>
<sysml:ProxyPort xmi:id="_s9" base_Port="_pump_out"/>
<sysml:ProxyPort xmi:id="_s10" base_Port="_engine_in"/>
<sysml:ProxyPort xmi:id="_s11" base_Port="_sys_in"/>
<sysml:ItemFlow xmi:id="_s12" base_InformationFlow="_if"/>
<sysml:ItemFlow xmi:id="_s13" base_InformationFlow="_if2"/>
<sysml:ItemFlow xmi:id="_s14" base_InformationFlow="_if3"/>`

// 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 = `
<packagedElement xmi:type="uml:Class" xmi:id="_fuel" name="Fuel"/>
<packagedElement xmi:type="uml:Class" xmi:id="_out_if" name="Outlet">
<ownedAttribute xmi:type="uml:Property" xmi:id="_out_fuel" name="fuel" type="_fuel"/>
</packagedElement>
<packagedElement xmi:type="uml:Class" xmi:id="_pump" name="Pump">
<ownedAttribute xmi:type="uml:Port" xmi:id="_pump_out" name="outlet" type="_out_if" aggregation="composite"/>
</packagedElement>
<packagedElement xmi:type="uml:Class" xmi:id="_sys" name="System">
<ownedAttribute xmi:type="uml:Property" xmi:id="_l" name="left" type="_pump" aggregation="composite"/>
<ownedAttribute xmi:type="uml:Property" xmi:id="_r" name="right" type="_pump" aggregation="composite"/>
<ownedConnector xmi:type="uml:Connector" xmi:id="_conn">
<end xmi:type="uml:ConnectorEnd" xmi:id="_e1" partWithPort="_l" role="_pump_out"/>
<end xmi:type="uml:ConnectorEnd" xmi:id="_e2" partWithPort="_r" role="_pump_out"/>
</ownedConnector>
<ownedConnector xmi:type="uml:Connector" xmi:id="_conn2">
<end xmi:type="uml:ConnectorEnd" xmi:id="_e3" partWithPort="_r" role="_pump_out"/>
<end xmi:type="uml:ConnectorEnd" xmi:id="_e4" partWithPort="_l" role="_pump_out"/>
</ownedConnector>
</packagedElement>
<packagedElement xmi:type="uml:InformationFlow" xmi:id="_if" informationSource="_pump" informationTarget="_out_if" conveyed="_fuel" realizingConnector="_conn"/>
<packagedElement xmi:type="uml:InformationFlow" xmi:id="_if2" informationSource="_l" informationTarget="_r" conveyed="_fuel" realizingConnector="_conn2"/>`

const twinFlowApplications = `
<sysml:Block xmi:id="_s1" base_Class="_sys"/>
<sysml:Block xmi:id="_s2" base_Class="_fuel"/>
<sysml:Block xmi:id="_s3" base_Class="_pump"/>
<sysml:InterfaceBlock xmi:id="_s5" base_Class="_out_if"/>
<sysml:FlowProperty xmi:id="_s7" base_Property="_out_fuel" direction="inout"/>
<sysml:ProxyPort xmi:id="_s9" base_Port="_pump_out"/>
<sysml:ItemFlow xmi:id="_s12" base_InformationFlow="_if"/>
<sysml:ItemFlow xmi:id="_s13" base_InformationFlow="_if2"/>`

// 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)
}
Loading