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
19 changes: 19 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,25 @@ rest call delete 'https://api.example.com/items/{1}' with (

**REST CALL supports full error handling** (`on error continue`, `on error rollback`, custom error handlers).

## Empty Java-Action Argument (`...`)

When `describe` round-trips a Java-action call that has an unbound parameter
in Studio Pro, it emits `...` as the argument value. This preserves the
underlying empty `BasicCodeActionParameterValue.Argument` so that the next
`describe → exec → describe` cycle stays symmetric.

```mdl
$Total = call java action SampleModule.Recalculate(
CompanyId = ...,
RecalculateAll = true,
ItemList = ...
);
```

`...` is a *round-trip-only* placeholder. New scripts should bind every
parameter to a real expression; reach for `...` only when you're regenerating
MDL from an existing project that already had unbound parameters.

## Error Handling

MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).
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 @@ -775,6 +775,7 @@ Module.OrderResponse_CustomerInfo/Module.CustomerInfo as customer {
| Create exposed action | `... exposed as 'caption' in 'Category' as $$ ... $$;` | Toolbox-visible in Studio Pro |
| Drop Java action | `drop java action Module.Name;` | Delete a Java action |
| Call from microflow | `$Result = call java action Module.Name(Param = value);` | Inside BEGIN...END |
| Empty argument | `call java action Module.Name(Param = ...);` | `...` placeholder for an unbound code-action parameter (round-trip only) |

**Parameter Types:** `string`, `integer`, `long`, `decimal`, `boolean`, `datetime`, `Module.Entity`, `list of Module.Entity`, `enum Module.EnumName`, `enumeration(Module.EnumName)`, `stringtemplate(sql)`, `stringtemplate(Oql)`, `entity <pEntity>` (type parameter declaration), bare `pEntity` (type parameter reference).

Expand Down
111 changes: 111 additions & 0 deletions docs/11-proposals/PROPOSAL_ellipsis_placeholder_expression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Ellipsis Placeholder Expression

Status: Draft

## Summary

Add a single-token expression `...` that represents an unbound /
intentionally-empty argument value in microflow call statements.

```mdl
$Total = call java action SampleModule.Recalculate(
CompanyId = ...,
RecalculateAll = true,
ItemList = ...
);
```

`...` produces a parameter binding with an empty `Argument` string in the
serialized BSON (`Microflows$BasicCodeActionParameterValue.Argument = ""`).
Re-executing a script that contains `...` reproduces the same empty
binding byte-for-byte, so describe → exec → describe stays symmetric for
existing Studio Pro projects that have unbound code-action parameters.

## Motivation

Studio Pro's Java-action call dialog allows a developer to leave individual
parameters empty — for example, when a Java action declares a parameter that
the calling microflow does not yet have a meaningful value for, or when an
external mapping is expected to fill the slot at runtime. The on-disk
representation is a `Microflows$JavaActionParameterMapping` whose `Value` is a
`BasicCodeActionParameterValue` with `Argument: ""`.

Before `...` existed, the describer had two options for these empty bindings:

1. Emit `''` (empty string literal). On re-exec, the visitor would round-trip
to a non-empty single-quote literal whose `Argument` was `''`, not `""`,
and Studio Pro would render the parameter as the literal string `''`.
2. Drop the parameter entirely. Studio Pro would then add a back a
placeholder mapping with a generated value, breaking the round-trip.

Both lose information. `...` lets the describer round-trip the empty binding
without inventing a fake value.

## Syntax

```antlr
atomicExpression
: literal
| ELLIPSIS
| ...
;
```

Where `ELLIPSIS` is the lexer token `'...'`. The token is reserved for this
single use; it is not valid in arithmetic / boolean / comparison
expressions.

## Semantics

- `...` is recognised by the builder via `isPlaceholderExpression` in
`mdl/executor/cmd_microflows_builder_calls.go`.
- Inside a Java-action `callArgument`, `...` produces a
`BasicCodeActionParameterValue` with `Argument: ""`.
- Outside of `callArgument` lists, `...` parses but the builder rejects it
(it never resolves to a runtime value). Future statements may extend the
set of contexts that accept `...` — see Open Questions.

## Examples

```mdl
-- Java action call with two unbound and one bound argument
$Total = call java action SampleModule.Recalculate(
CompanyId = ...,
RecalculateAll = true,
ItemList = ...
);
```

The Mendix BSON for the unbound arguments is:

```
JavaActionParameterMapping {
Parameter: 'SampleModule.Recalculate.CompanyId',
Value: BasicCodeActionParameterValue { Argument: '' }
}
```

## Tests And Examples

- Builder coverage: `TestBuildJavaAction_PlaceholderArgumentPreservesEmptyBasicValue`
in `mdl/executor/cmd_microflows_builder_java_action_test.go`.
- Visitor coverage: `atomicExpression`'s `ELLIPSIS` arm produces
`ast.SourceExpr{Source: "..."}` (see
`mdl/visitor/visitor_microflow_expression.go`).
- Example script: `mdl-examples/doctype-tests/ellipsis_placeholder.test.mdl`.

## Open Questions

- Should `...` be allowed as an argument to `call microflow` and
`call nanoflow` calls as well? Today only Java actions consume the
`BasicCodeActionParameterValue` form, so there is no symmetric BSON
representation, but a future proposal could extend this.
- Should we explicitly document `...` as round-trip-only and warn the linter
when an authored microflow uses `...` outside of a known describe-emitted
context? This would prevent users from authoring scripts that produce
Studio Pro warnings ("unbound parameter") on import.
- Should the surface syntax be `...` or a clearer keyword like
`unspecified` / `default` / `unbound`? `...` was chosen because it is
visually distinct, short, and matches existing "this is intentionally
blank" conventions in other tools (Python's `Ellipsis`, TypeScript's
`never` placeholder, etc.).
1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ BSON schema Registry ◄──── multi-version Support
|----------|--------|---------|------------|
| [MDL Syntax Improvements v1](PROPOSAL_mdl_syntax_improvements.md) | Draft | Go-style assignment, C-style braces, fluent list APIs | — |
| [MDL Syntax Improvements v2](PROPOSAL_mdl_syntax_improvements_v2.md) | Proposed | Consolidated v2: unified variable declaration, C-style braces, fluent list ops | Syntax Improvements v1 |
| [Ellipsis Placeholder Expression](PROPOSAL_ellipsis_placeholder_expression.md) | Draft | `...` placeholder for unbound code-action parameters; round-trip preservation of empty `BasicCodeActionParameterValue` bindings | — |
| [Page Syntax V2](PROPOSAL_page_syntax_v2.md) | Superseded | Page/widget syntax with `{}` blocks and `->` binding. Superseded by V3 (archived) | — |
| [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 |
Expand Down
25 changes: 25 additions & 0 deletions mdl-examples/doctype-tests/ellipsis_placeholder.test.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
create microflow SampleModule.ACT_RecalculateOpenItems ()
returns Void
begin
call java action SampleModule.Recalculate(
CompanyId = ...,
RecalculateAll = true,
ItemList = ...
);
return;
end;
/

create microflow SampleModule.ACT_RecalculateForCompany (
$CompanyId: String
)
returns Void
begin
call java action SampleModule.Recalculate(
CompanyId = $CompanyId,
RecalculateAll = false,
ItemList = ...
);
return;
end;
/
9 changes: 9 additions & 0 deletions mdl/ast/ast_expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ type IfThenElseExpr struct {

func (e *IfThenElseExpr) isExpression() {}

// SourceExpr preserves source text for expressions that do not have a richer
// AST representation yet.
type SourceExpr struct {
Expression Expression
Source string
}

func (e *SourceExpr) isExpression() {}

// ============================================================================
// XPath-Specific Expression Types
// ============================================================================
Expand Down
8 changes: 8 additions & 0 deletions mdl/executor/cmd_microflows_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ func (fb *flowBuilder) resolveAssociationPaths(expr ast.Expression) ast.Expressi
ThenExpr: fb.resolveAssociationPaths(e.ThenExpr),
ElseExpr: fb.resolveAssociationPaths(e.ElseExpr),
}
case *ast.SourceExpr:
if e.Source != "" {
return e
}
return &ast.SourceExpr{
Expression: fb.resolveAssociationPaths(e.Expression),
Source: e.Source,
}
default:
return expr
}
Expand Down
10 changes: 10 additions & 0 deletions mdl/executor/cmd_microflows_builder_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Entity: entityName,
}
} else if isPlaceholderExpression(arg.Value) {
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: "",
}
} else {
// Regular parameter: expression-based value
valueExpr := fb.exprToString(arg.Value)
Expand Down Expand Up @@ -268,6 +273,11 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
return activity.ID
}

