From 864280c5cb4e38bf9d0c9989d215f07171a3863e Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Thu, 23 Apr 2026 22:53:12 +0200 Subject: [PATCH 1/2] fix: drop empty else branch from describer output When an ExclusiveSplit's FALSE flow jumps straight to the merge point, as the builder emits for `if X then ... end if` without an else body, the describer was still printing `else` followed by nothing. That produced a spurious empty branch in MDL output. Re-parsing normalized the branch away again, but describe/exec/describe no longer stayed byte-identical. Skip the `else` keyword when the FALSE flow's destination is the matching merge point. Explicit false-branch bodies are still emitted normally. Tests cover traversal for an IF without an ELSE body. --- mdl/executor/cmd_microflows_show_helpers.go | 7 ++- mdl/executor/cmd_microflows_traverse_test.go | 56 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/mdl/executor/cmd_microflows_show_helpers.go b/mdl/executor/cmd_microflows_show_helpers.go index 6c646abb..589bd803 100644 --- a/mdl/executor/cmd_microflows_show_helpers.go +++ b/mdl/executor/cmd_microflows_show_helpers.go @@ -531,7 +531,12 @@ func traverseFlow( traverseFlowUntilMerge(ctx, trueFlow.DestinationID, mergeID, activityMap, flowsByOrigin, flowsByDest, splitMergeMap, visited, entityNames, microflowNames, lines, indent+1, sourceMap, headerLineCount, annotationsByTarget) } - if falseFlow != nil { + // Emit the ELSE branch only if it has statements. When the false + // flow jumps straight to the merge (the MDL was `if X then ... end if` + // with no else), emitting `else` with no body produces an empty + // branch that normalizes away on re-parse. + falseHasBody := falseFlow != nil && falseFlow.DestinationID != mergeID + if falseHasBody { *lines = append(*lines, indentStr+"else") visitedFalseBranch := make(map[model.ID]bool) for id := range visited { diff --git a/mdl/executor/cmd_microflows_traverse_test.go b/mdl/executor/cmd_microflows_traverse_test.go index f924ddd1..ab3e3dc0 100644 --- a/mdl/executor/cmd_microflows_traverse_test.go +++ b/mdl/executor/cmd_microflows_traverse_test.go @@ -124,6 +124,62 @@ func TestTraverseFlow_IfElse(t *testing.T) { } } +// TestTraverseFlow_IfWithoutElse verifies that when the FALSE branch jumps +// straight to the merge point (as emitted by the builder for `if X then ... end if`), +// the describer does not print an empty `else` — the previous behavior produced +// `if X then ... else end if;` which re-parses as an IF with an empty else body +// that is indistinguishable from the original. +func TestTraverseFlow_IfWithoutElse(t *testing.T) { + e := newTestExecutor() + + activityMap := map[model.ID]microflows.MicroflowObject{ + mkID("start"): µflows.StartEvent{BaseMicroflowObject: mkObj("start")}, + mkID("split"): µflows.ExclusiveSplit{ + BaseMicroflowObject: mkObj("split"), + SplitCondition: µflows.ExpressionSplitCondition{Expression: "$x > 0"}, + }, + mkID("true_act"): µflows.ActionActivity{ + BaseActivity: microflows.BaseActivity{BaseMicroflowObject: mkObj("true_act")}, + Action: µflows.LogMessageAction{LogLevel: "Info", LogNodeName: "'App'", MessageTemplate: &model.Text{Translations: map[string]string{"en_US": "positive"}}}, + }, + mkID("merge"): µflows.ExclusiveMerge{BaseMicroflowObject: mkObj("merge")}, + mkID("end"): µflows.EndEvent{BaseMicroflowObject: mkObj("end")}, + } + + flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ + mkID("start"): {mkFlow("start", "split")}, + mkID("split"): { + mkBranchFlow("split", "true_act", µflows.ExpressionCase{Expression: "true"}), + mkBranchFlow("split", "merge", µflows.ExpressionCase{Expression: "false"}), + }, + mkID("true_act"): {mkFlow("true_act", "merge")}, + mkID("merge"): {mkFlow("merge", "end")}, + } + + splitMergeMap := map[model.ID]model.ID{ + mkID("split"): mkID("merge"), + } + + var lines []string + visited := make(map[model.ID]bool) + e.traverseFlow(mkID("start"), activityMap, flowsByOrigin, splitMergeMap, visited, nil, nil, &lines, 1, nil, 0, nil) + + for _, line := range lines { + if contains(line, "else") { + t.Errorf("expected no else branch in output, got: %v", lines) + } + } + foundENDIF := false + for _, line := range lines { + if contains(line, "end if;") { + foundENDIF = true + } + } + if !foundENDIF { + t.Errorf("expected end if in output: %v", lines) + } +} + // ============================================================================= // collectErrorHandlerStatements // ============================================================================= From 174547f8a821911d9b321f6a98701d3a198ef3d0 Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Sun, 26 Apr 2026 23:53:46 +0200 Subject: [PATCH 2/2] test: add bug-test reproducer for empty else describer fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an MDL script under mdl-examples/bug-tests/ that exercises the describe → exec → describe fixpoint for IF-without-else, plus a negative control with an explicit else body. Co-Authored-By: Claude Opus 4.7 --- .../304-describer-empty-else-branch.mdl | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 mdl-examples/bug-tests/304-describer-empty-else-branch.mdl diff --git a/mdl-examples/bug-tests/304-describer-empty-else-branch.mdl b/mdl-examples/bug-tests/304-describer-empty-else-branch.mdl new file mode 100644 index 00000000..dc33189d --- /dev/null +++ b/mdl-examples/bug-tests/304-describer-empty-else-branch.mdl @@ -0,0 +1,57 @@ +-- ============================================================================ +-- Bug #304: Empty `else` branch emitted by DESCRIBE MICROFLOW +-- ============================================================================ +-- +-- Symptom (before fix): +-- `describe microflow` printed an `else` line for every IF whose false +-- branch jumped straight to the merge point (the shape produced by an +-- `if X then ... end if;` with no else body and a non-terminal true branch). +-- Output looked like +-- if $X > 0 then +-- ... +-- else +-- end if; +-- The empty else block re-parsed as a real branch, breaking +-- describe → exec → describe fixpoint and creating cosmetic BSON drift. +-- +-- After fix: +-- Split traversal in cmd_microflows_show_helpers.go skips the `else` +-- keyword when the false branch destination is the matching merge point. +-- Explicit else bodies (with at least one activity) are still emitted. +-- +-- Usage: +-- mxcli exec mdl-examples/bug-tests/304-describer-empty-else-branch.mdl -p app.mpr +-- mxcli -p app.mpr -c "describe microflow BugTest304.MF_IfWithoutElse" +-- The output must NOT contain an `else` keyword and MUST be a fixpoint +-- under describe → exec → describe. +-- ============================================================================ + +create module BugTest304; + +create microflow BugTest304.MF_IfWithoutElse ( + $value: integer +) +begin + if $value > 0 then + log info node 'BugTest304' 'positive value'; + end if; + + log info node 'BugTest304' 'always logged'; +end; +/ + +-- Negative control: explicit else body MUST still be preserved. +create microflow BugTest304.MF_IfWithElse ( + $value: integer +) +returns string as $label +begin + declare $label string = empty; + + if $value > 0 then + return 'positive'; + else + return 'non-positive'; + end if; +end; +/