Skip to content

fix: preserve custom error handler continuations#366

Open
hjotha wants to merge 8 commits intomendixlabs:mainfrom
hjotha:submit/custom-error-handler-routing
Open

fix: preserve custom error handler continuations#366
hjotha wants to merge 8 commits intomendixlabs:mainfrom
hjotha:submit/custom-error-handler-routing

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 27, 2026

Closes #349.
Part of #332.

Summary

  • keeps custom error-handler paths pending until the next safe continuation is known
  • rejoins non-terminal handler bodies through a merge instead of fabricating detached EndEvents
  • preserves empty custom-handler error flows and prevents output-producing error paths from continuing into statements that read a missing output variable

Validation

  • make build
  • make lint-go
  • make test

Copy link
Copy Markdown
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #366 — fix: preserve custom error handler continuations

Reviewed against the CLAUDE.md checklist.


Blockers

1. statementVarRefs and statementsVarRefs duplicated across open PRs — compile error when merged

PR #337 adds statementVarRefs and statementsVarRefs to cmd_microflows_builder_graph.go. This PR adds the same two functions to cmd_microflows_builder_flows.go. Both files are in package executor, both PRs target main. Merging them in either order produces:

./cmd_microflows_builder_flows.go: statementVarRefs redeclared in this block
./cmd_microflows_builder_flows.go: statementsVarRefs redeclared in this block

The two implementations are almost identical but differ in two places:

  • PR #337 is missing the RestCallStmt body field refs (s.Body.Template, s.Body.TemplateParams, s.Body.SourceVariable) that PR #366 adds — so the PR #366 version is the more complete one.
  • PR #337 has an if s.Item != "" nil-guard in the AddToListStmt case; PR #366 does not.

Fix options:

  • Move the canonical (merged) implementation to a new shared file cmd_microflows_builder_varutil.go before either PR merges, and remove the definitions from both builder_graph.go and builder_flows.go.
  • Or rebase PR #366 on PR #337's branch so that builder_graph.go already has the functions and builder_flows.go doesn't need to redeclare them.

Also worth noting: PR #337 adds referencedVariableSet(stmts) map[string]bool and this PR adds statementsReferenceVar(stmts, varName) bool. Both wrap statementsVarRefs to answer "does this variable appear here?" with different return shapes. After both PRs land, the package will have two overlapping utilities. Consolidating to one is not strictly a blocker, but flagging it for cleanup.


Moderate Issues

2. Active-handler state split between fields and slice — invariant not documented

flowBuilder gains 7 new fields (emptyErrorHandlerFrom, errorHandlerTailFrom, errorHandlerSource, errorHandlerSkipVar, errorHandlerTailIsSource, errorHandlerReturnValue, pendingErrorHandlers). The design is: the active handler lives in the flat fields; completed handlers are queued in the slice via queueActivePendingErrorHandler(). activePendingErrorHandler() and setActivePendingErrorHandler() shuttle between these two representations.

This invariant is non-obvious. A future reader adding a new call site may write directly to a field instead of going through the accessor methods and silently break the queuing. A short comment on the struct fields stating the invariant would pay off.

3. findExistingRejoinMerge scans all objects

The function iterates the entire fb.objects slice looking for an ExclusiveMerge whose destination flow matches. For typical Studio Pro microflows this is fine in practice, but a note that this is an O(N) scan (and the N is bounded by normal microflow sizes) would make the intent clearer.


Minor Issues

4. AnchorLeftAnchorTop change in newErrorHandlerFlow — rationale not documented

The change from AnchorLeft to AnchorTop as the destination connection index for error handler flows is presumably because error handlers enter the destination node from above rather than from the side. This is a visual Studio Pro layout change. A one-line comment explaining why would help future readers understand whether this is load-bearing or cosmetic.

5. hasReturnValue bool is redundant

