Skip to content

Commit 584c166

Browse files
committed
feat: allow expressions in add-to-list statements
Studio Pro stores list-add values as expression strings, but MDL only accepted `add $Item to $List` and the AST kept only a bare item variable. Round-tripping a list-add action whose value is an attribute path or function call could therefore lose the original expression shape. The grammar now accepts `add expression to $List`, the visitor stores the parsed expression while keeping the simple-variable compatibility field, and the builder writes the expression value back to the `ChangeListAction`. The existing variable-only form remains supported. Tests cover parser and builder behavior for expression and simple-variable forms. The branch also adds a draft proposal, doctype fixture, quick-reference entry, and skill guidance; `mxcli check` on the fixture plus make build, make lint-go, and make test pass locally.
1 parent d871691 commit 584c166

16 files changed

Lines changed: 235 additions & 29 deletions

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,18 @@ commit $Product with events refresh;
400400

401401
**Best Practice**: Use `with events` when you want before/after commit event handlers to execute. Use `refresh` when the committed object is displayed in the client and you want the UI to update immediately.
402402

403+
## List Operations
404+
405+
```mdl
406+
-- Existing variable form
407+
add $Item to $Items;
408+
409+
-- Expression-valued add, useful when round-tripping Studio Pro list-add values
410+
add head($SourceItems) to $Items;
411+
```
412+
413+
Use expression-valued `add` only when the expression returns an object compatible with the target list element type.
414+
403415
## Database Operations
404416

