Skip to content
Merged
1 change: 1 addition & 0 deletions changes/unreleased/calc-pins-compartment.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The Mermaid, D2 and PlantUML action and mixed renderings list an action's or calculation's parameter pins that no edge reaches in a `parameters` compartment of the node (`in x`, `in y`, `out result`, typed when typings are shown), as the tree form lists them, instead of leaving them out and naming them in a `pin(s) not drawn` notice; the Jupyter kernel's `%viz A::add` Mermaid diagram therefore shows a calculation's parameters.
14 changes: 8 additions & 6 deletions docs/project/view-rendering-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -605,12 +605,14 @@ digraph "VehicleViews::vehicleView" {
frame's pins are set on the cluster's border as a positioned node's are (`writePins` on the
root cluster). The text form lists a node's pins under it by direction and name (`in bread`)
and names the pins an edge joins as its ends (`heat.t => pack.t`, `ToastBread.bread ==
heat.b`), labelling the edge by its own name alone as DOT does. Mermaid's flowchart draws the
pins an edge ends at (`n0_p0 ===|"bread = b"| n1_p0`) and names the rest in a `not
represented` notice, `N pin(s) not drawn (…); a flowchart draws the pins an edge ends at`.
PlantUML's state grammar, which an action rendering uses, has no pin: the edge keeps its label
naming the pins (`n0 -- n1 : bread = b`) and every pin is named in a `not represented` notice,
`N pin(s) not drawn (…); PlantUML's state grammar has no pin, so the edges name them`.
heat.b`), labelling the edge by its own name alone as DOT does. Mermaid's flowchart and D2 draw
the pins an edge ends at (`n0_p0 ===|"bread = b"| n1_p0`) and list the rest in a `parameters`
compartment of their node (`labeller.parameterLines`, the pins `Rendering.unconnectedPin`
names), one line per pin as the tree form writes it (`in x`, `out ^result`, typed under
`typings`). PlantUML's state grammar, which an action rendering uses, has no pin: the edge keeps
its label naming the pins (`n0 -- n1 : bread = b`) and every pin is listed in its node's
`parameters` compartment. The `N pin(s) not drawn` notice remains for a pin that is neither
drawn nor listed.
- **Interconnection ports.** A part's node carries as its `Ports` the ports it has from its
definition and what that specializes without declaring them itself (`featureWalk.pinPorts`,
`Renderer.typedPorts` in `interconnection.go`: `Model.MembersOf` less the part's own members
Expand Down
10 changes: 5 additions & 5 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,11 +853,11 @@ binds its own parameters to its nodes' pins as the frame does. A binding one end
a literal, an expression, an attribute — draws nothing. `dot`
sets the frame's pins on the cluster's border as squares, the way it sets a node's; `text` lists
each pin under its node (`in bread`) and names the pins an edge joins (`heat.t => pack.t`,
`ToastBread.bread == heat.b`); `mermaid`'s flowchart draws the pins an edge ends at, and names the
rest in a `%% not represented:` notice; `plantuml`'s state grammar has no pin, so it names the edge's
pins in the edge's label (`n0 -- n1 : bread = b`) and the pins in a `' not represented:` notice; `d2`
draws the pins an edge ends at as nodes nested in their owner, and names the rest in a
`# not represented:` notice.
`ToastBread.bread == heat.b`); `mermaid`'s flowchart draws the pins an edge ends at, and lists the
rest in a `parameters` compartment of their node (`parameters`, `in x`, `out result`); `plantuml`'s
state grammar has no pin, so it names the edge's pins in the edge's label (`n0 -- n1 : bread = b`)
and lists every pin in its node's `parameters` compartment; `d2` draws the pins an edge ends at as
nodes nested in their owner, and lists the rest in the `parameters` compartment.

A tree rendering draws the directed parameters of a calculation, action or constraint definition or
usage — `in`, `out`, `inout` and `return` — as parameter pins of their owner's node rather than as
Expand Down
20 changes: 20 additions & 0 deletions internal/frontend/jupyter/replengine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,23 @@ func TestARejectedCellDoesNotHoldBackTheNextOne(t *testing.T) {
}
answer("add(10 [m], 20 [mm])")
}

