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
17 changes: 17 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,23 @@ if $entity/status = empty then
end if;
```

### ENUM SPLIT Statements

Use `split enum` when a microflow branches on an enumeration value.

```mdl
split enum $Status
case Open, Pending
return true;
case (empty)
return false;
else
return false;
end split;
```

`(empty)` represents an unset enumeration value. Multiple values can share one branch by separating them with commas.

### LOOP Statements

```mdl
Expand Down
1 change: 1 addition & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ authentication basic, session
| Color | `@color Green` | Background color (before activity) |
| Annotation | `@annotation 'text'` | Visual note attached to next activity |
| IF | `if condition then ... [else ...] end if;` | |
| Enum split | `split enum $Var case Value ... end split;` | Enumeration decision branches |
| LOOP | `loop $item in $list begin ... end loop;` | FOR EACH over list |
| WHILE | `while condition begin ... end while;` | Condition-based loop |
| Return | `return $value;` | Required at end of every flow path |
Expand Down
35 changes: 35 additions & 0 deletions docs/11-proposals/PROPOSAL_microflow_enum_split_statement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Proposal: Microflow ENUM SPLIT Statement

Status: Draft

## Summary

Add round-trip MDL support for enumeration decisions:

```mdl
split enum $Status
case Open, Pending
return true;
case (empty)
return false;
else
return false;
end split;
```

## Motivation

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.

## Semantics

`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.

## Tests And Examples

`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.

## Open Questions

- Should the builder validate case values against the referenced enumeration when backend metadata is available?
- Should enum value names be emitted fully qualified in ambiguous cross-module cases?
1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ BSON schema Registry ◄──── multi-version Support
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done | — |
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
| [XPath Gaps](xpath-gaps-proposal.md) | Partial | XPath constraint support gap analysis. ~85% complete, association paths and nested predicates remain | — |
| [Microflow ENUM SPLIT Statement](PROPOSAL_microflow_enum_split_statement.md) | Draft | Preserve enumeration decision splits in microflow round-trips | — |
| [LLM MDL Assistance](PROPOSAL_llm_mdl_assistance.md) | Proposed | Enhanced error messages with examples, reorganized skills by use case | — |

### Testing & Evaluation
Expand Down
24 changes: 24 additions & 0 deletions mdl-examples/doctype-tests/enum_split_statement.test.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
create module EnumSplitExample;

create enumeration EnumSplitExample.Status (
Open,
Pending,
Closed
);
/