func isPlaceholderExpression(expr ast.Expression) bool {
source, ok := expr.(*ast.SourceExpr)
return ok && strings.TrimSpace(source.Source) == "..."
}

// addCallExternalActionAction creates a CALL EXTERNAL ACTION statement.
func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt) model.ID {
serviceQN := s.ServiceName.Module + "." + s.ServiceName.Name
Expand Down
59 changes: 59 additions & 0 deletions mdl/executor/cmd_microflows_builder_java_action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/sdk/microflows"
)

func TestBuildJavaAction_PlaceholderArgumentPreservesEmptyBasicValue(t *testing.T) {
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing}
stmt := &ast.CallJavaActionStmt{
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "Recalculate"},
Arguments: []ast.CallArgument{
{Name: "CompanyId", Value: &ast.SourceExpr{Source: "..."}},
{Name: "RecalculateAll", Value: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true}},
{Name: "ItemList", Value: &ast.SourceExpr{Source: " ... "}},
},
}

id := fb.addCallJavaActionAction(stmt)
var activity *microflows.ActionActivity
for _, obj := range fb.objects {
if obj.GetID() == id {
activity, _ = obj.(*microflows.ActionActivity)
break
}
}
if activity == nil {
t.Fatal("expected Java action activity")
}
action, ok := activity.Action.(*microflows.JavaActionCallAction)
if !ok {
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
}
if len(action.ParameterMappings) != 3 {
t.Fatalf("parameter mappings = %d, want 3", len(action.ParameterMappings))
}

