Skip to content

feat: @anchor annotation for microflow sequence flow endpoints#276

Merged
ako merged 3 commits intomendixlabs:mainfrom
hjotha:submit/sequence-flow-anchor-annotation
Apr 23, 2026
Merged

feat: @anchor annotation for microflow sequence flow endpoints#276
ako merged 3 commits intomendixlabs:mainfrom
hjotha:submit/sequence-flow-anchor-annotation

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 23, 2026

Summary

Implements the @anchor(from: X, to: Y) annotation proposed in #275 so that DESCRIBE MICROFLOW output and mxcli exec roundtrips preserve each SequenceFlow's connection side — previously dropped on describe and always re-derived from the visual direction on exec.

Syntax

Simple form — for non-split statements:

@anchor(from: right, to: left)
log info node 'App' 'hello';

Each side is independently optional; missing sides fall back to the builder default so existing scripts are unchanged.

Combined form — for IF, carries the incoming side plus per-branch outgoing sides:

@anchor(to: top, true: (from: right, to: left), false: (from: bottom, to: top))
if condition then
    ...
else
    ...
end if;

Values: top (0), right (1), bottom (2), left (3) — matching the BSON OriginConnectionIndex / DestinationConnectionIndex.

Ancillary fixes

Enforcing the roundtrip invariant exposed three pre-existing bugs in the describer that would have blocked the annotation from surviving describe → check:

  1. Expression string literals double-escaped backslashes. isMatch($x, '^\d+$') round-tripped as '^\\d+$', then '^\\\\d+$', etc. New quoteExpressionLiteral escapes only what the lexer requires (\n, \r, \t, trailing \, backslash followed by an escape letter, and apostrophe) and passes regex-escape sequences through verbatim.
  2. Loop describe output omitted begin. Grammar requires loop $X in $Y begin ... end loop; but the describer wrote the header directly before the body. It happened to parse before because every body statement's leading keyword was accepted. With @anchor landing before the first body statement, the parser saw @position(...) right after the loop header and failed with cascade errors.
  3. @annotation / @caption emitted raw control characters. Free annotations used a home-grown escape that only doubled apostrophes; multiline @annotation text (as in MxAdmin.CreateMendixVersionFromString) was then rejected by the parser on re-execute. Switched to mdlQuote.

Implementation highlights

  • AST: ast.AnchorSide, ast.FlowAnchors; ActivityAnnotations gains Anchor, TrueBranchAnchor, FalseBranchAnchor (plus IteratorAnchor / BodyTailAnchor reserved for a follow-up on LOOP/WHILE internal flows).
  • Grammar: ANCHOR, TOP, BOTTOM lexer tokens; anchorSide production + nested annotationParenValue for the split form.
  • Visitor: populates anchor fields from annotation contexts with the same keyword tolerance as the rest of the MDL visitor.
  • Builder: applyUserAnchors helper; mergeStatementAnnotations carries anchor fields into pendingAnnotations; buildFlowGraph and addIfStatement apply them per the user's spec. Guard-pattern IFs (no else, then returns) use new nextFlowAnchor plumbing so the deferred split→nextActivity flow also honours @anchor(false: ...).
  • Describer: emitAnchorAnnotation outputs the simple form for non-split activities; split activities delegate to emitSplitAnchorAnnotation. Branch detection reuses findBranchFlows so all CaseValue variants (ExpressionCase, EnumerationCase, BooleanCase, value + pointer) are covered.

