Skip to content

Commit b6afa4f

Browse files
hjothamendixclaude
andcommitted
fixup: loop @anchor follow-up + comment placement fix (PR #276)
Addresses the two minor comments from the #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>
1 parent 1f8864a commit b6afa4f

9 files changed

Lines changed: 825 additions & 333 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1414
- **ServiceUrl validation**`ServiceUrl` parameter must now be a constant reference (e.g., `@Module.ConstantName`) to enforce Mendix best practice
1515
- **Shared URL utilities**`internal/pathutil` package with `NormalizeURL()`, `URIToPath()`, and `PathFromURL()` for reuse across components
1616
- **@anchor sequence flow annotation** — optional `@anchor(from: X, to: Y)` annotation on microflow statements that pins the side of the activity box a SequenceFlow attaches to (top / right / bottom / left). Split (`if`) statements support the combined form `@anchor(to: X, true: (from: ..., to: ...), false: (from: ..., to: ...))` so the incoming and per-branch outgoing anchors survive describe → exec round-trip. When omitted, the builder derives the anchor from the visual flow direction (existing behaviour is unchanged). Keeps the flow diagram stable across patches when an agent-generated microflow is applied back to a project
17+
- **@anchor loop form**`LOOP`/`WHILE` statements accept `@anchor(iterator: (...), tail: (...))` in the grammar so authoring tools can forward-propagate the intent. Today the builder deliberately does not translate those into SequenceFlows: Studio Pro rejects edges between a `LoopedActivity` and its body statements with CE0709 ("Sequence flow is not accepted by origin or destination"), since the iterator icon is drawn implicitly from the loop geometry. Reserving the grammar slot keeps scripts forward-compatible with any future Mendix capability
1718

1819
### Changed
1920

mdl-examples/bug-tests/anchor-sequence-flow-annotation.mdl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
-- Syntax:
1515
-- @anchor(from: X, to: Y) — anchors for the single outgoing flow
1616
-- @anchor(true: (from:..., to:...), false: (from:..., to:...)) — IF
17+
-- @anchor(iterator: (from:..., to:...), tail: (from:..., to:...)) — LOOP/WHILE
1718
--
1819
-- Each side is independently optional. Missing sides fall back to the
1920
-- builder's default for the visual flow direction.
@@ -57,3 +58,39 @@ begin
5758
return $result;
5859
end;
5960
/
61+
62+
-- ============================================================================
63+
-- LOOP / WHILE body anchors
64+
-- ============================================================================
65+
-- The grammar accepts @anchor(iterator: (...), tail: (...)) on LOOP and
66+
-- WHILE statements so authoring tools can forward-propagate the intent, but
67+
-- the builder deliberately does NOT translate them into SequenceFlows.
68+
-- Mendix rejects edges between a LoopedActivity and its body statements with
69+
-- CE0709 "Sequence flow is not accepted by origin or destination" — the
70+
-- iterator icon is drawn implicitly from the LoopedActivity geometry.
71+
-- Keeping the grammar slot reserved means existing scripts continue to parse
72+
-- if a future Mendix version starts supporting these edges.
73+
74+
create entity BugTestAnchor.Item (
75+
Label: String(100)
76+
);
77+
78+
create microflow BugTestAnchor.MF_LoopAnchors ()
79+
begin
80+
retrieve $items from BugTestAnchor.Item;
81+
82+
@anchor(iterator: (from: bottom, to: top), tail: (from: right, to: bottom))
83+
loop $item in $items begin
84+
log info node 'App' 'step';
85+
end loop;
86+
end;
87+
/
88+
89+
create microflow BugTestAnchor.MF_WhileAnchors ()
90+
begin
91+
@anchor(iterator: (from: bottom, to: left), tail: (from: top, to: right))
92+
while true begin
93+
log info node 'App' 'tick';
94+
end while;
95+
end;
96+
/

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
377377
restServices: fb.restServices, // Share REST services for parameter classification
378378
}
379379

380-
// Process loop body statements and connect them with flows
380+
// Process loop body statements and connect them with flows.
381381
var lastBodyID model.ID
382382
for _, stmt := range s.Body {
383383
actID := loopBuilder.addStatement(stmt)
@@ -416,6 +416,13 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
416416
ErrorHandlingType: microflows.ErrorHandlingTypeRollback,
417417
}
418418

419+
// @anchor(iterator: ..., tail: ...) parses and survives on
420+
// savedLoopAnnotations for forward compatibility, but we deliberately do
421+
// not serialise either edge as a SequenceFlow: Studio Pro rejects loop→body
422+
// and body→loop with CE0709 "Sequence flow is not accepted by origin or
423+
// destination", since the iterator icon is drawn implicitly by the loop
424+
// geometry.
425+
419426
fb.objects = append(fb.objects, loop)
420427

421428
// Add the internal flows to the parent's flows (top-level), not inside loop
@@ -504,6 +511,10 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID {
504511
ErrorHandlingType: microflows.ErrorHandlingTypeRollback,
505512
}
506513

