Skip to content
19 changes: 19 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,25 @@ end case;

`(empty)` represents an unset enumeration value. Multiple values can share one `when` branch by separating them with commas. Case values are bare identifiers — do **not** quote them.

### Type Split And Cast Statements

Use `split type` when a microflow branches on an object's runtime specialization.
Use `cast` inside a type branch to create the specialized variable used by the branch body.

```mdl
split type $Input
case Sample.SpecializedInput
cast $SpecificInput;
return true;
else
return false;
end split;
```

`case` values are qualified entity names. The optional `else` branch handles objects that do not match any listed specialization.

**`cast` only stores the output variable.** Studio Pro persists Microflows$CastAction with a single `VariableName` field — the source variable is implicit (the type-split's input). Use `cast $SpecificName;` to give the specialized variable its name. The two-variable form `$Output = cast $Source;` parses but `$Source` is dropped on roundtrip; prefer the single-variable form.

### LOOP Statements

```mdl
Expand Down
2 changes: 2 additions & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ authentication basic, session
| Free annotation | `@annotation 'text'` before `@position(...)` | Free-floating visual note preserved by order |
| IF | `if condition then ... [else ...] end if;` | |
| Enum split | `case $Var when Value then ... end case;` | Enumeration decision branches |
| Type split | `split type $Var case Module.Entity ... end split;` | Runtime specialization branches |
| Cast | `cast $SpecificVar;` | Downcast inside a type split branch |
| 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Proposal: Microflow Inheritance Split And Cast Statements

Status: Draft

## Summary

Add round-trip MDL support for type-based microflow decisions and cast actions:

```mdl
split type $Input
case Sample.SpecializedInput
cast $SpecificInput;
else
return false;
end split;
```

## Motivation

Studio Pro represents specialization/type decisions as `InheritanceSplit` objects and stores downcasts as `CastAction` activities. Without first-class MDL statements, `describe` can only emit unsupported comments or incomplete split output, and `exec` cannot rebuild the same graph.

## Semantics

`split type $Var` evaluates the runtime specialization of an object variable. Each `case Module.Entity` branch corresponds to an outgoing sequence flow with an `InheritanceCase`. The optional `else` branch maps to the outgoing flow without an inheritance case.

`cast $Output` emits a `CastAction` that produces the downcast variable. `$Output = cast $Input` is accepted for source-preserving authoring, but current Mendix BSON stores the generated cast variable as the primary persisted field.

## Tests And Examples

`mdl-examples/doctype-tests/inheritance_split_statement.test.mdl` demonstrates the syntax. Go regression tests cover parser construction, builder output, describer output, validation recursion, and BSON writer support for inheritance case values and cast actions.

## Open Questions

- Should `exec` validate `case Module.Entity` against the project's specialization hierarchy when connected?
- Should the source-preserving `$Output = cast $Input` form round-trip both variable names once the underlying BSON fields are confirmed for all supported Mendix versions?
1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ BSON schema Registry ◄──── multi-version Support
| [Microflow ENUM SPLIT Statement](PROPOSAL_microflow_enum_split_statement.md) | Implemented | Enumeration decision splits via `case $Var when Value then … end case;` | — |
| [Microflow CHANGE Refresh Modifier](PROPOSAL_microflow_change_refresh_modifier.md) | Draft | Preserve `RefreshInClient` on change-object actions | — |
| [Microflow ADD Expression To List](PROPOSAL_microflow_add_expression_to_list.md) | Draft | Preserve expression-valued list-add actions in microflow round-trips | — |
| [Microflow Inheritance Split And Cast Statements](PROPOSAL_microflow_inheritance_split_statement.md) | Draft | Preserve type-based microflow decisions and cast actions in 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-- ============================================================================
-- Bug #475: CE0079 on inheritance split when one branch continues
-- ============================================================================
--
-- Symptom (before fix):
-- Studio Pro `mx check` reports CE0079 "Activity has no outgoing flow"
-- on the body of a terminating branch in a `split type` whose other
-- branch continues to a follow-up activity. mxcli's
-- `addStructuredInheritanceSplit` took a "no-merge shortcut" when
-- exactly one non-split branch continued: it wired the parent's next
-- statement directly to the continuing branch's tail and skipped the
-- ExclusiveMerge. The terminating branch's tail then had nowhere to
-- converge — Studio Pro flagged its outgoing-flow gap as CE0079.
-- On re-describe the parent's follow-up activity also leaked inside
-- the case body, breaking subsequent roundtrips.
--
-- After fix:
-- `addStructuredInheritanceSplit` always emits an ExclusiveMerge when
-- any branch continues, mirroring the invariant `addEnumSplit`
-- already enforces. Both terminating and continuing branches converge
-- on the merge; the parent's next statement attaches after the merge.
--
-- Validation:
-- `mxcli check` parses the script.
-- `mx check` against the resulting MPR reports 0 errors.
-- Roundtrip (describe → exec → describe) preserves the structure
-- byte-for-byte: the post-split log activity stays outside both case
-- bodies.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/475-inheritance-split-continuing-branch-merge.mdl -p app.mpr
-- mxcli -p app.mpr -c "describe microflow BugTest475.MF_TypedDispatch"
-- ============================================================================

create module BugTest475;

create persistent entity BugTest475.Vehicle (
Plate : string
);
/

create persistent entity BugTest475.Car
extends BugTest475.Vehicle (
Wheels : integer
);
/

create persistent entity BugTest475.Boat
extends BugTest475.Vehicle (
HullLength : decimal
);
/

-- One case continues into the post-split log activity; the other case
-- terminates with a `return`. Before the fix this combination tripped
-- CE0079 on the boat case. After the fix the merge converges both tails
-- and the log activity attaches after the merge.
create microflow BugTest475.MF_TypedDispatch (
$obj : BugTest475.Vehicle
)
returns boolean
begin
split type $obj
case BugTest475.Car
log info node 'BugTest475' 'Dispatching car';
case BugTest475.Boat
log info node 'BugTest475' 'Dispatching boat';
return false;
end split;
log info node 'BugTest475' 'Dispatched';
return true;
end;
/
26 changes: 26 additions & 0 deletions mdl-examples/doctype-tests/inheritance_split_statement.test.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
create module InheritanceSplitExample;

create persistent entity InheritanceSplitExample.BaseInput (
Name: String(200)
);
/

create persistent entity InheritanceSplitExample.SpecializedInput extends InheritanceSplitExample.BaseInput (
Code: String(50)
);
/

create microflow InheritanceSplitExample.RouteInput (
$Input: InheritanceSplitExample.BaseInput
)
returns boolean
begin
split type $Input
case InheritanceSplitExample.SpecializedInput
cast $SpecializedInput;
return true;
else
return false;
end split;
end;
/
24 changes: 24 additions & 0 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,31 @@ type EnumSplitStmt struct {
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

// InheritanceSplitCase represents one typed branch in an inheritance split.
type InheritanceSplitCase struct {
Entity QualifiedName
Body []MicroflowStatement
}

// InheritanceSplitStmt represents: SPLIT TYPE $Var ... END SPLIT
type InheritanceSplitStmt struct {
Variable string // Variable name without $ prefix
Cases []InheritanceSplitCase
ElseBody []MicroflowStatement
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

func (s *EnumSplitStmt) isMicroflowStatement() {}
func (s *InheritanceSplitStmt) isMicroflowStatement() {}

// CastObjectStmt represents: $Output = CAST $Object
type CastObjectStmt struct {
OutputVariable string // Output variable name without $ prefix
ObjectVariable string // Source object variable name without $ prefix
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

func (s *CastObjectStmt) isMicroflowStatement() {}

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

case *ast.InheritanceSplitStmt:
lines = append(lines, fmt.Sprintf("%ssplit type $%s", indentStr, s.Variable))
for _, c := range s.Cases {
lines = append(lines, fmt.Sprintf("%scase %s", indentStr, c.Entity.String()))
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.CastObjectStmt:
if s.ObjectVariable == "" {
lines = append(lines, fmt.Sprintf("%scast $%s;", indentStr, s.OutputVariable))
} else {
lines = append(lines, fmt.Sprintf("%s$%s = cast $%s;", indentStr, s.OutputVariable, s.ObjectVariable))
}

case *ast.LoopStmt:
lines = append(lines, fmt.Sprintf("%sloop $%s in $%s", indentStr, s.LoopVariable, s.ListVariable))
for _, bodyStmt := range s.Body {
Expand Down
Loading
Loading