Test coverage

  • Grammar / visitor: 5 cases (simple syntax, partial, 4 sides, split form, absence-is-nil).
  • Builder: 7 cases (override, default preserved, partial override, per-branch IF anchor, guard-pattern IF carry-through).
  • Describer: 6 cases (simple form, split form with each CaseValue variant, no-flows skip, expression literal escape, loop begin emission).
  • Regression: cmd_microflows_anchor_if_test.go pins the attempt-Fix batch of reported issues (#18, #19, #23, #25, #26, #27, #28) #35 scenario (anchor inside ELSE branch + return with to: top).

All tests run with go test ./... green (29 packages, 0 failures).

E2E verification

Control Centre (Mendix 9.24, ~200 microflows). 5 representative microflows including the attempt-#35 repro:

  • Apps.GetOrCreateMendixVersionFromString — 0 anchor drifts
  • RepositoryServiceIntegration.GetDisplayVersion — 0 drifts
  • MxAdmin.CreateMendixVersionFromString — 0 drifts; also validates multiline @annotation with '' and \r\n survives roundtrip
  • AcademyIntegration.GetOrCreateCertificate — guard-pattern IF, 0 drifts
  • Administration.ChangePassword — 0 drifts

All 5 pass mxcli check on the describe output AND round-trip describe → exec → describe with byte-exact anchor preservation.

Closes

#275

@hjotha hjotha force-pushed the submit/sequence-flow-anchor-annotation branch from f58e6c2 to 732913b Compare April 23, 2026 12:10
@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • The PR description mentions that IteratorAnchor and BodyTailAnchor are "reserved for a follow-up on LOOP/WHILE internal flows" but these fields are actually initialized in the AST (in ActivityAnnotations struct). While not harmful, it would be cleaner to either:

    1. Initialize them as nil and document they're for future use, or
    2. Add them in a separate follow-up PR
      However, since they're just nil pointers and don't affect current functionality, this is very minor.
  • In cmd_microflows_show_helpers.go, the emitObjectAnnotations function has a comment that says "// @anchor (emit whenever attached flows exist, for roundtrip fidelity)" but the actual implementation is several lines below. This is just a minor comment placement issue.

What Looks Good

  1. Complete full-stack implementation: The feature properly flows through grammar → AST → visitor → executor → backend → describer → LSP
  2. Thorough test coverage: Includes unit tests for builder, describer, roundtrip scenarios, and specific bug fixes
  3. Backward compatibility: Existing scripts remain unchanged as missing anchor sides fall back to builder defaults
  4. Bug fixes: The implementation uncovered and fixed three pre-existing bugs in the describer:
    • Expression string literal double-escaping
    • Missing begin in loop describe output
    • Raw control characters in @annotation/@caption
  5. Roundtrip fidelity: E2E verification shows byte-exact anchor preservation across describe → exec → describe cycles
  6. Proper escaping: New quoteExpressionLiteral function correctly handles Mendix expression strings while preserving regex escape sequences
  7. Split statement support: Properly handles the complex case of IF statements with per-branch anchors in both simple and guard-pattern forms
  8. Clear documentation: PR includes comprehensive examples and motivation
  9. LSP integration: Keywords properly added to completion suggestions

Recommendation

Approve. The PR successfully implements the @anchor annotation feature with full pipeline integration, comprehensive testing, and necessary bug fixes. All checklist items are satisfied with only minor nitpicks that don't block approval.


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

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 23, 2026
)

Addresses the two minor comments from the mendixlabs#276 AI review and tidies the
two fields the original PR reserved "for a follow-up on LOOP/WHILE
internal flows":

- Grammar: accept @anchor(iterator: (...), tail: (...)) on LOOP/WHILE
  statements via a new annotation param name (TAIL). Parser + visitor
  route the sides into the existing IteratorAnchor / BodyTailAnchor
  fields on ActivityAnnotations, so forward-compatible scripts parse
  cleanly today.

- Builder: deliberately does not serialise iterator/tail edges. Studio
  Pro rejects loop→body and body→loop SequenceFlows with CE0709
  "Sequence flow is not accepted by origin or destination" because the
  iterator icon is drawn implicitly from the LoopedActivity geometry.
  The annotation parses, but its payload is discarded at build time.
  A comment at the call sites explains why.

