Skip to content
Open
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
64 changes: 64 additions & 0 deletions mdl-examples/bug-tests/349-custom-error-handler-routing.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- ============================================================================
-- Bug #349: Custom error-handler routing during microflow roundtrip
-- ============================================================================
--
-- Symptom (before fix):
-- Custom error-handler bodies that did NOT explicitly return were
-- rebuilt as DETACHED terminal paths — they got their own EndEvent and
-- never rejoined the next activity. Empty custom handlers could either
-- lose their error flow entirely or rejoin into statements that read
-- an output variable absent on the error path.
--
-- Root cause:
-- The microflow builder handled custom error handlers immediately at
-- the source activity. It did not retain pending handler state until
-- the next safe continuation was known, so a later handler could
-- overwrite an earlier pending handler.
--
-- After fix:
-- - Pending custom-handler state is queued.
-- - Non-terminal handler bodies rejoin through a merge before the
-- next safe continuation, instead of fabricating detached EndEvents.
-- - Empty custom-handler error flows are preserved.
-- - Output-producing handlers terminate before output-dependent
-- continuation in void microflows.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/349-custom-error-handler-routing.mdl -p app.mpr
-- mxcli -p app.mpr -c "describe microflow BugTest349.MF_Caller"
-- `mx check` against the resulting MPR must report 0 errors and the
-- non-terminal handler must rejoin the continuation activity.
-- ============================================================================

create module BugTest349;

create microflow BugTest349.MF_RefreshData (
$Token: string
)
begin
log info node 'BugTest349' 'refreshed: ' + $Token;
end;
/

create microflow BugTest349.MF_NextBatch ()
begin
log info node 'BugTest349' 'next batch';
end;
/

-- Caller with non-terminal custom error handler. The handler body logs
-- but does not return, so its tail must rejoin the continuation
-- (`call microflow ... MF_NextBatch ()`) instead of becoming a detached
-- terminal path.
create microflow BugTest349.MF_Caller (
$Token: string
)
begin
call microflow BugTest349.MF_RefreshData (Token = $Token)
on error without rollback {
log error node 'BugTest349' 'refresh failed';
};

call microflow BugTest349.MF_NextBatch ();
end;
/
1 change: 1 addition & 0 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ type IfStmt struct {
Condition Expression // IF condition
ThenBody []MicroflowStatement // THEN branch
ElseBody []MicroflowStatement // ELSE branch (optional)
HasElse bool // true when the source contained ELSE, even if the body is empty
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/cmd_diff_mdl.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func microflowStatementToMDL(ctx *ExecContext, stmt ast.MicroflowStatement, inde
for _, thenStmt := range s.ThenBody {
lines = append(lines, microflowStatementToMDL(ctx, thenStmt, indent+1)...)
}
if len(s.ElseBody) > 0 {
if s.HasElse || len(s.ElseBody) > 0 {
lines = append(lines, indentStr+"else")
for _, elseStmt := range s.ElseBody {
lines = append(lines, microflowStatementToMDL(ctx, elseStmt, indent+1)...)
Expand Down
20 changes: 19 additions & 1 deletion mdl/executor/cmd_microflows_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ type flowBuilder struct {
posY int
baseY int // Base Y position (for returning after ELSE branches)
spacing int
returnValue string // Return value expression for RETURN statement (used by buildFlowGraph final EndEvent)
returnValue string // Return value expression for RETURN statement (used by buildFlowGraph final EndEvent)
returnType *ast.MicroflowReturnType
endsWithReturn bool // True if the flow already ends with EndEvent(s) from RETURN statements
lastReturnEndID model.ID // Last explicit RETURN EndEvent, used as a fallback error-handler target
varTypes map[string]string // Variable name -> entity qualified name (for CHANGE statements)
declaredVars map[string]string // Declared primitive variables: name -> type (e.g., "$IsValid" -> "Boolean")
errors []string // Validation errors collected during build
Expand Down Expand Up @@ -54,6 +56,18 @@ type flowBuilder struct {
nanoflowsCache []*microflows.Nanoflow
nanoflowsCacheLoaded bool
manualLoopBackTarget model.ID
// Pending custom error-handler routing uses two representations: the
// currently active handler lives in the flat fields below, while handlers
// postponed across branch boundaries are queued in pendingErrorHandlers.
// Mutate this state through the helper methods in builder_flows.go so the
// active/queued invariant stays synchronized.
emptyErrorHandlerFrom model.ID
errorHandlerTailFrom model.ID
errorHandlerSource model.ID
errorHandlerSkipVar string
errorHandlerTailIsSource bool
errorHandlerReturnValue string
pendingErrorHandlers []pendingErrorHandlerState
}

// addError records a validation error during flow building.
Expand All @@ -71,6 +85,10 @@ func (fb *flowBuilder) GetErrors() []string {
return fb.errors
}

func (fb *flowBuilder) hasDeclaredReturnValue() bool {
return fb.returnType != nil && fb.returnType.Type.Kind != ast.TypeVoid
}

// errorExampleDeclareVariable returns an example for declaring a variable.
func errorExampleDeclareVariable(varName string) string {
// Remove $ prefix if present for cleaner display
Expand Down
28 changes: 4 additions & 24 deletions mdl/executor/cmd_microflows_builder_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID {
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.Variable)

return activity.ID
}
Expand Down Expand Up @@ -167,12 +162,7 @@ func (fb *flowBuilder) addCommitAction(s *ast.MfCommitStmt) model.ID {
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")

return activity.ID
}
Expand Down Expand Up @@ -201,12 +191,7 @@ func (fb *flowBuilder) addDeleteAction(s *ast.DeleteObjectStmt) model.ID {
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")

return activity.ID
}
Expand Down Expand Up @@ -468,12 +453,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.Variable)

return activity.ID
}
Expand Down
1 change: 1 addition & 0 deletions mdl/executor/cmd_microflows_builder_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ func (fb *flowBuilder) addEndEventWithReturn(s *ast.ReturnStmt) model.ID {

fb.objects = append(fb.objects, endEvent)
fb.endsWithReturn = true
fb.lastReturnEndID = endEvent.ID
fb.posX += fb.spacing / 2
return endEvent.ID
}
Expand Down
67 changes: 10 additions & 57 deletions mdl/executor/cmd_microflows_builder_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,7 @@ func (fb *flowBuilder) addCallMicroflowAction(s *ast.CallMicroflowStmt) model.ID
fb.registerResultVariableType(s.OutputVariable, fb.lookupMicroflowReturnType(mfQN))
}

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -218,12 +213,7 @@ func (fb *flowBuilder) addCallNanoflowAction(s *ast.CallNanoflowStmt) model.ID {
fb.registerResultVariableType(s.OutputVariable, fb.lookupNanoflowReturnType(nfQN))
}

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -325,12 +315,7 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -427,12 +412,7 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -478,12 +458,7 @@ func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -978,12 +953,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID {
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -1187,12 +1157,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

// Build custom error handler flow if present
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -1259,11 +1224,7 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt)
}
}

if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)

return activity.ID
}
Expand Down Expand Up @@ -1295,11 +1256,7 @@ func (fb *flowBuilder) addTransformJsonAction(s *ast.TransformJsonStmt) model.ID
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")

return activity.ID
}
Expand Down Expand Up @@ -1333,11 +1290,7 @@ func (fb *flowBuilder) addExportToMappingAction(s *ast.ExportToMappingStmt) mode
fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")

return activity.ID
}
Loading
Loading