Skip to content

Commit 57f7487

Browse files
committed
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # docs/11-proposals/README.md # mdl/grammar/parser/MDLParser.interp # mdl/grammar/parser/mdl_parser.go
2 parents 1abd9bc + e3bf4d2 commit 57f7487

28 files changed

Lines changed: 12377 additions & 10132 deletions

.claude/skills/mendix/write-microflows.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,23 @@ if $entity/status = empty then
323323
end if;
324324
```
325325

326+
### ENUM SPLIT Statements
327+
328+
Use `split enum` when a microflow branches on an enumeration value.
329+
330+
```mdl
331+
split enum $Status
332+
case Open, Pending
333+
return true;
334+
case (empty)
335+
return false;
336+
else
337+
return false;
338+
end split;
339+
```
340+
341+
`(empty)` represents an unset enumeration value. Multiple values can share one branch by separating them with commas.
342+
326343
### LOOP Statements
327344

328345
```mdl

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ authentication basic, session
242242
| Annotation | `@annotation 'text'` | Visual note attached to next activity |
243243
| Free annotation | `@annotation 'text'` before `@position(...)` | Free-floating visual note preserved by order |
244244
| IF | `if condition then ... [else ...] end if;` | |
245+
| Enum split | `split enum $Var case Value ... end split;` | Enumeration decision branches |
245246
| LOOP | `loop $item in $list begin ... end loop;` | FOR EACH over list |
246247
| WHILE | `while condition begin ... end while;` | Condition-based loop |
247248
| Return | `return $value;` | Required at end of every flow path |
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Proposal: Microflow ENUM SPLIT Statement
2+
3+
Status: Draft
4+
5+
## Summary
6+
7+
Add round-trip MDL support for enumeration decisions:
8+
9+
```mdl
10+
split enum $Status
11+
case Open, Pending
12+
return true;
13+
case (empty)
14+
return false;
15+
else
16+
return false;
17+
end split;
18+
```
19+
20+
## Motivation
21+
22+
Studio Pro represents enumeration decisions as exclusive splits whose outgoing sequence flows carry enumeration case values. Without a first-class MDL statement, describe/exec round-trips collapse those structures into boolean-looking decisions or unsupported comments.
23+
24+
## Semantics
25+
26+
`split enum` evaluates an enumeration variable or attribute path. Each `case` lists one or more enumeration values that enter the same branch. `(empty)` represents the Mendix empty enumeration case. `else` is optional and maps to the outgoing flow without an explicit case value.
27+
28+
## Tests And Examples
29+
30+
`mdl-examples/doctype-tests/enum_split_statement.test.mdl` demonstrates parser syntax. Go regression tests cover AST parsing, builder generation of enumeration case flows, and describer output for existing split graphs.
31+
32+
## Open Questions
33+
34+
- Should the builder validate case values against the referenced enumeration when backend metadata is available?
35+
- Should enum value names be emitted fully qualified in ambiguous cross-module cases?

docs/11-proposals/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ BSON schema Registry ◄──── multi-version Support
5151
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
5252
| [XPath Gaps](xpath-gaps-proposal.md) | Partial | XPath constraint support gap analysis. ~85% complete, association paths and nested predicates remain ||
5353
| [Microflow ADD Expression To List](PROPOSAL_microflow_add_expression_to_list.md) | Draft | Preserve expression-valued list-add actions in microflow round-trips ||
54+
| [Microflow ENUM SPLIT Statement](PROPOSAL_microflow_enum_split_statement.md) | Draft | Preserve enumeration decision splits in microflow round-trips ||
5455
| [Microflow CHANGE Refresh Modifier](PROPOSAL_microflow_change_refresh_modifier.md) | Draft | Preserve `RefreshInClient` on change-object actions ||
5556
| [LLM MDL Assistance](PROPOSAL_llm_mdl_assistance.md) | Proposed | Enhanced error messages with examples, reorganized skills by use case ||
5657

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
create module EnumSplitExample;
2+
3+
create enumeration EnumSplitExample.Status (
4+
Open,
5+
Pending,
6+
Closed
7+
);
8+
/
9+
10+
create microflow EnumSplitExample.RouteStatus (
11+
$Status: enum EnumSplitExample.Status
12+
)
13+
returns boolean
14+
begin
15+
split enum $Status
16+
case Open, Pending
17+
return true;
18+
case Closed
19+
return false;
20+
else
21+
return false;
22+
end split;
23+
end;
24+
/