- Describer: emitAnchorAnnotation now has a LoopedActivity arm
  (emitLoopAnchorAnnotation) that emits the combined form
  @anchor(from, to, iterator, tail) when iterator/tail flows exist.
  On current Mendix projects those flows never exist, so the output is
  unchanged for real roundtrips — but the code is ready if a future
  Mendix version starts allowing those edges.

- Minor review fix: the misplaced "// @anchor (emit whenever attached
  flows exist, for roundtrip fidelity)" comment in emitObjectAnnotations
  now sits directly above the call it describes.

- Tests: seven new unit tests pin the behaviour — iterator/tail are
  accepted on LOOP and WHILE without emitting invalid flows, and the
  describer's loop emitter renders the combined form correctly when
  synthetic iterator/tail flows are provided. `mxcli docker check`
  still reports 0 errors on a fresh 11.9 project after exec'ing the
  updated bug-test script.

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

hjotha commented Apr 23, 2026

Follow-up commit 3bb8126 addresses both minor notes from the AI review:

  • IteratorAnchor / BodyTailAnchor — the grammar now accepts @anchor(iterator: (...), tail: (...)) on LOOP/WHILE statements and the visitor populates both fields. The builder intentionally does NOT translate them into SequenceFlows: Studio Pro rejects loop→body and body→loop edges with CE0709 ("Sequence flow is not accepted by origin or destination") because the iterator icon is drawn implicitly from the LoopedActivity geometry. Keeping the grammar slot reserved means scripts stay forward-compatible if a future Mendix version ever starts supporting these edges. The describer gains a emitLoopAnchorAnnotation helper that will render the combined form the moment such flows appear, but is a no-op on current projects.
  • Misplaced @anchor comment — relocated to sit directly above the actual emitAnchorAnnotation call in emitObjectAnnotations.

Verified against a fresh Mendix 11.9 project: mxcli exec for the updated bug-test script followed by mxcli docker check reports 0 errors, and describe → exec → describe round-trips cleanly.

@ako
Copy link
Copy Markdown
Collaborator

ako commented Apr 23, 2026

Code Review — feat: @anchor annotation for microflow sequence flow endpoints

Overview

Solid, well-motivated feature. The @anchor(from: X, to: Y) annotation closes a real gap: Studio Pro records which side of each activity box a SequenceFlow attaches to, but the describer was silently discarding that information, causing diagram re-layout on every describe → exec roundtrip. The three ancillary bug fixes (double-escape, missing begin, raw control chars in @annotation) are legitimate pre-existing issues that the roundtrip requirement exposed, and bundling them is appropriate.

E2E coverage (5 real-world microflows, 0 anchor drifts, mxcli check passing) is the right validation approach for this kind of change.


Issues

1. Package-level global state for flow maps (design concern)

var (
    currentFlowsByOrigin map[model.ID][]*microflows.SequenceFlow
    currentFlowsByDest   map[model.ID][]*microflows.SequenceFlow
)