for _, idx := range []int{0, 2} {
value, ok := action.ParameterMappings[idx].Value.(*microflows.BasicCodeActionParameterValue)
if !ok {
t.Fatalf("mapping %d value = %T, want *BasicCodeActionParameterValue", idx, action.ParameterMappings[idx].Value)
}
if value.Argument != "" {
t.Fatalf("mapping %d argument = %q, want empty string", idx, value.Argument)
}
}

value, ok := action.ParameterMappings[1].Value.(*microflows.BasicCodeActionParameterValue)
if !ok {
t.Fatalf("boolean mapping value = %T, want *BasicCodeActionParameterValue", action.ParameterMappings[1].Value)
}
if value.Argument != "true" {
t.Fatalf("boolean argument = %q, want true", value.Argument)
}
}
5 changes: 5 additions & 0 deletions mdl/executor/cmd_microflows_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ func expressionToString(expr ast.Expression) string {
thenStr := expressionToString(e.ThenExpr)
elseStr := expressionToString(e.ElseExpr)
return "if " + cond + " then " + thenStr + " else " + elseStr
case *ast.SourceExpr:
if e.Source != "" {
return e.Source
}
return expressionToString(e.Expression)
default:
return ""
}
Expand Down
1 change: 1 addition & 0 deletions mdl/grammar/MDLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ DIV: D I V;

SEMICOLON: ';';
COMMA: ',';
ELLIPSIS: '...';
DOT: '.';
LPAREN: '(';
RPAREN: ')';
Expand Down
1 change: 1 addition & 0 deletions mdl/grammar/MDLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -3656,6 +3656,7 @@ argumentList

atomicExpression
: literal
| ELLIPSIS
| VARIABLE (DOT attributeName)* // $Var or $Widget.Attribute (data source ref)
| AT qualifiedName // @Module.ConstantName (constant reference)
| qualifiedName
Expand Down
2 changes: 1 addition & 1 deletion mdl/grammar/keyword_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestKeywordRuleCoverage(t *testing.T) {
// Literals
"STRING_LITERAL": true, "DOLLAR_STRING": true, "NUMBER_LITERAL": true, "MENDIX_TOKEN": true,
// Punctuation
"SEMICOLON": true, "COMMA": true, "DOT": true,
"SEMICOLON": true, "COMMA": true, "ELLIPSIS": true, "DOT": true,
"LPAREN": true, "RPAREN": true,
"LBRACE": true, "RBRACE": true,
"LBRACKET": true, "RBRACKET": true,
Expand Down
5 changes: 4 additions & 1 deletion mdl/grammar/parser/MDLLexer.interp

Large diffs are not rendered by default.

Loading