fb.returnType is already stored, and fb.hasReturnValue is always assigned as returns != nil && returns.Type.Kind != ast.TypeVoid. Callers can compute this from fb.returnType without the extra field. Not a correctness issue, but it adds a third way to answer the same question alongside fb.returnValue != "".


Positive Notes

  • The deduplication of the 10 copy-pasted error-handler blocks in builder_actions.go and builder_calls.go into finishCustomErrorHandler is a clear improvement.
  • 5 focused unit tests in cmd_microflows_builder_terminal_test.go with the flowPathExists BFS helper cover the key routing scenarios well.
  • Bug-test file 349-custom-error-handler-routing.mdl has thorough header documentation and correctly sets up the non-terminal handler scenario.
  • The pendingErrorHandlerState struct cleanly separates the data from the builder fields, making queueActivePendingErrorHandler easy to follow once the invariant is understood.

Summary: One blocker — the statementVarRefs/statementsVarRefs duplicate with PR #337 needs to be resolved before this can merge. The fix is a one-time extraction to a shared file. All other findings are moderate/minor documentation and style issues.

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

None found.

What Looks Good

  • Bug fix focus: The PR cleanly addresses issue Preserve custom error-handler routing during microflow roundtrip #349 without scope creep
  • Comprehensive testing:
    • Added mdl-examples/bug-tests/349-custom-error-handler-routing.mdl reproduces the bug and validates the fix
    • Added extensive unit tests in cmd_microflows_builder_terminal_test.go covering:
      • Non-terminal custom handler rejoining continuation
      • Consecutive custom handlers each rejoining continuation
      • Empty output handler terminating before output-dependent tail
      • Output handler terminating before declare referencing output
      • Empty no-output handler rejoining at next action
  • Logical refactor: Replaced scattered error handler handling with centralized finishCustomErrorHandler() logic
  • Proper layering: Fix resides entirely in executor layer (no grammar/AST/visitor changes needed)
  • Clear documentation: Bug test file includes symptom description, root cause, fix explanation, and validation steps
  • Deterministic testing: No time.sleep usage in tests
  • Consistent patterns: Uses existing microflow builder patterns and types

Recommendation

Approve the PR. It correctly fixes bug #349 with appropriate test coverage, follows project conventions, and introduces no regressions. The changes are focused, well-tested, and solve the specific problem described in the issue.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

None found.