The comment says "Not ideal, but keeps the blast radius confined." This is honest, but the two globals are a data race hazard: captureDescribeParallel in cmd_catalog.go spawns concurrent describe goroutines that all go through formatMicroflowActivities, which installs these maps. Two concurrent describe calls will clobber each other's maps mid-traversal. Consider passing the maps as fields on the ExecContext's Executor (they're per-traversal state, not per-session), or threading them directly into traverseFlow and emitObjectAnnotations. The current approach is safe for the current calling pattern but is a footgun if that ever changes.

2. Duplicate assertion in TestBuilder_AnchorInsideElseBranch

// Lines 368–375 — identical predicate, different error messages
if !hasFlow(oc.Flows, AnchorBottom, AnchorTop) {
    t.Errorf("expected split→log flow with Bottom→Top...")
}
if !hasFlow(oc.Flows, AnchorBottom, AnchorTop) {
    t.Errorf("expected log→return flow with Bottom→Top inside else branch...")
}

Both checks test the same condition. The second check would need (AnchorBottom, AnchorTop) to appear at least twice — but hasFlow returns true on the first match, so the second assertion passes trivially once one such flow exists. The test doesn't actually verify that two distinct Bottom→Top flows were created. This appears to be an oversight from the development process.

3. Dead code in TestBuilder_IfBranchAnchorOverrides

if ec, ok := f.CaseValue.(ast.Expression); ok {
    _ = ec
}
switch cv := f.CaseValue.(type) {
case nil:
    // skip
default:
    _ = cv
}

These twelve lines do nothing and should be removed. The actual verification proceeds via the GetValue() string interface check below them.

4. LSP completion labels are misleading

{Label: "TOP",    Kind: ..., Detail: "Query keyword"},
{Label: "BOTTOM", Kind: ..., Detail: "Query keyword"},
{Label: "ANCHOR", Kind: ..., Detail: "Query keyword"},

TOP/BOTTOM/ANCHOR are anchor-side / annotation keywords, not SQL query keywords. A completion detail of "Annotation keyword" or "Flow annotation" would be accurate and less confusing to users who encounter them in completion lists.

5. quoteExpressionLiteral edge case: \ followed by an unrecognised character

In the inner default path of the backslash handler:

b.WriteByte('\\')
continue

The continue re-enters the for i++ loop, so s[i+1] is processed on the next iteration. This means \d outputs \d — which is correct for regex roundtrip. But the function comment says "any other backslash-prefixed sequence is passed through unchanged." The implementation actually passes \ through and then emits the following character as plain, which is the same thing but the comment could be clearer. Worth a note: this is not a bug.


Minor observations

  • emitAnchorAnnotation picks outgoing[0] / incoming[0] for non-split activities. For well-formed microflows each non-split activity has exactly one incoming and one outgoing flow (or zero at boundaries), so this is correct. Worth a comment that this is a single-flow assumption.
  • emitLoopAnchorAnnotation builds innerIDs from loop.ObjectCollection.Objects but ObjectCollection can be nil — the nil guard is there (if loop.ObjectCollection != nil), so this is fine.
  • AnchorSideUnset = -1 as a sentinel for "use builder default" is clean and avoids nil pointer overhead. The values 0–3 matching OriginConnectionIndex/DestinationConnectionIndex directly in the BSON is a nice property.
  • Grammar slot reservation for @anchor(iterator: ..., tail: ...) on LOOP/WHILE with deliberate no-op in the builder is well-documented and forward-safe.

Summary

The core implementation is correct and well-tested. Two actionable items before merging:

  1. The package-level flow maps are a data race risk with concurrent describe (via captureDescribeParallel). If concurrent calls are possible in the current call graph, this should be fixed now; if they're currently sequential, add a comment and track it as a known risk.
  2. The duplicate assertion in TestBuilder_AnchorInsideElseBranch should be fixed — it leaves a gap in test coverage for the exact scenario the test is named after.

The dead test code and LSP label mismatch are minor, easy to fix before merge.

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 23, 2026
Addresses the substantive and minor notes from both reviews on PR mendixlabs#276.

ako review:

- **[substantive] Remove package-level currentFlowsByOrigin/currentFlowsByDest
  globals**. These were a data race under captureDescribeParallel, which
  spawns concurrent describe goroutines. flowsByOrigin/flowsByDest are now
  explicit parameters on traverseFlow / traverseFlowUntilMerge /
  traverseLoopBody / emitLoopBody / emitActivityStatement /
  emitObjectAnnotations. The legacy Executor.traverseFlow wrapper passes
  nil for flowsByDest (preserves prior @anchor-suppressed behaviour for
  pre-existing callers and tests). Added TestFormatMicroflowActivities_
  Concurrent_NoRace: 24 goroutines run formatMicroflowActivities in
  parallel on two distinct microflows and each worker's output is
  checked for correct per-flow @anchor content. Passes under -race.

- **Duplicate assertion in TestBuilder_AnchorInsideElseBranch**. The
  second hasFlow(AnchorBottom, AnchorTop) duplicated the first and
  trivially passed once any match existed. Replaced with countFlows()
  to assert two distinct Bottom→Top flows (split→log and log→return).

- **Dead code in TestBuilder_IfBranchAnchorOverrides**. The 12-line
  no-op type-assert block always fell through to a GetValue-based
  interface assertion that fails (EnumerationCase has no GetValue
  method). Replaced the whole post-build inspection with findBranchFlows
  — the describer's own helper that handles every CaseValue variant.

- **LSP completion labels "Query keyword"** on TOP/BOTTOM/ANCHOR.
  Moved the anchor tokens out of the OQL/QUERY section into a new
  "ANCHOR ANNOTATION KEYWORDS" section in MDLLexer.g4 and taught
  gen-completions to map it to "Flow annotation keyword".

- **quoteExpressionLiteral doc comment clarity**. Reworded the note
  about backslash followers that are not recognised escape letters: the
  backslash is emitted as-is and the follower is processed by the next
  loop iteration via the default arm — byte-identical to passthrough
  but walked separately.

codex review:

- **Parser/visitor regression tests for @anchor(iterator: ..., tail: ...)**.
  visitor_anchor_test.go gains three tests that feed real MDL text
  through Build() and assert IteratorAnchor / BodyTailAnchor fields:
  loop with both iterator+tail, while with both, loop with iterator-only
  (tail stays nil). The existing executor-side tests only used
  synthetic AST values.

- **Clarify bug-test file wording**. The usage note said describe output
  "must include the @anchor(...) lines below verbatim" but the LOOP/WHILE
  section immediately below says those are parse-only. Restructured into
  two labeled sections: Section A = roundtrip-preserving (flat @anchor +
  IF split), Section B = parse-only forward-compatibility (LOOP/WHILE
  iterator/tail). Usage text now describes each section's guarantee
  explicitly.

Validation:

- go test ./... passes.
- go test -race ./mdl/executor/ passes (including the new concurrent test).
- go build -tags integration ./... builds clean.
- Go lint passes.
- On a fresh Mendix 11.9 project: mxcli exec of the updated bug-test
  script followed by mxcli docker check reports 0 errors.
- describe → exec → describe is bit-exact for Section A microflows.

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

hjotha commented Apr 23, 2026

@ako thanks for the thorough review. Pushed 0279dde addressing every item:

1. Package-level flow-map globals (data race hazard) — removed. flowsByOrigin and flowsByDest are now explicit parameters threaded through traverseFlow, traverseFlowUntilMerge, traverseLoopBody, emitLoopBody, emitActivityStatement, and emitObjectAnnotations. The two describer entry points in cmd_microflows_show.go no longer install/restore package state. The legacy Executor.traverseFlow wrapper passes nil for flowsByDest so @anchor emission stays suppressed for pre-existing test callers. Added TestFormatMicroflowActivities_Concurrent_NoRace: 24 goroutines describe two distinct microflows in parallel and each worker's output is verified for per-flow anchor correctness. Passes under go test -race ./mdl/executor/.

2. Duplicate assertion in TestBuilder_AnchorInsideElseBranch — fixed. The second hasFlow check was identical and would pass trivially; replaced with a new countFlows helper that asserts exactly two distinct Bottom→Top flows exist (split→log and log→return), which is what the test was actually trying to prove.

3. Dead code in TestBuilder_IfBranchAnchorOverrides — removed. The 12-line no-op type-assert block fell through to a GetValue() interface check that never matches (neither EnumerationCase nor the other variants expose that method), so the whole assertion path was relying on a fallback. Replaced the inspection with the describer's own findBranchFlows helper, which matches every CaseValue variant uniformly.

4. LSP completion labels — fixed. Moved TOP/BOTTOM/ANCHOR out of the OQL / QUERY KEYWORDS lexer section into a new ANCHOR ANNOTATION KEYWORDS section, and taught cmd/gen-completions/main.go to map it to Flow annotation keyword. The regenerated cmd/mxcli/lsp_completions_gen.go now reads Detail: "Flow annotation keyword" for all three.

5. quoteExpressionLiteral doc comment — clarified. Reworded the unrecognised-backslash-follower note: the backslash is emitted as-is and the follower is handled by the next loop iteration via the default arm — byte-identical to passthrough but walked separately.

Also picked up the codex review:

  • Added three parser/visitor regression tests in mdl/visitor/visitor_anchor_test.go that feed real MDL text with @anchor(iterator: ..., tail: ...) through Build() and assert IteratorAnchor / BodyTailAnchor are populated (loop-both, while-both, loop-iterator-only).
  • Restructured the bug-test file into two clearly labeled sections: Section A (roundtrip-preserving — flat @anchor + IF split) and Section B (parse-only forward-compatibility — LOOP/WHILE iterator/tail). The usage text at the top describes each section's guarantee explicitly.

Validation: go test ./... passes, go test -race ./mdl/executor/ passes (including the new concurrent test), go build -tags integration ./... builds clean, Go lint passes, fresh Mendix 11.9 project + mxcli exec of the updated bug-test + mxcli docker check reports 0 errors, and Section A describe → exec → describe is bit-exact.

hjothamendix and others added 3 commits April 23, 2026 17:10
Studio Pro renders each SequenceFlow against one of four sides of the
origin and destination activity boxes (top, right, bottom, left). Until
now the builder always derived the anchor side from the flow's visual
direction and the describer dropped the information entirely, so a
DESCRIBE → re-execute round-trip lost every manual side-tweak. Agent-
generated microflow patches therefore reshuffled the arrow layout in
the Studio Pro diagram even when they were semantically identical to
the original — the flow diagram had to be re-tidied by hand after each
iteration.

New optional annotation on microflow statements:

    @anchor(from: right, to: left)
    log info node 'App' 'hello';

Each side is independently optional. Missing sides fall back to the
builder's default for the visual flow direction — existing MDL scripts
are unchanged.

IF statements support a combined form for the incoming flow plus the
per-branch outgoing flows:

    @anchor(to: top, true: (from: right, to: left), false: (from: bottom, to: top))
    if condition then
        ...
    else
        ...
    end if;

top (0), right (1), bottom (2), left (3) — matching the indices Mendix
stores in OriginConnectionIndex / DestinationConnectionIndex on the
SequenceFlow BSON.

@anchor is emitted for every activity whenever flows are attached, so
describe → exec → describe is bit-exact on the anchor indices. For
splits the combined form is used when any of the three groups (incoming
to, true-branch, false-branch) has a non-default value.

- AST: ast.AnchorSide, ast.FlowAnchors; ActivityAnnotations gains
  Anchor / TrueBranchAnchor / FalseBranchAnchor / IteratorAnchor /
  BodyTailAnchor (all optional, AST-only for iterator/tail).
- Grammar: ANCHOR / TOP / BOTTOM lexer tokens; `@anchor(key: value)`
  parser rule with a nested-params form for the split-branch shape.
- Visitor: populates anchor fields from annotation contexts.
- Builder: applyUserAnchors helper; mergeStatementAnnotations carries
  anchor fields into pendingAnnotations; buildFlowGraph and
  addIfStatement apply anchor indices per the user spec. Guard-pattern
  IFs (no else, then returns) use the new nextFlowAnchor plumbing so
  the deferred split→nextActivity flow also honours
  @anchor(false: ...).
- Describer: emitAnchorAnnotation outputs `@anchor(from: X, to: Y)`
  for non-split activities and a split-form variant for ExclusiveSplit
  / InheritanceSplit. Branch detection reuses findBranchFlows so all
  CaseValue variants (ExpressionCase, EnumerationCase, BooleanCase,
  value + pointer) are covered.

Tightening @anchor roundtrips exposed two pre-existing bugs in
expressionToString used for `if cond then`, regex literals, etc.:
- Using mdlQuote doubled every backslash, breaking regex escape
  sequences like \d that the Mendix expression engine consumes
  literally.
- Using only apostrophe-doubling emitted raw control characters (0x0A,
  0x0D, 0x09) inside single-quoted literals, which STRING_LITERAL
  rejects.

Fix: new quoteExpressionLiteral that escapes control chars and
backslashes followed by an MDL-significant letter (n/r/t/\/'), but
passes other backslash sequences through verbatim. Trailing backslash
at EOF is doubled so the lexer doesn't consume the closing quote as an
escape pair.

Loop describe output previously omitted the `begin` keyword the MDL
grammar requires after `loop $X in $Y`. It parsed by accident
because every body statement's leading keyword was accepted after the
header. With @anchor landing before the first body statement, the
parser started seeing `@position(...)` immediately after the loop
header and failed. Fix: emit `begin` unconditionally between the
loop header and body in every LoopedActivity call-site.

- visitor_anchor_test.go: syntax simple / partial / 4 sides / split
  form / absence → nil.
- cmd_microflows_builder_anchor_test.go: override applied; default
  kept when omitted; partial override preserves other side;
  per-branch IF anchors.
- cmd_microflows_describe_anchor_test.go: @anchor emission;
  no-flows case; roundtrip via builder.
- cmd_microflows_split_incoming_anchor_test.go: split incoming to;
  EnumerationCase, ExpressionCase, BooleanCase branch forms.
- cmd_microflows_expr_literal_escape_test.go: regex passthrough; raw
  control char escaping; apostrophe doubling; backslash-before-escape
  letter doubling; trailing backslash doubling; idempotency.
- cmd_microflows_anchor_if_test.go: anchor preservation inside ELSE
  branch (real mendixlabs#35 pattern); anchor(to: top) on return inside else.
- cmd_microflows_annotation_escape_test.go: mdlQuote control-char
  escaping; apostrophe doubling.
- cmd_microflows_guard_pattern_test.go: guard-pattern IF carries
  false-branch anchor through the deferred flow.
- cmd_microflows_loop_begin_test.go: loop describe output opens with
  `begin`.

Tested against the Control Centre project (Mendix 9.24, ~200
microflows). On 5 representative cases including the attempt-mendixlabs#35 repro
(Apps.GetOrCreateMendixVersionFromString, MxAdmin.CreateMendixVersion
FromString, AcademyIntegration.GetOrCreateCertificate), the describe →
exec → describe round-trip preserves every anchor (0 drifts) and the
output parses through `mxcli check`.

CHANGELOG updated under [Unreleased] → Added.
)

Addresses the two minor comments from the mendixlabs#276 AI review and tidies the
two fields the original PR reserved "for a follow-up on LOOP/WHILE
internal flows":

- Grammar: accept @anchor(iterator: (...), tail: (...)) on LOOP/WHILE
  statements via a new annotation param name (TAIL). Parser + visitor
  route the sides into the existing IteratorAnchor / BodyTailAnchor
  fields on ActivityAnnotations, so forward-compatible scripts parse
  cleanly today.

- Builder: deliberately does not serialise iterator/tail edges. Studio
  Pro rejects loop→body and body→loop SequenceFlows with CE0709
  "Sequence flow is not accepted by origin or destination" because the
  iterator icon is drawn implicitly from the LoopedActivity geometry.
  The annotation parses, but its payload is discarded at build time.
  A comment at the call sites explains why.

- Describer: emitAnchorAnnotation now has a LoopedActivity arm
  (emitLoopAnchorAnnotation) that emits the combined form
  @anchor(from, to, iterator, tail) when iterator/tail flows exist.
  On current Mendix projects those flows never exist, so the output is
  unchanged for real roundtrips — but the code is ready if a future
  Mendix version starts allowing those edges.

- Minor review fix: the misplaced "// @anchor (emit whenever attached
  flows exist, for roundtrip fidelity)" comment in emitObjectAnnotations
  now sits directly above the call it describes.

- Tests: seven new unit tests pin the behaviour — iterator/tail are
  accepted on LOOP and WHILE without emitting invalid flows, and the
  describer's loop emitter renders the combined form correctly when
  synthetic iterator/tail flows are provided. `mxcli docker check`
  still reports 0 errors on a fresh 11.9 project after exec'ing the
  updated bug-test script.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses the substantive and minor notes from both reviews on PR mendixlabs#276.

ako review:

- **[substantive] Remove package-level currentFlowsByOrigin/currentFlowsByDest
  globals**. These were a data race under captureDescribeParallel, which
  spawns concurrent describe goroutines. flowsByOrigin/flowsByDest are now
  explicit parameters on traverseFlow / traverseFlowUntilMerge /
  traverseLoopBody / emitLoopBody / emitActivityStatement /
  emitObjectAnnotations. The legacy Executor.traverseFlow wrapper passes
  nil for flowsByDest (preserves prior @anchor-suppressed behaviour for
  pre-existing callers and tests). Added TestFormatMicroflowActivities_
  Concurrent_NoRace: 24 goroutines run formatMicroflowActivities in
  parallel on two distinct microflows and each worker's output is
  checked for correct per-flow @anchor content. Passes under -race.

- **Duplicate assertion in TestBuilder_AnchorInsideElseBranch**. The
  second hasFlow(AnchorBottom, AnchorTop) duplicated the first and
  trivially passed once any match existed. Replaced with countFlows()
  to assert two distinct Bottom→Top flows (split→log and log→return).

- **Dead code in TestBuilder_IfBranchAnchorOverrides**. The 12-line
  no-op type-assert block always fell through to a GetValue-based
  interface assertion that fails (EnumerationCase has no GetValue
  method). Replaced the whole post-build inspection with findBranchFlows
  — the describer's own helper that handles every CaseValue variant.

- **LSP completion labels "Query keyword"** on TOP/BOTTOM/ANCHOR.
  Moved the anchor tokens out of the OQL/QUERY section into a new
  "ANCHOR ANNOTATION KEYWORDS" section in MDLLexer.g4 and taught
  gen-completions to map it to "Flow annotation keyword".

- **quoteExpressionLiteral doc comment clarity**. Reworded the note
  about backslash followers that are not recognised escape letters: the
  backslash is emitted as-is and the follower is processed by the next
  loop iteration via the default arm — byte-identical to passthrough
  but walked separately.

codex review:

- **Parser/visitor regression tests for @anchor(iterator: ..., tail: ...)**.
  visitor_anchor_test.go gains three tests that feed real MDL text
  through Build() and assert IteratorAnchor / BodyTailAnchor fields:
  loop with both iterator+tail, while with both, loop with iterator-only
  (tail stays nil). The existing executor-side tests only used
  synthetic AST values.

- **Clarify bug-test file wording**. The usage note said describe output
  "must include the @anchor(...) lines below verbatim" but the LOOP/WHILE
  section immediately below says those are parse-only. Restructured into
  two labeled sections: Section A = roundtrip-preserving (flat @anchor +
  IF split), Section B = parse-only forward-compatibility (LOOP/WHILE
  iterator/tail). Usage text now describes each section's guarantee
  explicitly.

Validation:

- go test ./... passes.
- go test -race ./mdl/executor/ passes (including the new concurrent test).
- go build -tags integration ./... builds clean.
- Go lint passes.
- On a fresh Mendix 11.9 project: mxcli exec of the updated bug-test
  script followed by mxcli docker check reports 0 errors.
- describe → exec → describe is bit-exact for Section A microflows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@hjotha hjotha force-pushed the submit/sequence-flow-anchor-annotation branch from 0279dde to 5fb2719 Compare April 23, 2026 15:12
@ako ako merged commit 0cad20d into mendixlabs:main Apr 23, 2026
2 checks passed
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.

3 participants