mdl/ast/ast_microflow.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ type DeclareStmt struct {
101101

102102
func (s *DeclareStmt) isMicroflowStatement() {}
103103

104+
// EnumSplitCase represents one enumeration branch in an EnumSplit.
105+
type EnumSplitCase struct {
106+
Value string // First enumeration value, or "(empty)" for Mendix's empty enum case.
107+
Values []string
108+
Body []MicroflowStatement
109+
}
110+
111+
// EnumSplitStmt represents: SPLIT ENUM $Var ... END SPLIT
112+
type EnumSplitStmt struct {
113+
Variable string // Variable or attribute path without $ prefix (e.g. EventType or Event/EventType)
114+
Cases []EnumSplitCase
115+
ElseBody []MicroflowStatement
116+
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
117+
}
118+
119+
func (s *EnumSplitStmt) isMicroflowStatement() {}
120+
104121
// MfSetStmt represents: SET $Var = expr or SET $Var/Attr = expr
105122
// (Named MfSetStmt to avoid conflict with existing SetStmt for SET key = value)
106123
type MfSetStmt struct {

mdl/executor/cmd_diff_mdl.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,22 @@ func microflowStatementToMDL(ctx *ExecContext, stmt ast.MicroflowStatement, inde
431431
}
432432
lines = append(lines, indentStr+"end if;")
433433

434+
case *ast.EnumSplitStmt:
435+
lines = append(lines, fmt.Sprintf("%ssplit enum $%s", indentStr, s.Variable))
436+
for _, c := range s.Cases {
437+
lines = append(lines, fmt.Sprintf("%scase %s", indentStr, formatEnumSplitCaseValues(enumSplitCaseValues(c))))
438+
for _, caseStmt := range c.Body {
439+
lines = append(lines, microflowStatementToMDL(ctx, caseStmt, indent+1)...)
440+
}
441+
}
442+
if len(s.ElseBody) > 0 {
443+
lines = append(lines, indentStr+"else")
444+
for _, elseStmt := range s.ElseBody {
445+
lines = append(lines, microflowStatementToMDL(ctx, elseStmt, indent+1)...)
446+
}
447+
}
448+
lines = append(lines, indentStr+"end split;")
449+
434450
case *ast.LoopStmt:
435451
lines = append(lines, fmt.Sprintf("%sloop $%s in $%s", indentStr, s.LoopVariable, s.ListVariable))
436452
for _, bodyStmt := range s.Body {

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,213 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID {
282282
return activity.ID
283283
}
284284

285+
func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
286+
if fb.measurer == nil {
287+
fb.measurer = &layoutMeasurer{varTypes: fb.varTypes}
288+
}
289+
290+
splitX := fb.posX
291+
centerY := fb.posY
292+
split := &microflows.ExclusiveSplit{
293+
BaseMicroflowObject: microflows.BaseMicroflowObject{
294+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
295+
Position: model.Point{X: splitX, Y: centerY},
296+
Size: model.Size{Width: SplitWidth, Height: SplitHeight},
297+
},
298+
Caption: "$" + s.Variable,
299+
SplitCondition: &microflows.ExpressionSplitCondition{
300+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
301+
Expression: "$" + s.Variable,
302+
},
303+
ErrorHandlingType: microflows.ErrorHandlingTypeRollback,
304+
}
305+
fb.objects = append(fb.objects, split)
306+
splitID := split.ID
307+
if fb.pendingAnnotations != nil {
308+
fb.applyAnnotations(splitID, fb.pendingAnnotations)
309+
fb.pendingAnnotations = nil
310+
}
311+
312+
type branch struct {
313+
values []string
314+
body []ast.MicroflowStatement
315+
}
316+
branches := make([]branch, 0, len(s.Cases)+1)
317+
for _, c := range s.Cases {
318+
branches = append(branches, branch{values: enumSplitCaseValues(c), body: c.Body})
319+
}
320+
if len(s.ElseBody) > 0 {
321+
branches = append(branches, branch{body: s.ElseBody})
322+
}
323+
324+
branchWidth := fb.measurer.measureStatements(appendEnumBodies(s)).Width
325+
if branchWidth == 0 {
326+
branchWidth = HorizontalSpacing / 2
327+
}
328+
mergeX := splitX + SplitWidth + HorizontalSpacing/2 + branchWidth + HorizontalSpacing/2
329+
var merge *microflows.ExclusiveMerge
330+
ensureMerge := func() *microflows.ExclusiveMerge {
331+
if merge == nil {
332+
merge = &microflows.ExclusiveMerge{
333+
BaseMicroflowObject: microflows.BaseMicroflowObject{
334+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
335+
Position: model.Point{X: mergeX, Y: centerY},
336+
Size: model.Size{Width: MergeSize, Height: MergeSize},
337+
},
338+
}
339+
fb.objects = append(fb.objects, merge)
340+
}
341+
return merge
342+
}
343+
344+
savedEndsWithReturn := fb.endsWithReturn
345+
allBranchesReturn := len(branches) > 0
346+
for i, br := range branches {
347+
branchY := centerY + i*VerticalSpacing
348+
fb.posX = splitX + SplitWidth + HorizontalSpacing/2
349+
fb.posY = branchY
350+
fb.endsWithReturn = false
351+
352+
lastID := model.ID("")
353+
pendingCase := ""
354+
for _, stmt := range br.body {
355+
actID := fb.addStatement(stmt)
356+
if actID == "" {
357+
continue
358+
}
359+
if fb.pendingAnnotations != nil {
360+
fb.applyAnnotations(actID, fb.pendingAnnotations)
361+
fb.pendingAnnotations = nil
362+
}
363+
if lastID == "" {
364+
fb.addGroupedEnumSplitFlows(splitID, actID, br.values, i, splitX+SplitWidth+HorizontalSpacing/4, branchY)
365+
} else {
366+
if pendingCase != "" {
367+
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, actID, pendingCase))
368+
pendingCase = ""
369+
} else {
370+
fb.flows = append(fb.flows, newHorizontalFlow(lastID, actID))
371+
}
372+
}
373+
if fb.nextConnectionPoint != "" {
374+
lastID = fb.nextConnectionPoint
375+
fb.nextConnectionPoint = ""
376+
pendingCase = fb.nextFlowCase
377+
fb.nextFlowCase = ""
378+
} else {
379+
lastID = actID
380+
}
381+
}
382+
383+
if lastStmtIsReturn(br.body) {
384+
continue
385+
}
386+
allBranchesReturn = false
387+
if lastID == "" {
388+
fb.addGroupedEnumSplitFlows(splitID, ensureMerge().ID, br.values, i, splitX+SplitWidth+HorizontalSpacing/4, branchY)
389+
} else {
390+
if pendingCase != "" {
391+
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, ensureMerge().ID, pendingCase))
392+
} else {
393+
fb.flows = append(fb.flows, newHorizontalFlow(lastID, ensureMerge().ID))
394+
}
395+
}
396+
}
397+
398+
fb.posX = mergeX + HorizontalSpacing/2
399+
fb.posY = centerY
400+
fb.endsWithReturn = savedEndsWithReturn
401+
if allBranchesReturn {
402+
fb.endsWithReturn = true
403+
} else {
404+
fb.nextConnectionPoint = ensureMerge().ID
405+
}
406+
return splitID
407+
}
408+
409+
func (fb *flowBuilder) addGroupedEnumSplitFlows(originID, destinationID model.ID, values []string, order int, mergeX, mergeY int) {
410+
if len(values) <= 1 {
411+
fb.addEnumSplitFlows(originID, destinationID, values, order)
412+
return
413+
}
414+
branchMerge := &microflows.ExclusiveMerge{
415+
BaseMicroflowObject: microflows.BaseMicroflowObject{
416+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
417+
Position: model.Point{X: mergeX, Y: mergeY},
418+
Size: model.Size{Width: MergeSize, Height: MergeSize},
419+
},
420+
}
421+
fb.objects = append(fb.objects, branchMerge)
422+
fb.addEnumSplitFlows(originID, branchMerge.ID, values, order)
423+
fb.flows = append(fb.flows, newHorizontalFlow(branchMerge.ID, destinationID))
424+
}
425+
426+
func (fb *flowBuilder) addEnumSplitFlows(originID, destinationID model.ID, values []string, order int) {
427+
if len(values) == 0 {
428+
flow := newHorizontalFlow(originID, destinationID)
429+
applySplitCaseOrder(flow, order)
430+
fb.flows = append(fb.flows, flow)
431+
return
432+
}
433+
for _, value := range values {
434+
flow := newHorizontalFlowWithEnumCase(originID, destinationID, value)
435+
applySplitCaseOrder(flow, order)
436+
fb.flows = append(fb.flows, flow)
437+
}
438+
}
439+
440+
type splitCaseOrderAnchor struct {
441+
origin int
442+
destination int
443+
}
444+
445+
var splitCaseOrderAnchors = []splitCaseOrderAnchor{
446+
{AnchorTop, AnchorLeft},
447+
{AnchorRight, AnchorLeft},
448+
{AnchorBottom, AnchorLeft},
449+
{AnchorLeft, AnchorLeft},
450+
{AnchorTop, AnchorTop},
451+
{AnchorRight, AnchorTop},
452+
{AnchorBottom, AnchorTop},
453+
{AnchorLeft, AnchorTop},
454+
{AnchorTop, AnchorRight},
455+
{AnchorRight, AnchorRight},
456+
{AnchorBottom, AnchorRight},
457+
{AnchorLeft, AnchorRight},
458+
{AnchorTop, AnchorBottom},
459+
{AnchorRight, AnchorBottom},
460+
{AnchorBottom, AnchorBottom},
461+
{AnchorLeft, AnchorBottom},
462+
}
463+
464+
func applySplitCaseOrder(flow *microflows.SequenceFlow, order int) {
465+
if flow == nil || order < 0 || order >= len(splitCaseOrderAnchors) {
466+
return
467+
}
468+
pair := splitCaseOrderAnchors[order]
469+
flow.OriginConnectionIndex = pair.origin
470+
flow.DestinationConnectionIndex = pair.destination
471+
}
472+
473+
func enumSplitCaseValues(c ast.EnumSplitCase) []string {
474+
if len(c.Values) > 0 {
475+
return append([]string(nil), c.Values...)
476+
}
477+
if c.Value != "" {
478+
return []string{c.Value}
479+
}
480+
return nil
481+
}
482+
483+
func appendEnumBodies(s *ast.EnumSplitStmt) []ast.MicroflowStatement {
484+
var stmts []ast.MicroflowStatement
485+
for _, c := range s.Cases {
486+
stmts = append(stmts, c.Body...)
487+
}
488+
stmts = append(stmts, s.ElseBody...)
489+
return stmts
490+
}
491+
285492
// addRetrieveAction creates a RETRIEVE statement.
286493
func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
287494
var source microflows.RetrieveSource

mdl/executor/cmd_microflows_builder_annotations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
3535
return s.Annotations
3636
case *ast.IfStmt:
3737
return s.Annotations
38+
case *ast.EnumSplitStmt:
39+
return s.Annotations
3840
case *ast.LoopStmt:
3941
return s.Annotations
4042
case *ast.WhileStmt:

0 commit comments

Comments
 (0)