What Looks Good

  • Targeted fix: The PR addresses a specific bug (Preserve custom error-handler routing during microflow roundtrip #349) in custom error handler routing without unrelated changes.
  • Comprehensive testing: Adds both a doctype test (mdl-examples/bug-tests/349-custom-error-handler-routing.mdl) and extensive unit tests in cmd_microflows_builder_terminal_test.go covering non-terminal handlers, consecutive handlers, empty handlers, and output-dependent continuation prevention.
  • Centralized logic: Replaces duplicated error handler building across action types with a unified finishCustomErrorHandler method and pending state management.
  • Clear documentation: The test file includes detailed symptom descriptions, root cause analysis, and validation instructions.
  • Consistent application: Changes applied uniformly to all microflow action types (create, commit, delete, retrieve, calls, etc.).
  • No syntax changes: As a bug fix to existing behavior, no MDL grammar/AST/visitor modifications were needed, avoiding unnecessary pipeline changes.
  • Robust state management: Implements queuing of pending error handlers across branch boundaries with proper synchronization via helper methods.

Recommendation

Approve the PR. This is a well-scoped bug fix with adequate test coverage that resolves the reported issue without violating any project conventions or introducing risks. The changes follow existing patterns and maintain code quality standards.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 29, 2026
Symptom: the combined open-PR branch still produced an MPR check failure for a
custom-error-handler path even though the described MDL round-tripped exactly.
The handler rejoined before a DECLARE whose initial value referenced the failed
action output, so Studio Pro reported the output variable as out of scope.

Root cause: the combined branch was missing the already-submitted custom error
handler fix that treats DECLARE initial values as output-variable references.
That fix exists in PR mendixlabs#366 but had been lost during combined-branch conflict
resolution.

Fix: restore DECLARE initial-value reference tracking and the synthetic graph
regression from the custom-error-handler PR.

Tests: ran the targeted executor regression for output-dependent DECLARE routing.
hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 30, 2026
Moves statementVarRefs / statementsVarRefs / referencedVariableSet /
expressionStringVarRefs out of cmd_microflows_builder_graph.go into
cmd_microflows_builder_varutil.go. Sibling builder passes (PR mendixlabs#366's
pending error-handler queue, future planners) need the same walker
without redeclaring it in their own file. Pure refactor — no
behavior change. Also marks the variable-alias-collision proposal as
Implemented now that the feature is shipping.

Addresses ako's review on PR mendixlabs#337.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
hjothamendix and others added 6 commits April 30, 2026 09:53
Symptom: custom error-handler bodies that did not explicitly return were rebuilt as detached terminal paths, while empty custom handlers could either lose their error flow or rejoin into statements that read an output variable that is 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, and a later handler could overwrite an earlier pending handler.

Fix: queue pending custom handler state, route non-terminal handlers through the next safe continuation via a merge, preserve empty custom-handler flows, and terminate output-producing handlers before output-dependent continuation statements in void microflows.

Tests: added synthetic builder regressions for non-terminal handler rejoin, consecutive pending handlers, empty output-handler termination, and empty no-output-handler rejoin. Ran make build, make lint-go, and make test.
Adds an MDL script under mdl-examples/bug-tests/ exercising a
non-terminal custom error handler on a microflow call followed by a
continuation call. After exec, `mx check` reports 0 errors,
confirming the handler tail rejoins the continuation instead of
becoming a detached terminal path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Symptom: a custom error handler for an output-producing action could rejoin
before a later DECLARE whose initial value referenced that output variable.
Studio Pro then rejected the roundtripped microflow because the output variable
is not in scope on the error-handler path.

Root cause: skip-variable routing collected references from many statement
kinds, but omitted DECLARE initial values.

Fix: include DECLARE initial values in statement reference analysis so handlers
wait for, or terminate before, output-dependent declarations.

Tests: add a graph-level regression that verifies the error-handler path cannot
reach an output-dependent DECLARE, plus the existing custom-handler routing
tests via make build and make test.
Symptom: merging the custom error-handler routing PR with the variable-alias
PR would redeclare statementVarRefs and statementsVarRefs in package executor.

Root cause: both PRs added local helper names for different pre-passes while
sharing the same package namespace.

Fix: rename the custom error-handler helper pair to handler-specific names and
add review-requested comments documenting the pending-handler state invariant,
the bounded rejoin scan, and the top-entry anchor used for error-handler flows.

Tests: make build; focused error-handler executor tests; make test.
Symptom: a custom error handler inside a returning IF branch could be forced to
terminate when the branch success tail declared values from the failed action
output. The resulting MDL inserted a return into the handler, and the generated
MPR no longer matched the original control flow.

Root cause: branch merge detection only treated empty custom handlers as needing
a merge, and output skip-var routing stopped at the first output-dependent
statement in void microflows. Derived variables declared from the failed output
and SHOW MESSAGE template arguments were also invisible to the skip-var scan.

Fix: treat non-terminal custom handlers as branch merge inputs, route pending
handlers from returning branches to that merge, carry skip-var state across
DECLARE-derived variables, and include SHOW MESSAGE expressions in handler
reference analysis.

Tests: added a synthetic branch-level regression covering a custom handler that
skips an output-derived success tail and rejoins at a shared error continuation.
Ran make build, make test, and make lint-go.
Symptom: after rebasing the custom error-handler routing PR, empty custom handlers on Java and nanoflow calls were still handled by the old body-only path, so output-dependent tails had no safe error-handler termination.

Root cause: the rebase kept legacy per-action handler code for those call actions instead of the shared finishCustomErrorHandler helper used by the rest of the PR.

Fix: route both actions through finishCustomErrorHandler so empty handlers, output skip variables, and non-empty handlers use the same pending-handler state machine.

Tests: make build; make test; make lint-go.
@hjotha hjotha force-pushed the submit/custom-error-handler-routing branch from d66c0ca to 3bf45d4 Compare April 30, 2026 08:00
@github-actions
Copy link
Copy Markdown

AI Code Review

What Looks Good

  • The PR addresses bug Preserve custom error-handler routing during microflow roundtrip #349 with a focused fix for custom error handler routing in microflows
  • Adds a comprehensive test script (mdl-examples/bug-tests/349-custom-error-handler-routing.mdl) that reproduces the issue and validates the fix
  • Includes extensive unit tests in cmd_microflows_builder_terminal_test.go covering multiple scenarios:
    • Non-terminal handlers rejoining continuations
    • Consecutive handlers each properly rejoining
    • Empty handlers terminating before output-dependent statements
    • Handler state propagation across control flow branches
  • Refactors error handler handling consistently across all activity types (create, commit, delete, retrieve, calls, etc.) and workflows
  • Preserves empty custom handler flows and prevents invalid continuations that would read missing output variables
  • Uses proper queuing mechanism for pending error handlers across branch boundaries
  • Maintains existing behavior for terminal handlers (those with explicit RETURN/RAISE)
  • Changes are isolated to the microflow builder executor layer without violating backend abstraction
  • No changes to MDL syntax, grammar, AST, or visitor layers (appropriate for a bug fix in execution logic)
  • Follows existing code patterns and style

Recommendation

Approve the PR. The fix correctly resolves the reported issue with appropriate test coverage and maintains code quality standards. No changes are needed to the PR.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 30, 2026
Moves statementVarRefs / statementsVarRefs / referencedVariableSet /
expressionStringVarRefs out of cmd_microflows_builder_graph.go into
cmd_microflows_builder_varutil.go. Sibling builder passes (PR mendixlabs#366's
pending error-handler queue, future planners) need the same walker
without redeclaring it in their own file. Pure refactor — no
behavior change. Also marks the variable-alias-collision proposal as
Implemented now that the feature is shipping.

Addresses ako's review on PR mendixlabs#337.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@hjotha
Copy link
Copy Markdown
Contributor Author

hjotha commented Apr 30, 2026

Review follow-up status on the current head:

  • Avoided the statementVarRefs/statementsVarRefs duplicate by keeping the custom-error-handler walker private to this PR (errorHandlerStatementVarRefs) instead of defining the same package-level helpers as fix: preserve duplicate implicit output aliases #337.
  • Added comments documenting the active-vs-queued pending-error-handler invariant.
  • Added comments for the bounded O(N) merge scan and the AnchorTop destination choice for error-handler flows.

Local validation after the update was green:

make build
make test
make lint-go

Symptom: the custom error-handler builder stored both returnType and a redundant hasReturnValue boolean, leaving two sources of truth for whether the microflow returns a value.

Root cause: hasReturnValue was assigned from returnType during graph construction and copied into nested error-handler builders even though the same answer can be derived directly from returnType.

Fix: remove the boolean field, add a small hasDeclaredReturnValue helper, and use it at the custom-handler routing decision point.

Tests: ran make build, targeted executor custom-error-handler tests, make test, and make lint-go.
Symptom: describe emitted IF statements inside custom error-handler blocks without the matching END IF, so exec rejected valid roundtripped handlers.

Root cause: error-handler traversal linearized every reachable activity until the first merge and formatted nested splits as plain statements, losing block structure.

Fix: traverse error-handler branches structurally, stop at the handler rejoin merge, and emit IF/ELSE/END IF around nested exclusive splits.

Tests: added a synthetic error-handler graph with a nested conditional and ran make test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preserve custom error-handler routing during microflow roundtrip

3 participants