Skip to content

Commit 353dde1

Browse files
committed
fix: preserve anchors from no-merge branch tails
Symptom: a nested IF whose continuing branch skips a local merge could lose its @anchor metadata when the parent IF connected that tail to its merge or next statement. In affected models this changed branch layout and could make Studio Pro report variables as out of scope after round-trip. Root cause: addIfStatement only consumed pending branch metadata when nextFlowCase was non-empty. No-merge continuations can carry only nextFlowAnchor, so those anchors were ignored. Fix: treat pending anchors as pending flow metadata even without a case value, and route their origin/destination sides through shared helper functions. Tests: added a synthetic nested no-merge IF regression that asserts the branch tail preserves a bottom-to-top anchor when it connects to the parent merge, and ran make build plus make test.
1 parent 2d8bd68 commit 353dde1

3 files changed

Lines changed: 112 additions & 35 deletions

File tree

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,20 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
134134
fb.flows = append(fb.flows, flow)
135135
} else {
136136
var flow *microflows.SequenceFlow
137-
if pendingThenCase != "" {
137+
originAnchor := prevThenAnchor
138+
destAnchor := thisAnchor
139+
if pendingThenCase != "" || pendingThenAnchor != nil {
140+
originAnchor, destAnchor = pendingFlowAnchors(prevThenAnchor, pendingThenAnchor, thisAnchor)
138141
flow = newHorizontalFlowWithCase(lastThenID, actID, pendingThenCase)
139-
if pendingThenAnchor != nil {
140-
prevThenAnchor = pendingThenAnchor
142+
if pendingThenCase == "" {
143+
flow = newHorizontalFlow(lastThenID, actID)
141144
}
142145
pendingThenCase = ""
143146
pendingThenAnchor = nil
144147
} else {
145148
flow = newHorizontalFlow(lastThenID, actID)
146149
}
147-
applyUserAnchors(flow, prevThenAnchor, thisAnchor)
150+
applyUserAnchors(flow, originAnchor, destAnchor)
148151
fb.flows = append(fb.flows, flow)
149152
}
150153
prevThenAnchor = thisAnchor
@@ -168,15 +171,18 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
168171
if !thenReturns && needMerge {
169172
if lastThenID != "" {
170173
var flow *microflows.SequenceFlow
171-
if pendingThenCase != "" {
174+
originAnchor := prevThenAnchor
175+
destAnchor := (*ast.FlowAnchors)(nil)
176+
if pendingThenCase != "" || pendingThenAnchor != nil {
177+
originAnchor, destAnchor = pendingFlowAnchors(prevThenAnchor, pendingThenAnchor, nil)
172178
flow = newHorizontalFlowWithCase(lastThenID, mergeID, pendingThenCase)
173-
if pendingThenAnchor != nil {
174-
prevThenAnchor = pendingThenAnchor
179+
if pendingThenCase == "" {
180+
flow = newHorizontalFlow(lastThenID, mergeID)
175181
}
176182
} else {
177183
flow = newHorizontalFlow(lastThenID, mergeID)
178184
}
179-
applyUserAnchors(flow, prevThenAnchor, nil)
185+
applyUserAnchors(flow, originAnchor, destAnchor)
180186
fb.flows = append(fb.flows, flow)
181187
} else {
182188
// Empty THEN body - connect split directly to merge with true case.
@@ -210,17 +216,20 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
210216
fb.flows = append(fb.flows, flow)
211217
} else {
212218
var flow *microflows.SequenceFlow
213-
if pendingElseCase != "" {
219+
originAnchor := prevElseAnchor
220+
destAnchor := thisAnchor
221+
if pendingElseCase != "" || pendingElseAnchor != nil {
222+
originAnchor, destAnchor = pendingFlowAnchors(prevElseAnchor, pendingElseAnchor, thisAnchor)
214223
flow = newHorizontalFlowWithCase(lastElseID, actID, pendingElseCase)
215-
if pendingElseAnchor != nil {
216-
prevElseAnchor = pendingElseAnchor
224+
if pendingElseCase == "" {
225+
flow = newHorizontalFlow(lastElseID, actID)
217226
}
218227
pendingElseCase = ""
219228
pendingElseAnchor = nil
220229
} else {
221230
flow = newHorizontalFlow(lastElseID, actID)
222231
}
223-
applyUserAnchors(flow, prevElseAnchor, thisAnchor)
232+
applyUserAnchors(flow, originAnchor, destAnchor)
224233
fb.flows = append(fb.flows, flow)
225234
}
226235
prevElseAnchor = thisAnchor
@@ -244,16 +253,18 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
244253
if !elseReturns && needMerge {
245254
if lastElseID != "" {
246255
flow := newUpwardFlow(lastElseID, mergeID)
247-
if pendingElseCase != "" {
248-
flow.CaseValue = microflows.EnumerationCase{
249-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
250-
Value: pendingElseCase,
251-
}
252-
if pendingElseAnchor != nil {
253-
prevElseAnchor = pendingElseAnchor
256+
originAnchor := prevElseAnchor
257+
destAnchor := (*ast.FlowAnchors)(nil)
258+
if pendingElseCase != "" || pendingElseAnchor != nil {
259+
originAnchor, destAnchor = pendingFlowAnchors(prevElseAnchor, pendingElseAnchor, nil)
260+
if pendingElseCase != "" {
261+
flow.CaseValue = microflows.EnumerationCase{
262+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
263+
Value: pendingElseCase,
264+
}
254265
}
255266
}
256-
applyUserAnchors(flow, prevElseAnchor, nil)
267+
applyUserAnchors(flow, originAnchor, destAnchor)
257268
fb.flows = append(fb.flows, flow)
258269
}
259270
}
@@ -324,17 +335,20 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
324335
fb.flows = append(fb.flows, flow)
325336
} else {
326337
var flow *microflows.SequenceFlow
327-
if pendingThenCase != "" {
338+
originAnchor := prevThenAnchor
339+
destAnchor := thisAnchor
340+
if pendingThenCase != "" || pendingThenAnchor != nil {
341+
originAnchor, destAnchor = pendingFlowAnchors(prevThenAnchor, pendingThenAnchor, thisAnchor)
328342
flow = newHorizontalFlowWithCase(lastThenID, actID, pendingThenCase)
329-
if pendingThenAnchor != nil {
330-
prevThenAnchor = pendingThenAnchor
343+
if pendingThenCase == "" {
344+
flow = newHorizontalFlow(lastThenID, actID)
331345
}
332346
pendingThenCase = ""
333347
pendingThenAnchor = nil
334348
} else {
335349
flow = newHorizontalFlow(lastThenID, actID)
336350
}
337-
applyUserAnchors(flow, prevThenAnchor, thisAnchor)
351+
applyUserAnchors(flow, originAnchor, destAnchor)
338352
fb.flows = append(fb.flows, flow)
339353
}
340354
prevThenAnchor = thisAnchor
@@ -358,16 +372,18 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
358372
if !thenReturns && needMerge {
359373
if lastThenID != "" {
360374
flow := newUpwardFlow(lastThenID, mergeID)
361-
if pendingThenCase != "" {
362-
flow.CaseValue = microflows.EnumerationCase{
363-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
364-
Value: pendingThenCase,
365-
}
366-
if pendingThenAnchor != nil {
367-
prevThenAnchor = pendingThenAnchor
375+
originAnchor := prevThenAnchor
376+
destAnchor := (*ast.FlowAnchors)(nil)
377+
if pendingThenCase != "" || pendingThenAnchor != nil {
378+
originAnchor, destAnchor = pendingFlowAnchors(prevThenAnchor, pendingThenAnchor, nil)
379+
if pendingThenCase != "" {
380+
flow.CaseValue = microflows.EnumerationCase{
381+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
382+
Value: pendingThenCase,
383+
}
368384
}
369385
}
370-
applyUserAnchors(flow, prevThenAnchor, nil)
386+
applyUserAnchors(flow, originAnchor, destAnchor)
371387
fb.flows = append(fb.flows, flow)
372388
} else {
373389
// Empty THEN body - connect split directly to merge going down and back up.

mdl/executor/cmd_microflows_builder_flows.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,10 @@ func applyUserAnchors(flow *microflows.SequenceFlow, origin *ast.FlowAnchors, de
192192
}
193193

194194
func branchDestinationAnchor(branchAnchor, stmtAnchor *ast.FlowAnchors) *ast.FlowAnchors {
195-
if stmtAnchor != nil && stmtAnchor.To != ast.AnchorSideUnset {
196-
return stmtAnchor
195+
if branchAnchor != nil && branchAnchor.To != ast.AnchorSideUnset {
196+
return branchAnchor
197197
}
198-
return branchAnchor
198+
return stmtAnchor
199199
}
200200

201201
func pendingFlowAnchors(previousAnchor, pendingAnchor, stmtAnchor *ast.FlowAnchors) (*ast.FlowAnchors, *ast.FlowAnchors) {

mdl/executor/cmd_microflows_builder_no_merge_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,58 @@ func TestBuildFlowGraph_NoMergeIfElseContinuesFromBranchTail(t *testing.T) {
4646
}
4747
}
4848

49+
func TestBuildFlowGraph_NestedNoMergeTailCarriesAnchorToParentMerge(t *testing.T) {
50+
anchoredTail := &ast.LogStmt{
51+
Level: ast.LogInfo,
52+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "nested else tail"},
53+
Annotations: &ast.ActivityAnnotations{
54+
Anchor: &ast.FlowAnchors{From: ast.AnchorSideBottom, To: ast.AnchorSideTop},
55+
},
56+
}
57+
body := []ast.MicroflowStatement{
58+
&ast.IfStmt{
59+
Condition: &ast.VariableExpr{Name: "Outer"},
60+
ThenBody: []ast.MicroflowStatement{
61+
&ast.IfStmt{
62+
Condition: &ast.VariableExpr{Name: "Inner"},
63+
ThenBody: []ast.MicroflowStatement{
64+
&ast.ReturnStmt{Value: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true}},
65+
},
66+
ElseBody: []ast.MicroflowStatement{anchoredTail},
67+
},
68+
},
69+
ElseBody: []ast.MicroflowStatement{
70+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "outer else"}},
71+
},
72+
},
73+
&ast.LogStmt{Level: ast.LogInfo, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "after outer"}},
74+
}
75+
76+
fb := &flowBuilder{
77+
posX: 100,
78+
posY: 100,
79+
spacing: HorizontalSpacing,
80+
declaredVars: map[string]string{"Outer": "Boolean", "Inner": "Boolean"},
81+
measurer: &layoutMeasurer{},
82+
}
83+
oc := fb.buildFlowGraph(body, &ast.MicroflowReturnType{Type: ast.DataType{Kind: ast.TypeBoolean}})
84+
85+
tailID := findLogActivityIDByMessage(t, oc, "nested else tail")
86+
for _, flow := range oc.Flows {
87+
if flow.OriginID != tailID {
88+
continue
89+
}
90+
if _, ok := objectByID(oc, flow.DestinationID).(*microflows.ExclusiveMerge); !ok {
91+
continue
92+
}
93+
if flow.OriginConnectionIndex != AnchorBottom || flow.DestinationConnectionIndex != AnchorTop {
94+
t.Fatalf("nested no-merge tail anchor = from %d to %d, want bottom/top", flow.OriginConnectionIndex, flow.DestinationConnectionIndex)
95+
}
96+
return
97+
}
98+
t.Fatal("expected nested no-merge tail to connect to the parent merge")
99+
}
100+
49101
func findLogActivityIDByMessage(t *testing.T, oc *microflows.MicroflowObjectCollection, message string) model.ID {
50102
t.Helper()
51103
for _, obj := range oc.Objects {
@@ -65,6 +117,15 @@ func findLogActivityIDByMessage(t *testing.T, oc *microflows.MicroflowObjectColl
65117
return ""
66118
}
67119

120+
func objectByID(oc *microflows.MicroflowObjectCollection, id model.ID) microflows.MicroflowObject {
121+
for _, obj := range oc.Objects {
122+
if obj.GetID() == id {
123+
return obj
124+
}
125+
}
126+
return nil
127+
}
128+
68129
func hasSequenceFlow(flows []*microflows.SequenceFlow, originID, destinationID model.ID) bool {
69130
for _, flow := range flows {
70131
if flow.OriginID == originID && flow.DestinationID == destinationID {

0 commit comments

Comments
 (0)