// %viz of a calculation shows its parameters in the Mermaid diagram the kernel
// bundles first: pins no edge reaches are listed in a parameters compartment.
func TestVizListsACalculationsParameters(t *testing.T) {
e := newEngine(t)
mustRun(t, e, "package A {\n private import ScalarValues::*;\n calc add {\n in x : Integer;\n in y : Integer;\n return : Integer;\n x + y\n }\n}\n")
out := mustRun(t, e, "%viz A::add")
if len(out.displays) != 1 {
t.Fatalf("%%viz displayed %d bundles, want 1", len(out.displays))
}
mermaid, _ := out.displays[0][mimeMermaid].(string)
for _, want := range []string{"flowchart", "parameters\nin x\nin y\nout result"} {
if !strings.Contains(mermaid, want) {
t.Errorf("%%viz A::add Mermaid lacks %q:\n%s", want, mermaid)
}
}
if strings.Contains(mermaid, "pin(s) not drawn") {
t.Errorf("%%viz A::add Mermaid notices pins it lists:\n%s", mermaid)
}
}
21 changes: 15 additions & 6 deletions internal/ir/view/action_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ func TestTextActionFramePins(t *testing.T) {
}
}

// The Mermaid form draws a frame pin an edge ends at as a node inside the
// frame's subgraph, as it draws the nodes' pins, and says which pins it leaves out.
// The Mermaid form draws a frame pin an edge ends at as a node inside the frame's
// subgraph, as it draws the nodes' pins, and lists the rest in a parameters compartment.
func TestMermaidActionFramePins(t *testing.T) {
rendering := renderActionPins(t, "Pins::ToastBread")
source := rendering.MermaidWith(fullNotation)
Expand All @@ -252,20 +252,29 @@ func TestMermaidActionFramePins(t *testing.T) {
}
}
loose := renderActionPins(t, "Pins::Loose").MermaidWith(fullNotation)
if want := "%% not represented: 3 pin(s) not drawn (heat.t, pack.t, pack.boxed); a flowchart draws the pins an edge ends at"; !strings.Contains(loose, want) {
t.Errorf("Mermaid lacks the notice %q:\n%s", want, loose)
for _, want := range []string{"**heat : Heat**\nparameters\nout ^t`", "**pack : Pack**\nparameters\nin t\nout ^boxed`"} {
if !strings.Contains(loose, want) {
t.Errorf("Mermaid does not list the unconnected pins %q:\n%s", want, loose)
}
}
if strings.Contains(loose, "pin(s) not drawn") {
t.Errorf("Mermaid notices pins it lists:\n%s", loose)
}
}

// PlantUML's state grammar has no pin: a binding is an undirected line between
// the frame and the node naming the pins, and the pins left undrawn are noticed.
// the frame and the node naming the pins, and every pin is listed in its node's
// `parameters` compartment.
func TestPlantUMLActionFramePins(t *testing.T) {
rendering := renderActionPins(t, "Pins::ToastBread")
source, _ := rendering.PlantUMLWith(fullNotation)
for _, want := range []string{"n0 -- n1 : bread = b\n", "n2 -- n0 : boxed = toast\n",
"' not represented: 6 pin(s) not drawn (Pins::ToastBread.bread, Pins::ToastBread.toast, heat.b, heat.t, pack.t, pack.boxed); PlantUML's state grammar has no pin, so the edges name them\n"} {
"**ToastBread**\\nparameters\\nin bread\\nout toast\" as n0", "**heat : Heat**\\nparameters\\nin b\\nout ^t\" as n1", "**pack : Pack**\\nparameters\\nin ^t\\nout ^boxed\" as n2"} {
if !strings.Contains(source, want) {
t.Errorf("PlantUML lacks %q:\n%s", want, source)
}
}
if strings.Contains(source, "pin(s) not drawn") {
t.Errorf("PlantUML notices pins it lists:\n%s", source)
}
}
23 changes: 23 additions & 0 deletions internal/ir/view/calc_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,26 @@ func TestMixedRenderingKeepsNestedCalcsUnderTheirOwner(t *testing.T) {
t.Errorf("default text does not name the nested calc under its owner:\n%s", plain)
}
}

