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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-16", "symptom": "`create microflow M.F () returns M.Enum as $V` (also nanoflow and rule) passed `check -p --references` and was then refused by exec with \"entity 'M.Enum' not found for return type\"; for a rule check refused it too, with a misleading \"a rule must return a Boolean or an enumeration, not Entity\". `returns enum M.Enum`, `Enumeration(M.Enum)` and a bare enum name as a PARAMETER type all worked", "cause": "the parser records a bare Module.Name as an entity reference; the create handlers retry it as an enumeration for parameters but their return-type block only looked for an entity, while declaredTypeResolver.check was written against the general rule (entity or enumeration) and validateRule saw the unretyped entity kind", "file": "`mdl/executor/cmd_microflows_build.go` (microflow and nanoflow build), `mdl/executor/cmd_rules_create.go`, `bareEnumerationType` in `cmd_microflows_helpers.go`, `declaredTypeResolver.bareEnumeration` and the rule case in `validate.go`", "insight": "A fallback added for one declaration site (parameters) but not its sibling (return types) is a check/exec disagreement, because the checker was written against the general rule, not the site. When a handler resolves a name in one place, grep for the same lookup at every sibling site before trusting a green `check -p`; and a check-time validator that reads the AST kind (validateRule: Boolean or enumeration) needs the same retyping exec applies, or it refuses what exec now accepts. Settled by a PedApp assertAgree test per kind plus a stored-BSON comparison against the `enum` spelling, and mx check 11.12.5 at 0 errors. The same ambiguity is still open for a Java action's bare enum parameter and return type, which stores an entity type (CE1613)", "refs": ["mendixlabs/mxcli#1562"]}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **A bare `returns Module.Name` that names an enumeration is accepted by `exec`, as `check` already said** (mendixlabs/mxcli#1562) — `create microflow M.F () returns M.Variant as $V` (and the nanoflow and rule forms) passed `check -p --references` and was then refused by `exec` with "entity 'M.Variant' not found for return type". The parser cannot tell an entity from an enumeration for a bare qualified name; `exec` resolved that for parameters (the entity first, then an enumeration) but not for return types. All three kinds now do, and store exactly what `returns enum M.Variant` stores. A rule's `check` refused the bare form too, with a misleading "a rule must return a Boolean or an enumeration, not Entity". A name that is both an entity and an enumeration stays the entity, as for a parameter, and a name that is neither now says "entity or enumeration" in `check` and in `exec`, for return types and parameters alike, where it used to say only "entity". `returns enum M.Variant` and `Enumeration(M.Variant)` always worked and are what `describe` prints.

## [0.27.0] - 2026-10-15

Headline: **`exec` is all or nothing: when a statement fails at write time, the whole run is rolled back, so a script never leaves a project half-changed.** Alongside it: a wave of `describe` → `check` → `exec` fixes from round-trip runs over real apps (the Siemens FSM template, the GenAI showcase, Marketplace modules), Mendix 9 projects can take microflows, nanoflows and pluggable widgets again, `check -p` resolves many more of the references `exec` and the build would refuse, the classic `filemanager` widget and role-based `Visible: roles (…)` are in MDL, and the companion draws a module's domain model. The MongoDB driver is upgraded for the October 2026 BSON advisories.
Expand Down
11 changes: 11 additions & 0 deletions docs-site/src/language/microflow-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ RETURN $Order;
RETURN $FilteredOrders;
```

### Returning an Enumeration

Declare the type in the signature with `returns enum Module.Name`; `returns Enumeration(Module.Name)` is the same. A bare `returns Module.Name` is also accepted when no entity has that name and an enumeration does. If an entity and an enumeration share the name, the entity wins, so write `enum` when a name could be either. Nanoflows and rules take the same spellings, and a rule must return a Boolean or an enumeration.

```sql
CREATE MICROFLOW Shop.SUB_Pick () RETURNS enum Shop.Variant AS $Variant
BEGIN
RETURN Shop.Variant.Original;
END;
```

### Returning Nothing

If the microflow returns nothing (void), you can omit the `RETURN` or use:
Expand Down
72 changes: 72 additions & 0 deletions mdl-examples/bug-tests/1562-enum-return-type.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
mdl 1;
-- ============================================================================
-- A bare `returns Module.Name` that names an enumeration
-- ============================================================================
--
-- POSITIVE TEST: `mxcli check` (with and without -p --references) and `exec`
-- MUST accept it.
--
-- The parser cannot tell an entity from an enumeration for a bare qualified
-- name, so it records an entity reference. `exec` resolved that for PARAMETERS
-- (entity first, then enumeration) but not for RETURN TYPES, while `check`
-- resolved both the same way: a script `check` passed failed at exec with
-- "entity 'Shop.Variant' not found for return type". For a rule, check refused
-- it too, with a misleading "a rule must return a Boolean or an enumeration,
-- not Entity".
--
-- A microflow, a nanoflow and a rule now accept the bare name, and store exactly
-- what `returns enum Module.Name` stores. A name that is both an entity and an
-- enumeration stays the entity, as it does for a parameter.
--
-- Measured on mxbuild 11.12.5: this script execs and the app checks at 0 errors.

CREATE MODULE EnumReturn;

CREATE OR MODIFY ENUMERATION EnumReturn.Variant (
Original 'Original',
Released 'Released',
None 'None'
);

CREATE OR MODIFY PERSISTENT ENTITY EnumReturn.Customer (
"Name": String(50)
);

-- The bare name, as a parameter (already worked) and as a return type.
CREATE OR MODIFY MICROFLOW EnumReturn.SUB_Pick ($Current: EnumReturn.Variant)
RETURNS EnumReturn.Variant AS $Variant
BEGIN
RETURN EnumReturn.Variant.Original;
END;

CREATE OR MODIFY NANOFLOW EnumReturn.NF_Pick ()
RETURNS EnumReturn.Variant AS $Variant
BEGIN
RETURN EnumReturn.Variant.Released;
END;

CREATE OR MODIFY RULE EnumReturn.RL_Pick ($Customer: EnumReturn.Customer)
RETURNS EnumReturn.Variant AS $Variant
BEGIN
RETURN EnumReturn.Variant.None;
END;

-- The explicit spellings, which always worked, store the same thing.
CREATE OR MODIFY MICROFLOW EnumReturn.SUB_PickExplicit ()
RETURNS enum EnumReturn.Variant AS $Variant
BEGIN
RETURN EnumReturn.Variant.Original;
END;

CREATE OR MODIFY MICROFLOW EnumReturn.SUB_PickFunction ()
RETURNS Enumeration(EnumReturn.Variant) AS $Variant
BEGIN
RETURN EnumReturn.Variant.Original;
END;

-- A bare entity name is still an entity.
CREATE OR MODIFY MICROFLOW EnumReturn.SUB_PickCustomer ()
RETURNS EnumReturn.Customer AS $Customer
BEGIN
RETURN empty;
END;
30 changes: 20 additions & 10 deletions mdl/executor/cmd_microflows_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b
if entityID == "" {
// Bare qualified name in microflow context is treated as TypeEntity by the
// visitor, but it may actually be an enumeration. Try enum lookup before failing.
if found := findEnumeration(ctx, p.Type.EntityRef.Module, p.Type.EntityRef.Name); found != nil {
s.Parameters[i].Type = ast.DataType{Kind: ast.TypeEnumeration, EnumRef: p.Type.EntityRef}
if retyped, ok := bareEnumerationType(ctx, p.Type); ok {
s.Parameters[i].Type = retyped
p = s.Parameters[i]
} else {
return nil, mdlerrors.NewNotFoundMsg("entity", p.Type.EntityRef.Module+"."+p.Type.EntityRef.Name,
fmt.Sprintf("entity '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
fmt.Sprintf("entity or enumeration '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
}
}
}
Expand Down Expand Up @@ -362,8 +362,13 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b
if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) {
entityID := entityResolver(*s.ReturnType.Type.EntityRef)
if entityID == "" {
return nil, mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
fmt.Sprintf("entity '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
// A bare qualified name may be an enumeration, as for parameters above.
if retyped, ok := bareEnumerationType(ctx, s.ReturnType.Type); ok && s.ReturnType.Type.Kind == ast.TypeEntity {
s.ReturnType.Type = retyped
} else {
return nil, mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
fmt.Sprintf("entity or enumeration '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
}
}
}
// Validate enumeration references for return type
Expand Down Expand Up @@ -625,12 +630,12 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui
if entityID == "" {
// Bare qualified name in microflow context is treated as TypeEntity by the
// visitor, but it may actually be an enumeration. Try enum lookup before failing.
if found := findEnumeration(ctx, p.Type.EntityRef.Module, p.Type.EntityRef.Name); found != nil {
s.Parameters[i].Type = ast.DataType{Kind: ast.TypeEnumeration, EnumRef: p.Type.EntityRef}
if retyped, ok := bareEnumerationType(ctx, p.Type); ok {
s.Parameters[i].Type = retyped
p = s.Parameters[i]
} else {
return nil, mdlerrors.NewNotFoundMsg("entity", p.Type.EntityRef.Module+"."+p.Type.EntityRef.Name,
fmt.Sprintf("entity '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
fmt.Sprintf("entity or enumeration '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
}
}
}
Expand All @@ -657,8 +662,13 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui
if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) {
entityID := entityResolver(*s.ReturnType.Type.EntityRef)
if entityID == "" {
return nil, mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
fmt.Sprintf("entity '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
// A bare qualified name may be an enumeration, as for parameters above.
if retyped, ok := bareEnumerationType(ctx, s.ReturnType.Type); ok && s.ReturnType.Type.Kind == ast.TypeEntity {
s.ReturnType.Type = retyped
} else {
return nil, mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
fmt.Sprintf("entity or enumeration '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
}
}
}
if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil {
Expand Down
16 changes: 16 additions & 0 deletions mdl/executor/cmd_microflows_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ func convertASTToMicroflowDataType(dt ast.DataType, entityResolver func(ast.Qual
}
}

// bareEnumerationType resolves the entity/enumeration ambiguity of a declared
// type. The parser cannot tell an entity from an enumeration for a bare
// `Module.Name` and always records an entity reference, so a caller that has
// found no entity of that name asks here whether an enumeration has it. It
// returns dt retyped as that enumeration, or false when none does.
//
// Parameters and return types of microflows, nanoflows and rules all resolve
// through this, so the three kinds accept the same spellings; `check` makes the
// same decision for a project that is not yet written in declaredTypeResolver.
func bareEnumerationType(ctx *ExecContext, dt ast.DataType) (ast.DataType, bool) {
if dt.EntityRef == nil || findEnumeration(ctx, dt.EntityRef.Module, dt.EntityRef.Name) == nil {
return dt, false
}
return ast.DataType{Kind: ast.TypeEnumeration, EnumRef: dt.EntityRef}, true
}

// The Mendix expression renderer lives in mdl/mendixexpr, where the visitor
// can reach it too (fmt --upgrade, ako/mxcli#804). These names are the
// executor's handles on it.
Expand Down
15 changes: 10 additions & 5 deletions mdl/executor/cmd_rules_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,12 @@ func execCreateRule(ctx *ExecContext, s *ast.CreateRuleStmt) error {
if entityID == "" {
// Bare qualified name in microflow context is treated as TypeEntity by the
// visitor, but it may actually be an enumeration. Try enum lookup before failing.
if found := findEnumeration(ctx, p.Type.EntityRef.Module, p.Type.EntityRef.Name); found != nil {
s.Parameters[i].Type = ast.DataType{Kind: ast.TypeEnumeration, EnumRef: p.Type.EntityRef}
if retyped, ok := bareEnumerationType(ctx, p.Type); ok {
s.Parameters[i].Type = retyped
p = s.Parameters[i]
} else {
return mdlerrors.NewNotFoundMsg("entity", p.Type.EntityRef.Module+"."+p.Type.EntityRef.Name,
fmt.Sprintf("entity '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
fmt.Sprintf("entity or enumeration '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
}
}
}
Expand All @@ -200,8 +200,13 @@ func execCreateRule(ctx *ExecContext, s *ast.CreateRuleStmt) error {
if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) {
entityID := entityResolver(*s.ReturnType.Type.EntityRef)
if entityID == "" {
return mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
fmt.Sprintf("entity '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
// A bare qualified name may be an enumeration, as for parameters above.
if retyped, ok := bareEnumerationType(ctx, s.ReturnType.Type); ok && s.ReturnType.Type.Kind == ast.TypeEntity {
s.ReturnType.Type = retyped
} else {
return mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
fmt.Sprintf("entity or enumeration '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
}
}
}
if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil {
Expand Down
Loading
Loading