514+
// See addLoopStatement — @anchor(iterator/tail) is parsed but not
515+
// serialised, since Studio Pro does not permit explicit edges between a
516+
// LoopedActivity and its body statements.
517+
507518
fb.objects = append(fb.objects, loop)
508519
fb.flows = append(fb.flows, loopBuilder.flows...)
509520

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Tests for LOOP/WHILE internal @anchor handling.
4+
//
5+
// @anchor(iterator: ..., tail: ...) is accepted by the grammar so authors can
6+
// carry the intent forward for a future Mendix capability, but today the
7+
// builder deliberately does NOT emit SequenceFlows between a LoopedActivity
8+
// and its body statements: Studio Pro rejects those edges with CE0709
9+
// "Sequence flow is not accepted by origin or destination." These tests pin
10+
// that behaviour — the loop must round-trip without extra flows even when
11+
// the annotation is present.
12+
package executor
13+
14+
import (
15+
"testing"
16+
17+
"github.com/mendixlabs/mxcli/mdl/ast"
18+
"github.com/mendixlabs/mxcli/sdk/microflows"
19+
)
20+
21+
func TestBuilder_LoopIteratorAnchorIsParsedButNotSerialised(t *testing.T) {
22+
body := []ast.MicroflowStatement{
23+
&ast.LogStmt{
24+
Level: ast.LogInfo,
25+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "a"},
26+
},
27+
}
28+
stmts := []ast.MicroflowStatement{
29+
&ast.LoopStmt{
30+
ListVariable: "Items",
31+
LoopVariable: "Item",
32+
Body: body,
33+
Annotations: &ast.ActivityAnnotations{
34+
IteratorAnchor: &ast.FlowAnchors{From: ast.AnchorSideBottom, To: ast.AnchorSideTop},
35+
},
36+
},
37+
}
38+
39+
fb := &flowBuilder{
40+
posX: 100, posY: 100, spacing: HorizontalSpacing,
41+
varTypes: map[string]string{"Items": "List of MfTest.Item"},
42+
declaredVars: map[string]string{"Items": "List of MfTest.Item"},
43+
}
44+
oc := fb.buildFlowGraph(stmts, nil)
45+
46+
var loop *microflows.LoopedActivity
47+
for _, obj := range oc.Objects {
48+
if l, ok := obj.(*microflows.LoopedActivity); ok {
49+
loop = l
50+
break
51+
}
52+
}
53+
if loop == nil {
54+
t.Fatalf("expected a LoopedActivity in output objects")
55+
}
56+
firstID := loop.ObjectCollection.Objects[0].GetID()
57+
58+
for _, f := range oc.Flows {
59+
if f.OriginID == loop.ID && f.DestinationID == firstID {
60+
t.Errorf("unexpected iterator flow loop→firstBody: CE0709 would reject it")
61+
}
62+
if f.OriginID == firstID && f.DestinationID == loop.ID {
63+
t.Errorf("unexpected tail flow firstBody→loop: CE0709 would reject it")
64+
}
65+
}
66+
}
67+
68+
func TestBuilder_WhileIteratorAndTailAnchorIsParsedButNotSerialised(t *testing.T) {
69+
body := []ast.MicroflowStatement{
70+
&ast.LogStmt{
71+
Level: ast.LogInfo,
72+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "step"},
73+
},
74+
}
75+
stmts := []ast.MicroflowStatement{
76+
&ast.WhileStmt{
77+
Condition: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true},
78+
Body: body,
79+
Annotations: &ast.ActivityAnnotations{
80+
IteratorAnchor: &ast.FlowAnchors{From: ast.AnchorSideTop, To: ast.AnchorSideLeft},
81+
BodyTailAnchor: &ast.FlowAnchors{From: ast.AnchorSideRight, To: ast.AnchorSideBottom},
82+
},
83+
},
84+
}
85+
86+
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing}
87+
oc := fb.buildFlowGraph(stmts, nil)
88+
89+
var loop *microflows.LoopedActivity
90+
for _, obj := range oc.Objects {
91+
if l, ok := obj.(*microflows.LoopedActivity); ok {
92+
loop = l
93+
break
94+
}
95+
}
96+
if loop == nil {
97+
t.Fatalf("expected LoopedActivity (from while)")
98+
}
99+
firstID := loop.ObjectCollection.Objects[0].GetID()
100+
101+
for _, f := range oc.Flows {
102+
if f.OriginID == loop.ID && f.DestinationID == firstID {
103+
t.Errorf("unexpected iterator flow on while loop")
104+
}
105+
if f.OriginID == firstID && f.DestinationID == loop.ID {
106+
t.Errorf("unexpected tail flow on while loop")
107+
}
108+
}
109+
}
110+
111+
func TestBuilder_LoopWithoutLoopAnchorEmitsNoIteratorOrTail(t *testing.T) {
112+
body := []ast.MicroflowStatement{
113+
&ast.LogStmt{
114+
Level: ast.LogInfo,
115+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "a"},
116+
},
117+
}
118+
stmts := []ast.MicroflowStatement{
119+
&ast.LoopStmt{
120+
ListVariable: "Items",
121+
LoopVariable: "Item",
122+
Body: body,
123+
},
124+
}
125+
126+
fb := &flowBuilder{
127+
posX: 100, posY: 100, spacing: HorizontalSpacing,
128+
varTypes: map[string]string{"Items": "List of MfTest.Item"},
129+
declaredVars: map[string]string{"Items": "List of MfTest.Item"},
130+
}
131+
oc := fb.buildFlowGraph(stmts, nil)
132+
133+
var loop *microflows.LoopedActivity
134+
for _, obj := range oc.Objects {
135+
if l, ok := obj.(*microflows.LoopedActivity); ok {
136+
loop = l
137+
break
138+
}
139+
}
140+
if loop == nil {
141+
t.Fatalf("expected LoopedActivity")
142+
}
143+
firstID := loop.ObjectCollection.Objects[0].GetID()
144+
145+
// Baseline: no iterator/tail flows when no annotation is given.
146+
for _, f := range oc.Flows {
147+
if f.OriginID == loop.ID && f.DestinationID == firstID {
148+
t.Errorf("unexpected iterator flow emitted without @anchor(iterator: ...)")
149+
}
150+
if f.OriginID == firstID && f.DestinationID == loop.ID {
151+
t.Errorf("unexpected tail flow emitted without @anchor(tail: ...)")
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)