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/view-trigger-end-names.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Transition triggers are labelled by the name their signal or operation ends in.** A rendered state or action view wrote an `accept` trigger as its source text, so a migrated transition accepting `TMT::'02 JPL'::…::Control::'Post-Segment Exchange Alignment'` carried that whole path across the drawing. The DOT, Mermaid, PlantUML and text forms now head the trigger by its end name — `accept 'Post-Segment Exchange Alignment'`, `accept msg : Halt`, `accept setSpeed(value)` — the way a node's type is headed; time and change events keep their written text.
7 changes: 7 additions & 0 deletions docs/project/view-rendering-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ the source had none (`MigrationMetadata::SynthesizedName`) never becomes a label
text of its own and such a name is drawn unlabelled, as its source drew it. The rule is one place,
so the text, Mermaid, DOT and PlantUML forms label an edge alike.

A trigger names its signal or operation the way a node's type is named, by the name the reference
ends in (`triggerLabel` in `behavior.go`): `accept Signals::'APS Internal'::'Go Now'` reads
`accept 'Go Now'`, a named payload keeps its name (`accept msg : Halt`) and a call trigger its
arguments (`accept setSpeed(value)`, `accept halt()` — the parentheses tell a call from a signal), so a transition a v1 migration wrote with the signal's whole
path does not carry that path across the drawing. A time or change event, and an accept of an
event feature (`accept :> shutDown`), keep their written text.

## Why DOT next to Mermaid

Mermaid was chosen first because it draws where models are read — Markdown, documentation sites,
Expand Down
47 changes: 39 additions & 8 deletions internal/ir/view/behavior.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,23 @@ func behaviorNames(behaviors []lower.StateBehavior) string {
return strings.Join(names, ", ")
}

// triggerLabel is the event a transition waits for, as written when the source
// is at hand, else what kind of event it is.
// triggerLabel is the event a transition waits for: an accepted signal or called
// operation by the name it ends in, as a type is; a time or change event as
// written when the source is at hand; else what kind of event it is.
func (r *Renderer) triggerLabel(doc string, trigger ast.Node) string {
if trigger == nil {
return ""
}
switch event := trigger.(type) {
case *ast.AcceptEvent:
if event.SignalType != nil {
return joinNonEmpty(joinNonEmpty("accept", payloadHead(event.Payload)), endName(event.SignalType))
}
case *ast.CallEvent:
if event.Operation != nil {
return "accept " + endName(event.Operation) + callParameters(event.Parameters)
}
}
if text := r.nodeText(doc, trigger); text != "" {
return text
}
Expand All @@ -325,19 +336,39 @@ func (r *Renderer) triggerLabel(doc string, trigger ast.Node) string {
if event.Subsets != nil {
return "accept :> " + notationName(qualifiedText(event.Subsets))
}
if event.SignalType != nil {
return "accept " + notationName(qualifiedText(event.SignalType))
}
return "accept event"
case *ast.CallEvent:
if event.Operation != nil {
return "accept " + notationName(qualifiedText(event.Operation))
}
return "call event"
}
return "event"
}

// payloadHead is the payload parameter an accept declares, `msg :` for
// `accept msg : Warning`, and "" when the accept names none.
func payloadHead(payload *ast.Usage) string {
if payload == nil || payload.Ident.Name == "" {
return ""
}
return nameText(payload.Ident.Name) + " :"
}

// endName is the last segment of a qualified reference, quoted as the notation does.
func endName(name *ast.QualifiedName) string {
if name == nil || len(name.Parts) == 0 {
return ""
}
return nameText(name.Parts[len(name.Parts)-1].Text)
}

// callParameters writes a call trigger's argument list, `(speed)`, `()` for none.
func callParameters(parameters []ast.NameSegment) string {
names := make([]string, len(parameters))
for i, parameter := range parameters {
names[i] = nameText(parameter.Text)
}
return "(" + strings.Join(names, ", ") + ")"
}

// nodeText is the notation a node was written in, collapsed to one line, and ""
// when the rendering holds no source for it.
func (r *Renderer) nodeText(doc string, node ast.Node) string {
Expand Down
49 changes: 49 additions & 0 deletions internal/ir/view/edge_label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,55 @@ func TestBindingIsAnInterconnectionEdge(t *testing.T) {
}
}

// A trigger names its signal or operation by the name it ends in, as a type is
// headed, however far the source qualifies it; its payload name and call
// arguments are kept, and time and change events keep their written text.
func TestTriggerLabelsHeadTheirSignalByItsEndName(t *testing.T) {
model := `package Triggers {
package Signals { package 'APS Internal' { attribute def 'Go Now'; attribute def Halt; attribute def 'Go::Now'; } }
action def setSpeed { in value : ScalarValues::Real; }
action def halt;
state def Machine {
entry; then idle;
state idle;
state moving;
state stopped;
state parked;
transition first idle accept Triggers::Signals::'APS Internal'::'Go Now' then moving;
transition first moving accept msg : Signals::'APS Internal'::Halt then stopped;
transition first stopped accept Triggers::setSpeed(value) then moving;
transition first moving accept after 5 then idle;
transition first idle accept Signals::'APS Internal'::'Go::Now' then parked;
transition first parked accept Triggers::halt() then stopped;
}
view machineView : StandardViewDefinitions::StateTransitionView { expose Machine; }
}
`
r, idx := loadSources(t, []string{"triggers.sysml"}, [][]byte{[]byte(model)})
rendering, err := r.Render(lookup(t, idx, "Triggers::machineView"))
if err != nil {
t.Fatalf("render: %v", err)
}
text := rendering.Text()
for _, want := range []string{
"idle -> moving: accept 'Go Now'",
"moving -> stopped: accept msg : Halt",
"stopped -> moving: accept setSpeed(value)",
"moving -> idle: after 5",
"idle -> parked: accept 'Go::Now'",
"parked -> stopped: accept halt()",
} {
if !strings.Contains(text, want) {
t.Errorf("rendering lacks %q:\n%s", want, text)
}
}
for _, edge := range rendering.Edges {
if strings.Contains(edge.Label, "Triggers::") || strings.Contains(edge.Label, "Signals::") {
t.Errorf("a trigger label keeps its qualification: %q", edge.Label)
}
}
}

// A name labels an edge only when the edge has no text of its own: a trigger, guard,
// pin or payload takes the label and the name is left out, given or synthesized.
func TestEdgeLabelsYieldToTheEdgesOwnText(t *testing.T) {
Expand Down
Loading