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
66 changes: 66 additions & 0 deletions mdl-examples/bug-tests/328-describer-nested-loop-body-flows.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
-- ============================================================================
-- Bug #328: DESCRIBE MICROFLOW omitted nested loop body continuation flows
-- ============================================================================
--
-- Symptom (before fix):
-- Some microflow graphs store the loop boundary flow (last body activity →
-- loop iterator) in the loop's local object collection, while the real
-- body-to-body continuation flows are stored in the parent microflow
-- graph. When a body activity already had a local outgoing flow (the
-- boundary one), the describer ignored parent-level flows with the same
-- origin. Result: activities downstream of that origin disappeared from
-- the described loop body — most visible on nested loops, where the
-- inner body collapsed into one statement.
--
-- Root cause:
-- `emitLoopBody` built a fresh `loopFlowsByOrigin` from the loop's local
-- object collection only, then short-circuited when an origin had a
-- local entry, never merging the equivalent parent-level flows.
--
-- After fix:
-- The describer now merges parent-level body flows into the loop-local
-- traversal map for any origin that lives in the loop body, including
-- nested loop descendants collected recursively.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/328-describer-nested-loop-body-flows.mdl -p app.mpr
-- mxcli -p app.mpr -c "describe microflow BugTest328.MF_NestedLoops"
-- The describe output must contain BOTH outer- and inner-loop body
-- activities and must be a fixpoint under describe → exec → describe.
-- ============================================================================

create module BugTest328;

create entity BugTest328.Item (
Name : string(100)
);
/

create entity BugTest328.Role (
Name : string(100)
);
/

create microflow BugTest328.MF_NestedLoops (
$Items: list of BugTest328.Item,
$Roles: list of BugTest328.Role
)
begin
log info node 'BugTest328' 'before outer';

loop $Item in $Items
begin
log info node 'BugTest328' 'outer head';

loop $Role in $Roles
begin
log info node 'BugTest328' 'inner body';
log info node 'BugTest328' 'inner tail';
end loop;

log info node 'BugTest328' 'outer tail';
end loop;

log info node 'BugTest328' 'after outer';
end;
/
2 changes: 1 addition & 1 deletion mdl/executor/cmd_microflows_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type flowBuilder struct {
microflowsCacheLoaded bool
nanoflowsCache []*microflows.Nanoflow
nanoflowsCacheLoaded bool
manualLoopBackTarget model.ID
manualLoopBackTarget model.ID
}

// addError records a validation error during flow building.
Expand Down
42 changes: 21 additions & 21 deletions mdl/executor/cmd_microflows_format_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,30 +351,30 @@ func formatAction(

stmt := fmt.Sprintf("retrieve $%s from %s", outputVar, entityName)

if dbSource.XPathConstraint != "" {
constraint := strings.TrimSpace(dbSource.XPathConstraint)
// XPath may contain multiple predicates like [a][b] or [a]\n[b].
// Split them and join with MDL 'and' so the parser sees
// separate xpathConstraint nodes.
if strings.HasPrefix(constraint, "[") && strings.HasSuffix(constraint, "]") {
// Split on "][" boundary (possibly separated by \n literals),
// then re-wrap each predicate.
inner := constraint[1 : len(constraint)-1]
// Normalise real newlines between predicates: ]\n[ → ][
inner = strings.ReplaceAll(inner, "]\n[", "][")
parts := strings.Split(inner, "][")
if len(parts) > 1 {
var wrapped []string
for _, p := range parts {
wrapped = append(wrapped, "["+strings.TrimSpace(p)+"]")
if dbSource.XPathConstraint != "" {
constraint := strings.TrimSpace(dbSource.XPathConstraint)
// XPath may contain multiple predicates like [a][b] or [a]\n[b].
// Split them and join with MDL 'and' so the parser sees
// separate xpathConstraint nodes.
if strings.HasPrefix(constraint, "[") && strings.HasSuffix(constraint, "]") {
// Split on "][" boundary (possibly separated by \n literals),
// then re-wrap each predicate.
inner := constraint[1 : len(constraint)-1]
// Normalise real newlines between predicates: ]\n[ → ][
inner = strings.ReplaceAll(inner, "]\n[", "][")
parts := strings.Split(inner, "][")
if len(parts) > 1 {
var wrapped []string
for _, p := range parts {
wrapped = append(wrapped, "["+strings.TrimSpace(p)+"]")
}
constraint = strings.Join(wrapped, "\n ")
} else {
constraint = parts[0]
}
constraint = strings.Join(wrapped, "\n ")
} else {
constraint = parts[0]
}
stmt += fmt.Sprintf("\n where %s", constraint)
}
stmt += fmt.Sprintf("\n where %s", constraint)
}

// Output SORT BY clause if present
if len(dbSource.Sorting) > 0 {
Expand Down
17 changes: 13 additions & 4 deletions mdl/executor/cmd_microflows_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -844,16 +844,25 @@ func findSplitMergePoints(
oc *microflows.MicroflowObjectCollection,
activityMap map[model.ID]microflows.MicroflowObject,
) map[model.ID]model.ID {
result := make(map[model.ID]model.ID)

// Build flow graph for forward traversal
flowsByOrigin := make(map[model.ID][]*microflows.SequenceFlow)
for _, flow := range oc.Flows {
flowsByOrigin[flow.OriginID] = append(flowsByOrigin[flow.OriginID], flow)
}

// For each ExclusiveSplit, find its merge point
for _, obj := range oc.Objects {
return findSplitMergePointsForGraph(ctx, activityMap, flowsByOrigin)
}

// findSplitMergePointsForGraph finds the corresponding merge point for each
// split in an already materialized flow graph. Nested traversals such as loop
// bodies use this because they do not have a top-level object collection.
func findSplitMergePointsForGraph(
ctx *ExecContext,
activityMap map[model.ID]microflows.MicroflowObject,
flowsByOrigin map[model.ID][]*microflows.SequenceFlow,
) map[model.ID]model.ID {
result := make(map[model.ID]model.ID)
for _, obj := range activityMap {
if _, ok := obj.(*microflows.ExclusiveSplit); ok {
splitID := obj.GetID()
// Find merge by following both branches until they converge
Expand Down
Loading
Loading