405417
### RETRIEVE Statement

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ authentication basic, session
221221
| Rollback | `rollback $entity [refresh];` | Reverts uncommitted changes |
222222
| Retrieve (DB) | `retrieve $Var from Module.Entity [where condition];` | Database XPath retrieve |
223223
| Retrieve (Assoc) | `retrieve $list from $Parent/Module.AssocName;` | Retrieve by association |
224+
| Add to list | `add expression to $list;` | Also accepts existing `add $item to $list;` form |
224225
| Call microflow | `$Result = call microflow Module.Name (Param = $value);` | |
225226
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
226227
| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` |
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Proposal: Microflow ADD Expression To List
2+
3+
Status: Draft
4+
5+
## Summary
6+
7+
Allow `add` microflow statements to use any expression as the value being added to a list:
8+
9+
```mdl
10+
add head($SourceList) to $TargetList;
11+
```
12+
13+
The existing variable-only form remains valid:
14+
15+
```mdl
16+
add $Item to $TargetList;
17+
```
18+
19+
## Motivation
20+
21+
Studio Pro stores the value of a list-add action as an expression string. Existing models can therefore contain a list-add value that is not a bare variable. MDL previously parsed only `add $Item to $List`, so describe/exec round-trips could not preserve expression-valued list additions.
22+
23+
## Semantics
24+
25+
The parser stores the add value as an expression. For compatibility, a bare variable expression also populates the legacy `Item` field in the AST. The builder writes the expression source to the Mendix `ChangeListAction.Value` field and falls back to the legacy item variable only when no expression is present.
26+
27+
## Tests And Examples
28+
29+
`mdl-examples/doctype-tests/add_expression_to_list.test.mdl` demonstrates adding `head($SourceList)` to another list. Go regression tests cover parser behavior and builder output for both expression and simple-variable forms.
30+
31+
## Open Questions
32+
33+
- Should validation infer the list element type and reject expressions that cannot produce an object compatible with the target list?

docs/11-proposals/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ BSON schema Registry ◄──── multi-version Support
4747
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done ||
4848
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
4949
| [XPath Gaps](xpath-gaps-proposal.md) | Partial | XPath constraint support gap analysis. ~85% complete, association paths and nested predicates remain ||
50+
| [Microflow ADD Expression To List](PROPOSAL_microflow_add_expression_to_list.md) | Draft | Preserve expression-valued list-add actions in microflow round-trips ||
5051
| [LLM MDL Assistance](PROPOSAL_llm_mdl_assistance.md) | Proposed | Enhanced error messages with examples, reorganized skills by use case ||
5152

5253
### Testing & Evaluation
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
create module AddListExample;
2+
3+
create persistent entity AddListExample.Item (
4+
Name: string
5+
);
6+
/
7+
8+
create microflow AddListExample.AddFirstItem (
9+
$SourceItems: list of AddListExample.Item
10+
)
11+
returns list of AddListExample.Item as $Items
12+
begin
13+
$Items = create list of AddListExample.Item;
14+
add head($SourceItems) to $Items;
15+
return $Items;
16+
end;
17+
/

mdl/ast/ast_microflow.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,10 @@ type CreateListStmt struct {
504504

505505
func (s *CreateListStmt) isMicroflowStatement() {}
506506

507-
// AddToListStmt represents: ADD $Item TO $List
507+
// AddToListStmt represents: ADD expr TO $List
508508
type AddToListStmt struct {
509-
Item string // Item variable to add
509+
Item string // Item variable to add, kept for simple $Var compatibility
510+
Value Expression // Item expression to add
510511
List string // Target list variable
511512
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
512513
}

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,11 +686,15 @@ func (fb *flowBuilder) addCreateListAction(s *ast.CreateListStmt) model.ID {
686686

687687
// addAddToListAction creates an ADD TO list statement.
688688
func (fb *flowBuilder) addAddToListAction(s *ast.AddToListStmt) model.ID {
689+
value := fb.exprToString(s.Value)
690+
if value == "" && s.Item != "" {
691+
value = "$" + s.Item
692+
}
689693
action := &microflows.ChangeListAction{
690694
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
691695
Type: microflows.ChangeListTypeAdd,
692696
ChangeVariable: s.List,
693-
Value: "$" + s.Item,
697+
Value: value,
694698
}
695699

696700
activity := &microflows.ActionActivity{
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
"github.com/mendixlabs/mxcli/sdk/microflows"
10+
)
11+
12+
func TestAddToListBuilderUsesExpressionValue(t *testing.T) {
13+
fb := &flowBuilder{}
14+
15+
fb.addAddToListAction(&ast.AddToListStmt{
16+
Value: &ast.AttributePathExpr{
17+
Variable: "Order",
18+
Path: []string{"Number"},
19+
},
20+
List: "Numbers",
21+
})
22+
23+
action := lastChangeListAction(t, fb)
24+
if action.Value != "$Order/Number" {
25+
t.Fatalf("Value = %q, want $Order/Number", action.Value)
26+
}
27+
}
28+
29+
func TestAddToListBuilderKeepsSimpleVariableFallback(t *testing.T) {
30+
fb := &flowBuilder{}
31+
32+
fb.addAddToListAction(&ast.AddToListStmt{
33+
Item: "Order",
34+
List: "Orders",
35+
})
36+
37+
action := lastChangeListAction(t, fb)
38+
if action.Value != "$Order" {
39+
t.Fatalf("Value = %q, want $Order", action.Value)
40+
}
41+
}
42+
43+
func lastChangeListAction(t *testing.T, fb *flowBuilder) *microflows.ChangeListAction {
44+
t.Helper()
45+
46+
if len(fb.objects) == 0 {
47+
t.Fatal("Expected builder to create an action activity")
48+
}
49+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
50+
if !ok {
51+
t.Fatalf("Last object = %T, want ActionActivity", fb.objects[len(fb.objects)-1])
52+
}
53+
action, ok := activity.Action.(*microflows.ChangeListAction)
54+
if !ok {
55+
t.Fatalf("Action = %T, want ChangeListAction", activity.Action)
56+
}
57+
return action
58+
}

mdl/grammar/MDLParser.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1867,7 +1867,7 @@ createListStatement
18671867
* ```
18681868
*/
18691869
addToListStatement
1870-
: ADD VARIABLE TO VARIABLE
1870+
: ADD expression TO VARIABLE
18711871
;
18721872

18731873
/**

mdl/grammar/parser/MDLParser.interp

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)