create microflow EnumSplitExample.RouteStatus (
$Status: enum EnumSplitExample.Status
)
returns boolean
begin
split enum $Status
case Open, Pending
return true;
case Closed
return false;
else
return false;
end split;
end;
/
17 changes: 17 additions & 0 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ type DeclareStmt struct {

func (s *DeclareStmt) isMicroflowStatement() {}

// EnumSplitCase represents one enumeration branch in an EnumSplit.
type EnumSplitCase struct {
Value string // First enumeration value, or "(empty)" for Mendix's empty enum case.
Values []string
Body []MicroflowStatement
}

// EnumSplitStmt represents: SPLIT ENUM $Var ... END SPLIT
type EnumSplitStmt struct {
Variable string // Variable or attribute path without $ prefix (e.g. EventType or Event/EventType)
Cases []EnumSplitCase
ElseBody []MicroflowStatement
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

func (s *EnumSplitStmt) isMicroflowStatement() {}

// MfSetStmt represents: SET $Var = expr or SET $Var/Attr = expr
// (Named MfSetStmt to avoid conflict with existing SetStmt for SET key = value)
type MfSetStmt struct {
Expand Down
16 changes: 16 additions & 0 deletions mdl/executor/cmd_diff_mdl.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,22 @@ func microflowStatementToMDL(ctx *ExecContext, stmt ast.MicroflowStatement, inde
}
lines = append(lines, indentStr+"end if;")

case *ast.EnumSplitStmt:
lines = append(lines, fmt.Sprintf("%ssplit enum $%s", indentStr, s.Variable))
for _, c := range s.Cases {
lines = append(lines, fmt.Sprintf("%scase %s", indentStr, formatEnumSplitCaseValues(enumSplitCaseValues(c))))
for _, caseStmt := range c.Body {
lines = append(lines, microflowStatementToMDL(ctx, caseStmt, indent+1)...)
}
}
if len(s.ElseBody) > 0 {
lines = append(lines, indentStr+"else")
for _, elseStmt := range s.ElseBody {
lines = append(lines, microflowStatementToMDL(ctx, elseStmt, indent+1)...)
}
}
lines = append(lines, indentStr+"end split;")

case *ast.LoopStmt:
lines = append(lines, fmt.Sprintf("%sloop $%s in $%s", indentStr, s.LoopVariable, s.ListVariable))
for _, bodyStmt := range s.Body {
Expand Down
147 changes: 147 additions & 0 deletions mdl/executor/cmd_microflows_builder_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,153 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID {
return activity.ID
}

func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
if fb.measurer == nil {
fb.measurer = &layoutMeasurer{varTypes: fb.varTypes}
}

splitX := fb.posX
centerY := fb.posY
split := &microflows.ExclusiveSplit{
BaseMicroflowObject: microflows.BaseMicroflowObject{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Position: model.Point{X: splitX, Y: centerY},
Size: model.Size{Width: SplitWidth, Height: SplitHeight},
},
Caption: "$" + s.Variable,
SplitCondition: &microflows.ExpressionSplitCondition{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Expression: "$" + s.Variable,
},
ErrorHandlingType: microflows.ErrorHandlingTypeRollback,
}
fb.objects = append(fb.objects, split)
splitID := split.ID
if fb.pendingAnnotations != nil {
fb.applyAnnotations(splitID, fb.pendingAnnotations)
fb.pendingAnnotations = nil
}

type branch struct {
values []string
body []ast.MicroflowStatement
}
branches := make([]branch, 0, len(s.Cases)+1)
for _, c := range s.Cases {
branches = append(branches, branch{values: enumSplitCaseValues(c), body: c.Body})
}
if len(s.ElseBody) > 0 {
branches = append(branches, branch{body: s.ElseBody})
}

branchWidth := fb.measurer.measureStatements(appendEnumBodies(s)).Width
if branchWidth == 0 {
branchWidth = HorizontalSpacing / 2
}
mergeX := splitX + SplitWidth + HorizontalSpacing/2 + branchWidth + HorizontalSpacing/2
merge := &microflows.ExclusiveMerge{
BaseMicroflowObject: microflows.BaseMicroflowObject{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Position: model.Point{X: mergeX, Y: centerY},
Size: model.Size{Width: MergeSize, Height: MergeSize},
},
}
fb.objects = append(fb.objects, merge)

savedEndsWithReturn := fb.endsWithReturn
allBranchesReturn := len(branches) > 0
for i, br := range branches {
branchY := centerY + i*VerticalSpacing
fb.posX = splitX + SplitWidth + HorizontalSpacing/2
fb.posY = branchY
fb.endsWithReturn = false

lastID := model.ID("")
pendingCase := ""
for _, stmt := range br.body {
actID := fb.addStatement(stmt)
if actID == "" {
continue
}
if fb.pendingAnnotations != nil {
fb.applyAnnotations(actID, fb.pendingAnnotations)
fb.pendingAnnotations = nil
}
if lastID == "" {
fb.addEnumSplitFlows(splitID, actID, br.values)
} else {
if pendingCase != "" {
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, actID, pendingCase))
pendingCase = ""
} else {
fb.flows = append(fb.flows, newHorizontalFlow(lastID, actID))
}
}
if fb.nextConnectionPoint != "" {
lastID = fb.nextConnectionPoint
fb.nextConnectionPoint = ""
pendingCase = fb.nextFlowCase
fb.nextFlowCase = ""
} else {
lastID = actID
}
}

if lastStmtIsReturn(br.body) {
continue
}
allBranchesReturn = false
if lastID == "" {
fb.addEnumSplitFlows(splitID, merge.ID, br.values)
} else {
if pendingCase != "" {
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, merge.ID, pendingCase))
} else {
fb.flows = append(fb.flows, newHorizontalFlow(lastID, merge.ID))
}
}
}

fb.posX = mergeX + HorizontalSpacing/2
fb.posY = centerY
fb.endsWithReturn = savedEndsWithReturn
if allBranchesReturn {
fb.endsWithReturn = true
} else {
fb.nextConnectionPoint = merge.ID
}
return splitID
}

func (fb *flowBuilder) addEnumSplitFlows(originID, destinationID model.ID, values []string) {
if len(values) == 0 {
fb.flows = append(fb.flows, newHorizontalFlow(originID, destinationID))
return
}
for _, value := range values {
fb.flows = append(fb.flows, newHorizontalFlowWithEnumCase(originID, destinationID, value))
}
}

func enumSplitCaseValues(c ast.EnumSplitCase) []string {
if len(c.Values) > 0 {
return append([]string(nil), c.Values...)
}
if c.Value != "" {
return []string{c.Value}
}
return nil
}

func appendEnumBodies(s *ast.EnumSplitStmt) []ast.MicroflowStatement {
var stmts []ast.MicroflowStatement
for _, c := range s.Cases {
stmts = append(stmts, c.Body...)
}
stmts = append(stmts, s.ElseBody...)
return stmts
}

// addRetrieveAction creates a RETRIEVE statement.
func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
var source microflows.RetrieveSource
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_builder_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
return s.Annotations
case *ast.IfStmt:
return s.Annotations
case *ast.EnumSplitStmt:
return s.Annotations
case *ast.LoopStmt:
return s.Annotations
case *ast.WhileStmt:
Expand Down
Loading
Loading