Skip to content

Commit 6be89fe

Browse files
authored
Merge pull request #204 from engalar/fix/connection-index-int64
fix: microflow layout issues (ConnectionIndex, redundant Merge, DESCRIBE roundtrip)
2 parents 78982ad + 197b4e7 commit 6be89fe

14 files changed

Lines changed: 396 additions & 279 deletions

mdl-examples/doctype-tests/keyword-as-identifier.mdl

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,9 @@ CREATE ENUMERATION KeywordTest.MiscKeywords (
6262
Action,
6363
Source,
6464
Target,
65-
Owner,
66-
Type,
6765
Name,
6866
Value,
6967
Result,
70-
Object,
7168
Index,
7269
Input,
7370
Output,
@@ -94,7 +91,6 @@ CREATE ENTITY KeywordTest.Data (
9491

9592
CREATE ENTITY KeywordTest.Activity (
9693
Name : String(200),
97-
Type : String(200),
9894
Result : String(200)
9995
);
10096

@@ -111,7 +107,6 @@ BEGIN
111107

112108
$Activity = CREATE KeywordTest.Activity (
113109
Name = 'test',
114-
Type = 'task',
115110
Result = 'ok'
116111
);
117112
END;

mdl/executor/cmd_microflows_builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type flowBuilder struct {
2929
errors []string // Validation errors collected during build
3030
measurer *layoutMeasurer // For measuring statement dimensions
3131
nextConnectionPoint model.ID // For compound statements: the exit point differs from entry point
32+
nextFlowCase string // If set, next connecting flow uses this case value (for merge-less splits)
3233
reader *mpr.Reader // For looking up page/microflow references
3334
hierarchy *ContainerHierarchy // For resolving container IDs to module names
3435
pendingAnnotations *ast.ActivityAnnotations // Pending annotations to attach to next activity

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,19 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
6565
// Calculate merge position (after the longest branch)
6666
mergeX := splitX + SplitWidth + HorizontalSpacing/2 + branchWidth + HorizontalSpacing/2
6767

68-
// Only create merge if at least one branch does NOT end with RETURN
69-
var mergeID model.ID
68+
// Determine if the merge would have 2+ incoming edges (non-redundant).
69+
// Skip merge when only one branch flows into it (the other returns).
70+
needMerge := false
7071
if !bothReturn {
72+
if hasElseBody {
73+
needMerge = !thenReturns && !elseReturns // both branches continue → 2 inputs
74+
} else {
75+
needMerge = !thenReturns // THEN continues + FALSE path → 2 inputs
76+
}
77+
}
78+
79+
var mergeID model.ID
80+
if needMerge {
7181
merge := &microflows.ExclusiveMerge{
7282
BaseMicroflowObject: microflows.BaseMicroflowObject{
7383
BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())},
@@ -153,8 +163,12 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
153163
// IF WITHOUT ELSE: FALSE path horizontal (happy path), TRUE path below
154164
// This keeps the "do nothing" path straight and the "do something" path below
155165

156-
// FALSE path: connect split directly to merge horizontally
157-
fb.flows = append(fb.flows, newHorizontalFlowWithCase(splitID, mergeID, "false"))
166+
if needMerge {
167+
// FALSE path: connect split directly to merge horizontally
168+
fb.flows = append(fb.flows, newHorizontalFlowWithCase(splitID, mergeID, "false"))
169+
}
170+
// When !needMerge (thenReturns): FALSE flow is deferred — the parent will
171+
// connect splitID to the next activity with nextFlowCase="false".
158172

159173
// TRUE path: goes below the main line
160174
thenCenterY := centerY + VerticalSpacing
@@ -202,13 +216,34 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID {
202216
// Restore endsWithReturn - a single branch returning doesn't end the overall flow
203217
fb.endsWithReturn = savedEndsWithReturn
204218

205-
// Update position to after the merge, on the happy path center line
206-
fb.posX = mergeX + MergeSize + HorizontalSpacing/2
207-
fb.posY = centerY
208-
209-
// Set nextConnectionPoint so the next activity connects FROM the merge
210-
// while incoming connection goes TO the split (returned below)
211-
fb.nextConnectionPoint = mergeID
219+
if needMerge {
220+
// Update position to after the merge, on the happy path center line
221+
fb.posX = mergeX + MergeSize + HorizontalSpacing/2
222+
fb.posY = centerY
223+
fb.nextConnectionPoint = mergeID
224+
} else {
225+
// No merge: the split's continuing branch connects directly to the next activity.
226+
// Position after the split, past the downward branch's horizontal extent.
227+
afterSplit := splitX + SplitWidth + HorizontalSpacing
228+
afterBranch := thenStartX + thenBounds.Width + HorizontalSpacing/2
229+
if !hasElseBody {
230+
fb.posX = max(afterSplit, afterBranch)
231+
} else {
232+
fb.posX = max(afterSplit, afterBranch)
233+
}
234+
fb.posY = centerY
235+
fb.nextConnectionPoint = splitID
236+
// Tell parent to attach the case value on the next flow
237+
if hasElseBody {
238+
if thenReturns {
239+
fb.nextFlowCase = "false"
240+
} else {
241+
fb.nextFlowCase = "true"
242+
}
243+
} else {
244+
fb.nextFlowCase = "false" // IF without ELSE: false is the continuing path
245+
}
246+
}
212247

213248
return splitID
214249
}

mdl/executor/cmd_microflows_builder_graph.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a
5353
fb.posX += fb.spacing
5454

5555
// Process each statement
56+
// pendingCase holds the case value for the NEXT flow (set by merge-less splits)
57+
pendingCase := ""
5658
for _, stmt := range stmts {
5759
activityID := fb.addStatement(stmt)
5860
if activityID != "" {
@@ -62,11 +64,19 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a
6264
fb.pendingAnnotations = nil
6365
}
6466
// Connect to previous object with horizontal SequenceFlow
65-
fb.flows = append(fb.flows, newHorizontalFlow(lastID, activityID))
67+
if pendingCase != "" {
68+
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, activityID, pendingCase))
69+
pendingCase = ""
70+
} else {
71+
fb.flows = append(fb.flows, newHorizontalFlow(lastID, activityID))
72+
}
6673
// For compound statements (IF, LOOP), the exit point differs from entry point
6774
if fb.nextConnectionPoint != "" {
6875
lastID = fb.nextConnectionPoint
6976
fb.nextConnectionPoint = ""
77+
// Save nextFlowCase for the NEXT iteration's flow creation
78+
pendingCase = fb.nextFlowCase
79+
fb.nextFlowCase = ""
7080
} else {
7181
lastID = activityID
7282
}
@@ -98,7 +108,11 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a
98108
fb.objects = append(fb.objects, endEvent)
99109

100110
// Connect last activity to end event
101-
fb.flows = append(fb.flows, newHorizontalFlow(lastID, endEvent.ID))
111+
if pendingCase != "" {
112+
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, endEvent.ID, pendingCase))
113+
} else {
114+
fb.flows = append(fb.flows, newHorizontalFlow(lastID, endEvent.ID))
115+
}
102116
}
103117