// A rendering showing no compartments lists no parameters compartment either:
// the pins a form does not draw go back to the not-drawn notice.
func TestHiddenCompartmentsListNoParameters(t *testing.T) {
rendering := renderSource(t, "Bare::flow", `package Model {
private import ScalarValues::*;
calc add { in x : Integer; in y : Integer; return : Integer; x + y }
}
package Bare {
private import StandardViewDefinitions::*;
rendering bare :> Views::asInterconnectionDiagram {
attribute showCompartments : ScalarValues::Boolean default = false;
}
view flow : ActionFlowView { expose Model::add; render bare; }
}`)
mermaid := rendering.Mermaid()
if strings.Contains(mermaid, "parameters") {
t.Errorf("Mermaid lists a parameters compartment the rendering hides:\n%s", mermaid)
}
if !strings.Contains(mermaid, "pin(s) not drawn") || !strings.Contains(mermaid, "add.result") {
t.Errorf("Mermaid does not notice the pins it neither draws nor lists:\n%s", mermaid)
}
}
3 changes: 2 additions & 1 deletion internal/ir/view/d2.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (r *Rendering) D2With(options Options) (string, error) {
}
if r.Kind == KindAction {
w.used = r.usedPorts(w.ports)
w.labels.listedPin = r.listedPin(func(node *Node, port Port) bool { return w.used[node.ID][port.ID] })
}
for _, root := range r.Roots {
w.fills.collect(root)
Expand Down Expand Up @@ -119,7 +120,7 @@ func (r *Rendering) d2Notices(options Options, w *d2Writer) []string {
notices = append(notices, linesNotice(options.Lines, FormD2))
}
if r.Kind == KindAction {
if pins := r.undrawnPins(func(node *Node, port Port) bool { return w.used[node.ID][port.ID] }); len(pins) > 0 {
if pins := r.undrawnPins(func(node *Node, port Port) bool { return w.used[node.ID][port.ID] || w.labels.lists(node, port) }); len(pins) > 0 {
notices = append(notices, fmt.Sprintf("%d pin(s) not drawn (%s); D2 draws the pins an edge ends at",
len(pins), strings.Join(pins, ", ")))
}
Expand Down
9 changes: 3 additions & 6 deletions internal/ir/view/d2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,8 @@ func TestD2StateDiagram(t *testing.T) {
}
}

// An action graph takes the pseudostate glyphs: a fork and a join a bar, a
// decision a diamond named by the node, the final a double-bordered dot; the
// pins an edge ends at are drawn inside their action and joined by path, the
// rest noticed.
// An action graph takes the pseudostate glyphs (fork/join bar, decision diamond, final
// dot); pins an edge ends at are drawn inside their action, the rest listed as parameters.
func TestD2ActionGlyphsAndPins(t *testing.T) {
d2, err := render(t, "action.sysml", "FlowViews::driveView").D2With(fullNotation)
if err != nil {
Expand All @@ -257,8 +255,7 @@ func TestD2ActionGlyphsAndPins(t *testing.T) {
"\n bar: { shape: rectangle; width: 80; height: 8; style: { fill: black; stroke: black } }\n",
"\n choice: { shape: diamond; style: { fill: white; stroke: \"#181818\"; stroke-width: 1; font-color: black; font-size: 14 } }\n",
"# not represented: 2 fork/join name(s) (split, sync); D2's fork bar draws no label\n",
"# not represented: 1 pin(s) not drawn (provide.fuel); D2 draws the pins an edge ends at\n",
"\n n2: \"«action»\\nprovide : Provide\" {\n class: usage\n \"n2.1\": \"^torque\" { class: pin }\n }\n",
"\n n2: \"«action»\\nprovide : Provide\\nparameters\\nin ^fuel\" {\n class: usage\n \"n2.1\": \"^torque\" { class: pin }\n }\n",
"\n \"n3.0\": \"reading\" { class: pin }\n",
"\nn0.n2.\"n2.1\" -> n0.n3.\"n3.0\": \"torque to reading\" { class: edge }\n",
} {
Expand Down
28 changes: 19 additions & 9 deletions internal/ir/view/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type labeller struct {
// pinLines lists a node's parameter pins as label lines, for a form that
// draws no pin on a tree node.
pinLines bool
// listedPin reports a parameter pin the form lists in the node's `parameters`
// compartment instead of drawing it; nil lists none.
listedPin func(node *Node, port Port) bool
}

// labelsOf finds the namespace the named roots share — the longest run of
Expand Down Expand Up @@ -369,18 +372,25 @@ func (l labeller) titleLines(node *Node) []string {
return append(lines, l.compartmentLines(node)...)
}

// parameterLines are a node's parameter pins as the notation's `parameters`
// compartment, `in x : Integer` a line under the heading, where pinLines asks
// for them; nothing for a node with no pin.
// lists reports whether the node's pin is listed in its `parameters` compartment:
// every pin where pinLines asks, else one listedPin names under shown compartments.
func (l labeller) lists(node *Node, port Port) bool {
return l.pinLines || l.shown.Has(ElementCompartments) && l.listedPin != nil && l.listedPin(node, port)
}

// parameterLines are the node's listed parameter pins as the notation's
// `parameters` compartment, `in x : Integer` a line under the heading.
func (l labeller) parameterLines(node *Node) []string {
if !l.pinLines || len(node.Ports) == 0 {
return nil
}
lines := []string{"parameters"}
var lines []string
for _, port := range node.Ports {
lines = append(lines, parameterPinLine(port))
if l.lists(node, port) {
lines = append(lines, parameterPinLine(port))
}
}
return lines
if len(lines) == 0 {
return nil
}
return append([]string{"parameters"}, lines...)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
}

// cameoStateDetails splits a state's detail into Cameo's compartment lines, one
Expand Down
8 changes: 5 additions & 3 deletions internal/ir/view/mermaid.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func (r *Rendering) MermaidWith(options Options) string {
labels.pinLines = r.Kind == KindTree
labels.skin = skinOf(options.Style)
ports := r.portView(options.Ports)
used := r.usedPorts(ports)
labels.listedPin = r.listedPin(func(node *Node, port Port) bool { return used[node.ID][port.ID] })
r.writeMermaidFrontmatter(&b, labels, options, ports)
if r.Run {
fmt.Fprintf(&b, "%%%% run — %s rendering", r.Kind)
Expand All @@ -74,7 +76,7 @@ func (r *Rendering) MermaidWith(options Options) string {
fills.collect(root)
}
}
for _, notice := range r.mermaidNotices(options) {
for _, notice := range r.mermaidNotices(options, labels) {
fmt.Fprintf(&b, "%%%% not represented: %s\n", notice)
}
r.writeGeometryComments(&b, "%%")
Expand Down Expand Up @@ -103,7 +105,7 @@ func (r *Rendering) MermaidWith(options Options) string {
return b.String()
}

func (r *Rendering) mermaidNotices(options Options) []string {
func (r *Rendering) mermaidNotices(options Options, labels labeller) []string {
var notices []string
if err := options.Palette.check(); err != nil {
notices = append(notices, err.Error())
Expand All @@ -116,7 +118,7 @@ func (r *Rendering) mermaidNotices(options Options) []string {
if r.Kind == KindAction || r.Kind == KindMixed {
used := r.usedPorts(r.portView(options.Ports))
if pins := r.undrawnPins(func(node *Node, port Port) bool {
return r.Kind == KindMixed && port.Direction == PortUndirected || used[node.ID][port.ID]
return r.Kind == KindMixed && port.Direction == PortUndirected || used[node.ID][port.ID] || labels.lists(node, port)
}); len(pins) > 0 {
notices = append(notices, fmt.Sprintf("%d pin(s) not drawn (%s); a flowchart draws the pins an edge ends at",
len(pins), strings.Join(pins, ", ")))
Expand Down
3 changes: 2 additions & 1 deletion internal/ir/view/plantuml.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func (r *Rendering) PlantUMLWith(options Options) (string, error) {
w := &plantumlWriter{kind: r.Kind, borders: r.Kind.paletteBorders(), fills: familyFills{palette: options.Palette, tree: r.Kind == KindTree},
labels: labelsOf(r.Roots, false, nil, r.Shown), ports: r.portView(options.Ports), links: options.Links,
writtenPorts: make(map[string]struct{}), nesting: r.nestingEdges()}
w.labels.listedPin = r.listedPin(func(node *Node, port Port) bool { return r.Kind == KindMixed && drawsPort(w.ports.of(node), port) })
w.labels.pinLines = r.Kind == KindTree
for _, root := range r.Roots {
w.fills.collect(root)
Expand All @@ -76,7 +77,7 @@ func (r *Rendering) PlantUMLWith(options Options) (string, error) {
notices = append(notices, w.mixedContainmentNotices(r.Roots)...)
}
if r.Kind == KindAction {
if pins := r.undrawnPins(func(*Node, Port) bool { return false }); len(pins) > 0 {
if pins := r.undrawnPins(w.labels.lists); len(pins) > 0 {
notices = append(notices, fmt.Sprintf("%d pin(s) not drawn (%s); PlantUML's state grammar has no pin, so the edges name them",
len(pins), strings.Join(pins, ", ")))
}
Expand Down
2 changes: 1 addition & 1 deletion internal/ir/view/plantuml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func TestPlantUMLActionUsesStateGrammar(t *testing.T) {
" state \"<size:10>//«fork»//</size>\\n**split**\" as n7 <<fork>>\n",
" state \"**final**\" as n9 <<end>>\n",
" state \"<size:10>//«decision»//</size>\\n**check**\" as n11 <<choice>>\n",
" state \"<size:10>//«action»//</size>\\n**monitor**\\nown flow\" as n3 <<action>> <<usage>> {\n",
" state \"<size:10>//«action»//</size>\\n**monitor**\\nown flow\\nparameters\\nin reading\" as n3 <<action>> <<usage>> {\n",
"\nn2 --> n3 : torque to reading\n",
"\nn11 -[dashed]-> n9 : [speed <U+003E> 0]\n",
"\nn1 -[dashed]-> n7\n",
Expand Down
15 changes: 15 additions & 0 deletions internal/ir/view/ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package view
import (
"errors"
"fmt"
"slices"
"strings"
)

Expand Down Expand Up @@ -166,3 +167,17 @@ func (r *Rendering) undrawnPins(drawn func(node *Node, port Port) bool) []string
}
return names
}

// listedPin lists, in an action or mixed rendering, the parameter pins a form
// does not draw in their node's `parameters` compartment; drawn says which it draws.
func (r *Rendering) listedPin(drawn func(node *Node, port Port) bool) func(node *Node, port Port) bool {
if r.Kind != KindAction && r.Kind != KindMixed {
return nil
}
return func(node *Node, port Port) bool { return port.Parameter && !drawn(node, port) }
}

// drawsPort reports whether port is among the ports a form draws on a node.
func drawsPort(ports []Port, port Port) bool {
return slices.ContainsFunc(ports, func(drawn Port) bool { return drawn.ID == port.ID })
}
7 changes: 5 additions & 2 deletions internal/ir/view/ports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ func TestMixedActionPinsKeepTheirFormBehavior(t *testing.T) {
if open < 0 || closeAt < 0 || !strings.Contains(plantUMLMinimal[start+open+1:start+open+1+closeAt], "port \"request\" as "+request.ID) {
t.Errorf("minimal PlantUML does not write request inside Check's container:\n%s", plantUMLMinimal)
}
if strings.Contains(plantUMLMinimal, "parameters") {
t.Errorf("minimal PlantUML lists the action pin it writes as a port:\n%s", plantUMLMinimal)
}
if !strings.Contains(textMinimal, "in request") {
t.Errorf("minimal text does not use the action pin direction:\n%s", textMinimal)
}
Expand All @@ -341,8 +344,8 @@ func TestMixedActionPinsKeepTheirFormBehavior(t *testing.T) {
if strings.Contains(output, check.ID+"_p0[") || strings.Contains(output, `["request"]`) {
t.Errorf("%s Mermaid draws Check's unconnected action pin, unlike an action rendering:\n%s", label, output)
}
if !strings.Contains(output, "Check.request") || !strings.Contains(output, "pin(s) not drawn") {
t.Errorf("%s Mermaid does not report Check.request as an undrawn action pin:\n%s", label, output)
if !strings.Contains(output, "parameters\nin request") || strings.Contains(output, "pin(s) not drawn") {
t.Errorf("%s Mermaid does not list Check.request in a parameters compartment:\n%s", label, output)
}
}
if strings.Contains(mermaidMinimal, "unused") {
Expand Down
3 changes: 1 addition & 2 deletions internal/ir/view/testdata/action.d2.golden
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# FlowViews::driveView — action rendering (render asInterconnectionDiagram, view def ActionFlowView)
# not represented: 1 pin(s) not drawn (provide.fuel); D2 draws the pins an edge ends at
# not represented: 2 fork/join name(s) (split, sync); D2's fork bar draws no label
classes: {
definition: { style: { fill: white; stroke: "#181818"; stroke-width: 1; font-color: black; font-size: 14 } }
Expand All @@ -15,7 +14,7 @@ classes: {
n0: "«action def»\nDrive" {
class: definition
n1: "" { class: initial }
n2: "«action»\nprovide" {
n2: "«action»\nprovide\nparameters\nin ^fuel" {
class: usage
"n2.1": "^torque" { class: pin }
}
Expand Down
5 changes: 3 additions & 2 deletions internal/ir/view/testdata/action.mermaid.golden
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ config:
---
%% FlowViews::driveView — action rendering (render asInterconnectionDiagram, view def ActionFlowView)
%% not represented: 2 fork/join name(s) (split, sync); Mermaid's fork bar draws no label
%% not represented: 1 pin(s) not drawn (provide.fuel); a flowchart draws the pins an edge ends at
flowchart TD
subgraph n0 ["`*«action def»* **Drive**`"]
direction TD
n1@{ shape: f-circ, label: "" }
subgraph n2 ["`*«action»* **provide**`"]
subgraph n2 ["`*«action»* **provide**
parameters
in ^fuel`"]
direction TD
n2_anchor[" "]
n2_p1["^torque"]
Expand Down
Loading
Loading