104118
return &microflows.MicroflowObjectCollection{

mdl/executor/cmd_microflows_format_action.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ func (e *Executor) formatActivity(
2424
case *microflows.EndEvent:
2525
if activity.ReturnValue != "" {
2626
returnVal := strings.TrimSuffix(activity.ReturnValue, "\n")
27-
if !strings.HasPrefix(returnVal, "$") && !isMendixKeyword(returnVal) && !isQualifiedEnumLiteral(returnVal) {
27+
// Only add $ prefix for bare identifiers (no operators, quotes, or parens)
28+
if !strings.HasPrefix(returnVal, "$") && !isMendixKeyword(returnVal) && !isQualifiedEnumLiteral(returnVal) &&
29+
!strings.ContainsAny(returnVal, "+'\"()") {
2830
returnVal = "$" + returnVal
2931
}
3032
return fmt.Sprintf("RETURN %s;", returnVal)
@@ -97,7 +99,7 @@ func (e *Executor) formatAction(
9799
if a.DataType != nil {
98100
varType = e.formatMicroflowDataType(a.DataType, entityNames)
99101
}
100-
initialValue := a.InitialValue
102+
initialValue := strings.TrimSuffix(a.InitialValue, "\n")
101103
if initialValue == "" {
102104
initialValue = "empty"
103105
}

mdl/executor/cmd_microflows_show_helpers.go

Lines changed: 100 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -211,28 +211,63 @@ func (e *Executor) traverseFlow(
211211

212212
trueFlow, falseFlow := findBranchFlows(flows)
213213

214+
// Guard pattern: true branch is a single EndEvent (RETURN),
215+
// but only when the false branch does NOT also end directly.
216+
// If both branches return, use normal IF/ELSE/END IF.
217+
isGuard := false
214218
if trueFlow != nil {
215-
e.traverseFlowUntilMerge(trueFlow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
219+
if _, isEnd := activityMap[trueFlow.DestinationID].(*microflows.EndEvent); isEnd {
220+
isGuard = true
221+
// Not a guard if both branches return directly
222+
if falseFlow != nil {
223+
if _, falseIsEnd := activityMap[falseFlow.DestinationID].(*microflows.EndEvent); falseIsEnd {
224+
isGuard = false
225+
}
226+
}
227+
}
216228
}
217229

218-
if falseFlow != nil {
219-
*lines = append(*lines, indentStr+"ELSE")
220-
visitedFalseBranch := make(map[model.ID]bool)
221-
for id := range visited {
222-
visitedFalseBranch[id] = true
230+
if isGuard {
231+
e.traverseFlowUntilMerge(trueFlow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
232+
*lines = append(*lines, indentStr+"END IF;")
233+
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
234+
235+
// Continue from the false branch (skip through merge if present)
236+
if falseFlow != nil {
237+
contID := falseFlow.DestinationID
238+
if _, isMerge := activityMap[contID].(*microflows.ExclusiveMerge); isMerge {
239+
visited[contID] = true
240+
for _, flow := range flowsByOrigin[contID] {
241+
contID = flow.DestinationID
242+
break
243+
}
244+
}
245+
e.traverseFlow(contID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
246+
}
247+
} else {
248+
if trueFlow != nil {
249+
e.traverseFlowUntilMerge(trueFlow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
223250
}
224-
e.traverseFlowUntilMerge(falseFlow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visitedFalseBranch, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
225-
}
226251

227-
*lines = append(*lines, indentStr+"END IF;")
228-
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
252+
if falseFlow != nil {
253+
*lines = append(*lines, indentStr+"ELSE")
254+
visitedFalseBranch := make(map[model.ID]bool)
255+
for id := range visited {
256+
visitedFalseBranch[id] = true
257+
}
258+
e.traverseFlowUntilMerge(falseFlow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visitedFalseBranch, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
259+
}
260+
261+
*lines = append(*lines, indentStr+"END IF;")
262+
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
229263

230-
// Continue after the merge point
231-
if mergeID != "" {
232-
visited[mergeID] = true
233-
nextFlows := flowsByOrigin[mergeID]
234-
for _, flow := range nextFlows {
235-
e.traverseFlow(flow.DestinationID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
264+
// Continue after the merge point
265+
if mergeID != "" {
266+
visited[mergeID] = true
267+
nextFlows := flowsByOrigin[mergeID]
268+
for _, flow := range nextFlows {
269+
e.traverseFlow(flow.DestinationID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
270+
}
236271
}
237272
}
238273
return
@@ -324,28 +359,61 @@ func (e *Executor) traverseFlowUntilMerge(
324359

325360
trueFlow, falseFlow := findBranchFlows(flows)
326361

362+
// Guard pattern: true branch is a single EndEvent (RETURN),
363+
// but only when the false branch does NOT also end directly.
364+
isGuard := false
327365
if trueFlow != nil {
328-
e.traverseFlowUntilMerge(trueFlow.DestinationID, nestedMergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
366+
if _, isEnd := activityMap[trueFlow.DestinationID].(*microflows.EndEvent); isEnd {
367+
isGuard = true
368+
if falseFlow != nil {
369+
if _, falseIsEnd := activityMap[falseFlow.DestinationID].(*microflows.EndEvent); falseIsEnd {
370+
isGuard = false
371+
}
372+
}
373+
}
329374
}
330375

331-
if falseFlow != nil {
332-
*lines = append(*lines, indentStr+"ELSE")
333-
visitedFalseBranch := make(map[model.ID]bool)
334-
for id := range visited {
335-
visitedFalseBranch[id] = true
376+
if isGuard {
377+
e.traverseFlowUntilMerge(trueFlow.DestinationID, nestedMergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
378+
*lines = append(*lines, indentStr+"END IF;")
379+
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
380+
381+
// Continue from the false branch (skip through merge if present)
382+
if falseFlow != nil {
383+
contID := falseFlow.DestinationID
384+
if _, isMerge := activityMap[contID].(*microflows.ExclusiveMerge); isMerge {
385+
visited[contID] = true
386+
for _, flow := range flowsByOrigin[contID] {
387+
contID = flow.DestinationID
388+
break
389+
}
390+
}
391+
e.traverseFlowUntilMerge(contID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
392+
}
393+
} else {
394+
if trueFlow != nil {
395+
e.traverseFlowUntilMerge(trueFlow.DestinationID, nestedMergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
336396
}
337-
e.traverseFlowUntilMerge(falseFlow.DestinationID, nestedMergeID, activityMap, flowsByOrigin, splitMergeMap, visitedFalseBranch, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
338-
}
339397

340-
*lines = append(*lines, indentStr+"END IF;")
341-
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
398+
if falseFlow != nil {
399+
*lines = append(*lines, indentStr+"ELSE")
400+
visitedFalseBranch := make(map[model.ID]bool)
401+
for id := range visited {
402+
visitedFalseBranch[id] = true
403+
}
404+
e.traverseFlowUntilMerge(falseFlow.DestinationID, nestedMergeID, activityMap, flowsByOrigin, splitMergeMap, visitedFalseBranch, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget)
405+
}
406+
407+
*lines = append(*lines, indentStr+"END IF;")
408+
recordSourceMap(sourceMap, currentID, startLine, len(*lines)+headerLineCount-1)
342409

343-
// Continue after nested merge
344-
if nestedMergeID != "" && nestedMergeID != mergeID {
345-
visited[nestedMergeID] = true
346-
nextFlows := flowsByOrigin[nestedMergeID]
347-
for _, flow := range nextFlows {
348-
e.traverseFlowUntilMerge(flow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
410+
// Continue after nested merge
411+
if nestedMergeID != "" && nestedMergeID != mergeID {
412+
visited[nestedMergeID] = true
413+
nextFlows := flowsByOrigin[nestedMergeID]
414+
for _, flow := range nextFlows {
415+
e.traverseFlowUntilMerge(flow.DestinationID, mergeID, activityMap, flowsByOrigin, splitMergeMap, visited, entityNames, microflowNames, lines, indent, sourceMap, headerLineCount, annotationsByTarget)
416+
}
349417
}
350418
}
351419
return

mdl/grammar/MDLParser.g4

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2867,7 +2867,8 @@ alterSettingsClause
28672867
;
28682868

28692869
settingsSection
2870-
: IDENTIFIER // MODEL, LANGUAGE
2870+
: IDENTIFIER // LANGUAGE, etc.
2871+
| MODEL
28712872
| WORKFLOWS
28722873
;
28732874

mdl/grammar/parser/MDLParser.interp

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

mdl/grammar/parser/mdl_lexer.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)