Skip to content

feat: support empty Java action arguments#336

Merged
ako merged 9 commits intomendixlabs:mainfrom
hjotha:submit/microflow-ellipsis-placeholder-expression
May 1, 2026
Merged

feat: support empty Java action arguments#336
ako merged 9 commits intomendixlabs:mainfrom
hjotha:submit/microflow-ellipsis-placeholder-expression

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 27, 2026

Summary

Uses the existing empty literal as the MDL representation for intentionally unbound Java-action parameters. This lets describe output preserve empty BasicCodeActionParameterValue bindings and lets exec write them back without inventing synthetic expression values or adding a new placeholder token.

Part of #332.

Changes

  • Maps Java action arguments written as empty/null back to empty code-action parameter values.
  • Preserves empty microflow callback parameter references as empty MicroflowParameterValue references.
  • Documents the syntax in the proposal index and proposal document.
  • Adds a doctype example and parser/builder regression coverage.

Validation

  • go test ./mdl/executor -run 'TestBuildJavaAction_Empty|TestBuildJavaAction' -count=1
  • make build
  • ./bin/mxcli check mdl-examples/doctype-tests/empty_java_action_argument.mdl
  • make test

@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • Visitor logic not explicitly shown: While the implementation appears correct (tests pass and documentation states the visitor produces ast.SourceExpr{Source: "..."} for ELLIPSIS), the diff doesn't show explicit visitor changes. Given the truncation note, this may be in the omitted sections, but it would be good to confirm the visitor correctly bridges the ELLIPSIS token to the SourceExpr AST node.

What Looks Good

  • Full-stack implementation: The feature is properly implemented through the entire pipeline:

    • Grammar: Added ELLIPSIS token and rule
    • AST: Added SourceExpr to preserve source text
    • Executor: Handles placeholder detection and conversion to empty BasicCodeActionParameterValue
    • Tests: Includes unit test verifying BSON preservation and doctype example
    • Documentation: Updated quick reference, skills doc, and added detailed proposal
  • Correct semantics: The implementation correctly preserves empty Argument values in BSON for round-trip compatibility, addressing the core motivation of maintaining symmetry in describe → exec → describe cycles.

  • Clear documentation: The proposal and accompanying documentation clearly explain that ... is round-trip-only and should not be used in newly authored scripts, preventing misuse.

  • Robust handling: The implementation correctly handles whitespace variations (e.g., " ... ") through trimming in the placeholder detection function.

  • Test coverage: Includes both unit test for the builder logic and a doctype test for end-to-end validation.

  • Minimal scope: Focused solely on adding the ellipsis placeholder for Java action parameters without attempting to over-extend to other contexts prematurely.

Recommendation

Approve. The PR thoroughly implements the ellipsis placeholder feature following all project guidelines, with proper documentation, test coverage, and full-stack implementation. The minor concern about visitor logic is likely addressed in the generated or omitted code sections given the passing tests and explicit documentation of the AST behavior.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Symptom: microflow Java action parameters with an empty BasicCodeActionParameterValue could not be represented in MDL, so describe/exec round-trips had to either invent a value or fail to parse the generated source.

Root cause: the expression grammar had no placeholder expression that could carry an intentionally unbound code-action argument through the parser and builder.

Fix: add an ellipsis source expression, emit it for `...`, preserve it through expression formatting, and map it back to an empty BasicCodeActionParameterValue for Java action arguments.

Tests: added parser/visitor coverage, builder coverage for placeholder Java action arguments, an MDL doctype fixture, and validation docs for the new syntax.
Symptom: Java actions with microflow-typed parameters roundtripped placeholder arguments as basic empty expressions, which Studio Pro reported as stale Java action arguments.

Root cause: the SDK/parser/writer did not model Microflows$MicroflowParameterValue, and the builder treated all ellipsis placeholders as BasicCodeActionParameterValue.

Fix: add MicroflowParameterValue support end-to-end and emit it for Java action parameters whose definition is MicroflowType, keeping ellipsis as an empty microflow reference.

Tests: added builder and parser regressions and ran make test.
@hjotha hjotha force-pushed the submit/microflow-ellipsis-placeholder-expression branch from b8ee049 to a8dee83 Compare April 30, 2026 08:29
@github-actions
Copy link
Copy Markdown

AI Code Review

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • LSP wiring not verified: While the PR mentions that LSP wiring should be checked if the feature adds formatting, diagnostics, or navigation targets, this expression placeholder doesn't appear to require LSP changes. However, it would be good to confirm that the existing LSP implementation correctly handles the new SourceExpr AST node for features like hover/tooltips.

  • VS Code extension not verified: Similar to LSP, since this doesn't add new LSP capabilities, VS Code extension updates may not be needed, but it's worth confirming the extension correctly tokenizes and highlights the new ... syntax.

What Looks Good

  1. Complete full-stack implementation: The PR properly implements the feature through all required layers:

    • Grammar (MDLLexer.g4, MDLParser.g4)
    • AST (ast_expression.go with SourceExpr type)
    • Executor handling (cmd_microflows_builder_calls.go with isPlaceholderExpression)
    • Tests covering the functionality (cmd_microflows_builder_java_action_test.go)
    • Documentation (skills file, quick reference, proposal)
    • Doctype test example (ellipsis_placeholder.test.mdl)
  2. Correct semantics: The implementation properly distinguishes between:

    • Java action arguments (using BasicCodeActionParameterValue with empty Argument)
    • Microflow/nanoflow arguments (using MicroflowParameterValue with empty Microflow)
    • Only allows the placeholder in appropriate contexts
  3. Round-trip focus: The feature is correctly positioned as a round-trip-only placeholder, with clear documentation advising against its use in authored scripts.

  4. Test coverage: Includes unit tests verifying:

    • Preservation of empty BasicCodeActionParameterValue.Argument for Java actions
    • Proper handling of microflow-type parameters with MicroflowParameterValue
    • Whitespace tolerance in the placeholder (" ... ")
  5. Documentation quality:

    • Clear explanation of the motivation and problem being solved
    • Proper warning about round-trip-only usage
    • Examples showing correct usage in context
    • Updated quick reference with appropriate categorization

Recommendation

Approve the PR. The implementation correctly addresses the need to preserve round-trip symmetry for unbound Java action parameters, follows all required architectural patterns, includes comprehensive tests, and provides appropriate documentation. The minor concerns about LSP/VS Code extension are unlikely to impact functionality given the nature of the feature as an expression placeholder that doesn't introduce new language constructs requiring special editor support.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Copy link
Copy Markdown
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not empty?

The ... token solves a real problem, but MDL already has the right tool for this: the empty keyword.

empty is already:

  • A lexer token (EMPTY: E M P T Y in MDLLexer.g4)
  • A valid literal in MDLParser.g4 — parseable as an expression anywhere a literal is accepted, including call arguments
  • A LiteralEmpty AST kind in mdl/ast/ast_expression.go
  • Already handled by the executorexpressionToString() returns "empty" for LiteralEmpty, and validate_microflow.go already treats it as a null-like sentinel

So the builder could simply check if lit.Kind == ast.LiteralEmpty instead of isPlaceholderExpression(...), and describe would emit:

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

This is already valid MDL, already human-readable, and consistent with how = empty is used for uninitialized list variables (declare $Items list of Module.Entity = empty).

The ... choice instead introduced a new lexer token, regenerated the entire ANTLR parser (3000+ lines of churn in mdl_lexer.go / mdl_parser.go), and added syntax with no precedent in MDL — all to solve a problem the existing keyword already covers. CompanyId = ... reads as broken/incomplete code; CompanyId = empty reads as intentional.

Suggested change: drop ELLIPSIS from the grammar, remove the isPlaceholderExpression helper, and map LiteralEmptyBasicCodeActionParameterValue { Argument: "" } in addCallJavaActionAction. The doctype example, skill docs, and quick-reference entry would all stay — just s/.../empty/.

Symptom: review pointed out that the ellipsis placeholder introduced a new MDL
token for a Java-action argument case that the existing `empty` literal already
expresses more clearly.

Root cause: the feature modeled unbound code-action parameters as a new
`...` expression, which created parser churn and diverged from normal MDL
expression conventions.

Fix: remove the ELLIPSIS token and parser alternative, map `empty` literals in
Java-action arguments to empty BasicCodeActionParameterValue/MicroflowParameterValue
bindings, and update formatter output, docs, proposal, skill text, and examples.

Tests: regenerate the parser, run focused grammar/visitor/executor tests, then
make build and make test.
Symptom: the empty Java action argument example used a .test.mdl suffix, so TestMxCheck_DoctypeScripts skipped it.

Root cause: doctype CI only discovers plain .mdl files under mdl-examples/doctype-tests.

Fix: rename the fixture to .mdl without changing its contents.

Tests: mxcli check on the fixture plus make build and make test.
…psis-placeholder-expression

# Conflicts:
#	docs/11-proposals/README.md
#	mdl/executor/cmd_microflows_builder_calls.go
#	sdk/mpr/parser_microflow_test.go
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

AI Code Review

We are reviewing a pull request that adds support for the empty literal in the context of Java-action arguments to represent unbound parameters.

The PR includes:

  • Documentation updates (skills, quick reference, proposal)
  • A new doctype test file
  • Changes to the executor to handle empty in Java-action arguments (both for basic and microflow parameters)
  • Changes to the formatter to output empty for empty BasicCodeActionParameterValue and MicroflowParameterValue
  • Visitor test to ensure the parser accepts empty in Java-action arguments
  • SDK changes to add a new MicroflowParameterValue type and updates to parsers and writers to handle it.

We must review against the provided checklist.

Let's break down the review by section:

Overlap & duplication

  • The PR does not appear to duplicate existing functionality. It introduces a new use of the existing empty literal (which was already part of the MDL language) in a specific context (Java-action arguments). The proposal is new and we don't see an existing proposal for the same thing in the provided list (the list of proposals was empty, but we see a new proposal file was added).

MDL syntax design

The PR does not add new MDL syntax (like new keywords or statements). It modifies the interpretation of an existing literal (empty) in a specific context (Java-action arguments). However, note that the PR also adds documentation for the empty literal in the quick reference and in the skills.

We must check if the use of empty in this context follows the design guidelines:

  • The empty literal is already part of the MDL language (used for declaring empty lists, etc.). The PR is extending its meaning in a new context (Java-action arguments) to represent an unbound parameter.

  • The design guidelines for MDL syntax (from .claude/skills/design-mdl-syntax.md) are not fully provided in the context, but we can infer from the checklist:

    • It should follow standard CREATE/ALTER/DROP/SHOW/DESCRIBE pattern? Not applicable because we are not adding a new statement.
    • Uses qualified names? Not applicable because we are dealing with an expression literal.
    • Property lists? Not applicable.
    • Colon for property definitions, AS for name mappings? Not applicable.
    • Keywords are full English words? The empty literal is already a keyword? Actually, empty is a literal (like null, true, false). It is not a keyword in the sense of a verb.

    However, note that the PR adds a new use of empty in the context of Java-action arguments. We must ensure that this use is consistent and does not break existing uses.

    The PR also updates the quick reference to show:
    | Empty argument | call java action Module.Name(Param = empty); | Unbound code-action parameter preserved as empty mapping |

    This is a clear and concise example.

    The skill file (.claude/skills/mendix/write-microflows.md) is updated to explain the use of empty in Java-action arguments.

    The proposal document explains the motivation and semantics.

    We note that the empty literal is already used in MDL for:
    declare $List list of String = empty

    Now, we are also using it in:
    call java action ... (Param = empty)

    This is acceptable because the meaning is similar: in both cases, it represents an "empty" or "uninitialized" value. However, note that in the list declaration, empty creates an empty list, whereas in the Java-action argument, it represents an unbound parameter (which is stored as an empty string in the BSON for basic types and an empty reference for microflow parameters).

    The PR does not introduce any new keyword or symbol, so there is no risk of keyword overloading.

    The statement reads as English: "Call java action ... with CompanyId = empty" is understandable.

    The diff-friendly criterion: adding this usage does not require a change in the grammar (because empty is already a literal) so it doesn't add any new syntax lines. However, note that the PR does not actually change the grammar? Let's check the diff:

    We don't see any changes to the grammar files (MDLLexer.g4 or MDLParser.g4) in the diff. This is because the empty literal is already defined. So the grammar already supports it.

    Therefore, the syntax design is acceptable.

Full-stack consistency

The PR adds a new feature (the use of empty in Java-action arguments) and we must check if it is wired through the full pipeline.

  • Grammar: The empty literal is already in the grammar (so no change needed). We don't see any grammar changes in the diff, which is correct.

  • AST: We don't see any new AST node type. The empty literal is already represented by ast.LiteralExpr with Kind == ast.LiteralEmpty. So no change needed.

  • Visitor: We don't see any changes to the visitor. The visitor already produces an ast.LiteralExpr for empty. So no change needed.

  • Executor: We see changes in mdl/executor/cmd_microflows_builder_calls.go to handle the empty literal when building Java-action arguments. Specifically, it checks for isPlaceholderExpression (which includes LiteralEmpty and LiteralNull) and then creates the appropriate parameter value (either BasicCodeActionParameterValue with empty string or MicroflowParameterValue with empty string).

  • Backend: The changes are in the executor, which uses the backend abstraction? Actually, the executor is the backend for microflows? We see that the executor is in mdl/executor/ and it uses the sdk/microflows package to build the BSON structures. There is no direct call to sdk/mpr for writing? We must check the backend abstraction compliance.

    Looking at the checklist item for backend abstraction compliance:

    • No sdk/mpr write imports in executor — executor files must not call sdk/mpr writer/parser types directly; use ctx.Backend.* instead

    In the changed file mdl/executor/cmd_microflows_builder_calls.go, we don't see any import of sdk/mpr. We see:
    "github.com/mendixlabs/mxcli/sdk/javaactions"
    "github.com/mendixlabs/mxcli/sdk/microflows"

    And the code uses microflows.BasicCodeActionParameterValue and microflows.MicroflowParameterValue. These are from the sdk/microflows package, which is part of the model, not the MPR writer/parser.

    However, note that the executor is building the microflow activity objects (which are then passed to the backend for writing). The backend for microflows is in mdl/backend/mpr/ (specifically, the MicroflowBackend interface and its implementation in mdl/backend/mpr/microflow.go). We don't see changes to the backend in this PR because the changes are in the executor (which is the frontend) and the backend already knows how to handle these parameter values (because the SDK types are used).

    We must check if the backend (in mdl/backend/mpr/) has been updated to handle the new MicroflowParameterValue? Actually, the MicroflowParameterValue is a new type in the sdk/microflows package. The backend (which uses the SDK) will need to know how to write it. We see changes in the SDK:

    - `sdk/microflows/microflows_actions.go`: added `MicroflowParameterValue` type.
    - `sdk/mpr/parser_microflow_actions.go`: added parsing for `Microflows$MicroflowParameterValue`.
    - `sdk/mpr/writer_microflow_actions.go`: added writing for `MicroflowParameterValue`.
    

    And the backend implementation in mdl/backend/mpr/ uses the SDK's writer and parser, so it should be covered.

    However, note that the PR does not show changes to mdl/backend/mpr/ because the backend uses the SDK. The SDK changes are in the sdk/ directory.

    We also see a mock stub for the backend? The checklist requires a mock stub for new backend methods. But note: the backend methods are not changed because the backend uses the SDK. The SDK changes are in the sdk/mpr package, which is not part of the backend abstraction. The backend abstraction is in mdl/backend/ and it uses the SDK via the modelsdk-go? Actually, the mdl/backend/mpr/ package uses the sdk/mpr package to read and write BSON.

    Therefore, the changes to the SDK (in sdk/mpr/) are part of the backend implementation. We must check if there are mock stubs for any new methods? But note: the SDK changes are in the parsing and writing of BSON, which is internal to the backend. The backend abstraction layer (mdl/backend/) does not expose these details. The backend abstraction layer has interfaces like MicroflowBackend which have methods for creating activities, etc. The changes we made are in the executor (which builds the activity objects) and the SDK (which knows how to serialize those objects to BSON). The backend abstraction layer is not changed because we are not adding a new backend method; we are just using existing methods to create activities that now have a new kind of parameter value.

    However, we must check if the executor is using the backend abstraction correctly. The executor in mdl/executor/ does not use the backend abstraction for building the AST? It builds the AST and then the executor uses the SDK to build the model objects (which are then passed to the backend for writing). This is acceptable because the executor is the frontend and the backend is responsible for the actual writing via the ctx.Backend (which is an interface in mdl/backend/). We see in the executor that it uses fb.objects (a slice of model.Element) and then at the end, it returns the ID. The actual writing to the MPR is done by the backend when the model is saved.

    Therefore, we believe the full-stack consistency is satisfied.

    We also note:
    - LSP: We don't see any changes to the LSP. The feature does not add formatting, diagnostics, or navigation targets that would require LSP changes? Actually, the formatter for microflow actions was changed (in mdl/executor/cmd_microflows_format_action.go) to output empty for empty values. This affects the describe output, which is used by the LSP for hover and diagnostics? But the LSP itself doesn't need to change because it relies on the executor to format the actions.

    - DESCRIBE roundtrip: We have a doctype test and we see that the formatter now outputs `empty` for empty values, so the roundtrip should work.
    
    - VS Code extension: We don't see any changes to the VS Code extension. The feature does not add new LSP capabilities, so no update to `package.json` is needed.
    

Test coverage

  • We see new test files:
    - mdl/executor/cmd_microflows_builder_java_action_test.go
    - mdl/executor/cmd_microflows_format_action_test.go
    - mdl/visitor/visitor_test.go (added a test for the parser accepting empty in Java-action arguments)
    - sdk/mpr/parser_javaactions_test.go
    - sdk/mpr/parser_microflow_test.go

  • We also see a new doctype test: mdl-examples/doctype-tests/empty_java_action_argument.mdl

  • The tests appear to cover the new functionality.

  • There is no use of time.Sleep in the tests we see.

Security & robustness

  • We don't see any obvious security issues (like command injection, XSS, etc.) in the changes.
  • The changes are in the executor and SDK, which are not exposed to external input in a way that would cause injection.
  • We don't see any file I/O in hot paths.
  • The method receivers appear to be correct.

Scope & atomicity

  • The PR is focused on a single feature: supporting the empty literal in Java-action arguments to represent unbound parameters.
  • All changes are related to this feature.

Code quality

  • We don't see any obvious refactors that were inconsistently applied.
  • The changes are localized and follow the existing patterns.

Bugs & correctness

  • We note that the visitor test expects ast.LiteralNull for the empty literal? Let's look:

    if lit.Kind != ast.LiteralNull {
        t.Fatalf("argument %d kind = %v, want LiteralNull", idx, lit.Kind)
    }
    

    But wait: in the ast package, what is the kind for the empty literal?

    We don't have the ast package code in the diff, but we can infer from the changes in cmd_microflows_builder_calls.go:

    func isPlaceholderExpression(expr ast.Expression) bool {
        lit, ok := expr.(*ast.LiteralExpr)
        return ok && (lit.Kind == ast.LiteralEmpty || lit.Kind == ast.LiteralNull)
    }
    

    So the empty literal can be represented by either ast.LiteralEmpty or ast.LiteralNull? This seems odd.

    Looking at the visitor test:

    input := `CREATE MICROFLOW Synthetic.Check ()
    RETURNS Boolean AS $Success
    BEGIN
      $Total = CALL JAVA ACTION Synthetic.Recalculate(CompanyId = empty, RecalculateAll = true, ItemList = empty);
      RETURN true;
    END;`
    

    And then it checks that the kind is ast.LiteralNull.

    Why would the empty literal be parsed as ast.LiteralNull?

    We must check the lexer and parser. However, note that the PR does not show changes to the grammar. So the existing grammar must be producing ast.LiteralNull for the empty keyword?

    Alternatively, it's possible that the empty literal is being confused with the null literal?

    This is a potential bug: the empty literal and the null literal are two different things in MDL.

    In MDL, we have:
    - empty: for empty lists (and now for unbound Java-action parameters)
    - null: for the null value

    They should be represented by different AST node kinds.

    The visitor test is expecting ast.LiteralNull for the empty literal, which is wrong.

    We must check how the empty literal is defined in the lexer and parser.

    Since we don't have the grammar files in the diff, we must rely on the fact that the PR did not change them. Therefore, the existing grammar must have a rule for empty that produces a token that is then interpreted as ast.LiteralEmpty? Or maybe the existing grammar already had empty as a token and it was mapped to ast.LiteralEmpty?

    However, the visitor test is failing if it gets ast.LiteralEmpty? Because it expects ast.LiteralNull.

    Let me look at the visitor test again:

    if lit.Kind != ast.LiteralNull {
        t.Fatalf("argument %d kind = %v, want LiteralNull", idx, lit.Kind)
    }
    

    This test would fail if the empty literal was parsed as ast.LiteralEmpty.

    Therefore, we suspect that the empty literal is currently being parsed as ast.LiteralNull because of a mistake in the grammar or visitor?

    Alternatively, it's possible that the empty literal is not defined in the grammar and is being mistaken for an identifier? But then it would be an error.

    Without the grammar files, we cannot be sure. However, note that the PR adds a test that expects ast.LiteralNull and passes. This suggests that the current state of the code (before the PR) already parses empty as ast.LiteralNull?

    But that would be incorrect because empty and null are different.

    We must flag this as a potential issue.

    However, note that the PR also changes the formatter to output empty for an empty BasicCodeActionParameterValue (which is correct). And the executor treats ast.LiteralEmpty and ast.LiteralNull the same in the isPlaceholderExpression function.

    So even if the empty literal is parsed as ast.LiteralNull, the executor will still treat it as a placeholder and produce the correct BSON (empty string for basic types, empty reference for microflow types). And the formatter will output empty for these empty values.

    Therefore, from a functional perspective, it works. But it is semantically incorrect because the AST node for the empty literal should be ast.LiteralEmpty, not ast.LiteralNull.

    We should fix the AST representation. However, note that the PR does not change the grammar or visitor, so we cannot fix it in this PR. We must note this as a pre-existing issue? Or perhaps the empty literal is not currently supported in the grammar and is being parsed as an identifier? Then the visitor would not produce a literal at all.

    Given the complexity and the fact that the PR does not touch the grammar, we will assume that the current state of the code (before the PR) already has the empty literal defined and parsed as ast.LiteralNull? That seems unlikely.

    Alternatively, note that the visitor test was added in this PR and it passes. So the author must have verified that the empty literal is parsed as ast.LiteralNull in the current codebase.

    We must check the history: perhaps the empty literal was already implemented and was mistakenly mapped to ast.LiteralNull?

    Since we are not able to change the grammar in this PR (and the PR doesn't show any grammar changes), we will accept that the current representation is ast.LiteralNull for the empty literal and note that it is a separate issue to fix the AST representation.

    However, we note that the isPlaceholderExpression function treats both ast.LiteralEmpty and ast.LiteralNull as placeholders. So if the grammar ever gets fixed to produce ast.LiteralEmpty for the empty literal, the code will still work.

    Therefore, we do not consider this a


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

The implementation already uses the existing `empty`/`null` literals for
unbound Java action arguments after review feedback rejected a dedicated
ellipsis placeholder token. The remaining helper name still used the old
placeholder terminology, which made the branch look like it still introduced
new syntax.

Rename the helper to the Java-action-specific behavior it implements and fix
the proposal example path to the actual .mdl fixture name.

Tests:
- go test ./mdl/executor -run 'TestBuildJavaAction_Empty|TestBuildJavaAction' -count=1
- make build
- ./bin/mxcli check mdl-examples/doctype-tests/empty_java_action_argument.mdl
- make test
@hjotha hjotha changed the title feat: support ellipsis placeholder expression feat: support empty Java action arguments May 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

AI Code Review

Critical Issues

  • None

Moderate Issues

  • None

Minor Issues

  • The PR title and description mention "ellipsis placeholder expression" but the actual changes implement support for the empty literal in Java-action arguments. This appears to be a documentation/metadata mismatch, though the code changes themselves are correct and focused.
  • The visitor test expects ast.LiteralNull for the empty literal, but the isEmptyJavaActionArgument function checks for both ast.LiteralEmpty and ast.LiteralNull. While functionally safe, this suggests potential inconsistency in how the empty literal is parsed (though no grammar changes are shown in the diff).

What Looks Good

  • The feature correctly addresses the round-trip preservation issue for unbound Java-action parameters by leveraging the existing empty literal with context-specific semantics.
  • Implementation follows established patterns: minimal changes to executor/builder, formatter updates for symmetric output, and appropriate BSON handling for both basic and microflow parameter types.
  • Comprehensive test coverage added at multiple levels: unit tests for builder/formatter/parser, visitor test for MDL acceptance, and doctest example for end-to-end validation.
  • Documentation updates are thorough: skill documentation, quick reference, proposal, and doctest example all clearly explain the feature's purpose and usage constraints.
  • The solution avoids introducing new syntax by reusing empty with documented contextual meaning, maintaining MDL's design principles.

Recommendation
Approve the PR. The minor documentation inconsistency does not affect the correctness of the implementation, and the feature is ready for merging.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

…psis-placeholder-expression

# Conflicts:
#	sdk/mpr/parser_microflow_test.go
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

AI Code Review

We are reviewing a pull request that adds support for the empty literal in the context of Java-action arguments.
The PR includes:

  • Documentation updates (skills, quick reference, proposal)
  • A new doctype test file
  • Changes to the executor (builder, formatter, and tests)
  • Changes to the visitor (test)
  • Changes to the SDK (new type and parser/writer updates)

Let's go through the checklist:

Overlap & duplication

  • The PR does not appear to duplicate existing functionality. It is adding support for empty in a specific context (Java-action arguments).
  • The proposal is new (PROPOSAL_microflow_empty_java_action_argument.md) and the doctype test is new.
  • We note that the empty literal already existed in MDL for other uses (e.g., in expressions to represent an empty list or null). The PR is extending its use in Java-action arguments.

MDL syntax design

  • The PR does not introduce new MDL syntax (keywords or grammar rules). It uses the existing empty literal in a new context.
  • Therefore, we don't need to check for new grammar rules, AST nodes, etc. (because the syntax is already present).
  • However, we must check that the use of empty in this context follows the design guidelines:
    • It uses the existing empty literal, which is a keyword (but note: the guidelines say to avoid keyword overloading, but here we are reusing an existing keyword in a new context).
    • The guidelines say: "No keyword overloading — each keyword has one consistent meaning". However, note that the empty literal already had multiple meanings (e.g., in expressions for empty list, null, etc.). The PR is adding another meaning in the context of Java-action arguments.

But note: the PR documentation states:
"Outside Java-action call arguments, empty keeps its normal MDL literal meaning."

So the meaning of empty is context-dependent. This is a form of overloading.

However, looking at the existing code, the empty literal is already used in multiple contexts (e.g., in expressions to represent an empty list, and in variable declarations as an initial value). The PR is adding a third context.

The design guidelines for MDL syntax (in .claude/skills/design-mdl-syntax.md) might have something to say about this. But note that the PR is not introducing a new keyword, it is reusing an existing one.

We must check if this overloading is acceptable. The PR argues that it is clearer than introducing a new placeholder token.

Since the PR is not adding new syntax (no changes to the grammar), we don't have to update the grammar, AST, visitor, etc. for the syntax itself. However, we do have changes in the executor to handle the empty literal in the Java-action argument context.

The PR does update the quick reference to document this usage.

Let's check the quick reference change: it adds a row under "Call from microflow" for "Empty argument". This is acceptable.

The PR also updates the write-microflows skill to document the usage.

We note that the PR does not change the grammar, so we don't have to check for:
- Grammar rule added to MDLParser.g4
- AST node type added
- Visitor handler
- etc.

However, note that the PR does add a test in the visitor to ensure that the parser accepts empty in Java-action arguments.

Full-stack consistency

Since the syntax (the empty literal) already existed, we don't need to add a new grammar rule. However, we do need to check that the feature is wired through the full pipeline for the new use case.

The PR includes:
- Executor changes (builder and formatter) to handle the empty literal in Java-action arguments.
- Changes to the SDK to support the new BSON types (MicroflowParameterValue) and to parse/write them.
- Tests in the executor and SDK.

We note that the PR does not add LSP wiring or DESCRIBE roundtrip specifically for this feature? But note:
- The PR states: "This lets describe output preserve empty BasicCodeActionParameterValue bindings and lets exec write them back"

So the DESCRIBE roundtrip is implicitly covered by the formatter changes (which output empty for empty values) and the parser changes (which accept empty in the argument).

We see that the PR adds a test in the formatter for formatting an empty Java-action argument (in cmd_microflows_format_action_test.go).

Also, the PR adds a doctype test file and tests in the executor builder.

Therefore, the full stack is covered.

Test coverage

  • The PR adds:
    • A new doctype test file: mdl-examples/doctype-tests/empty_java_action_argument.mdl
    • Unit tests in the executor builder: cmd_microflows_builder_java_action_test.go
    • Unit tests in the formatter: cmd_microflows_format_action_test.go
    • Unit tests in the visitor: visitor_test.go
    • Unit tests in the SDK: parser_javaactions_test.go, parser_microflow_test.go
  • The PR also runs validation commands (as listed in the body).

We note that the PR does not add any integration tests beyond the unit tests and the doctype test (which is tested via mxcli check and exec).

However, the doctype test is in the correct place and is tested.

Security & robustness

  • We don't see any obvious security issues (command injection, etc.) in the changes.
  • The changes are in the MDL executor and SDK, which are not exposed to untrusted input in a way that would cause command injection (the MDL script is trusted).
  • The changes do not introduce file I/O in hot paths.

Scope & atomicity

  • The PR is focused on one feature: supporting empty for Java-action arguments.
  • The changes are all related to this feature.

Code quality

  • The code changes look consistent and follow the existing patterns.
  • We note that the PR adds a helper function isEmptyJavaActionArgument in the executor builder.

However, we note that the PR does not update any manually maintained lists that might be a maintenance risk? We don't see any such lists in the changes.

The design documents (skills, quick reference) are updated to match the implementation.

Bugs & correctness

  • The PR includes tests that verify the correct BSON is generated and parsed.
  • The tests cover both the basic code action parameter value and the microflow parameter value cases.

We note that the visitor test expects the empty literal to be parsed as a LiteralExpr with kind LiteralNull?
In the visitor test:
if lit.Kind != ast.LiteralNull {
t.Fatalf("argument %d kind = %v, want LiteralNull", idx, lit.Kind)
}

But note: in the MDL lexer, both empty and null are tokenized as the same token? Let's check the lexer (not in the diff, but we know from context).

Actually, in the MDL lexer, empty and null are two different tokens? Or are they the same?

Looking at the visitor test, it is checking for ast.LiteralNull. But note that the PR also uses ast.LiteralEmpty in the builder tests.

In the builder test, we see:
{Name: "CompanyId", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},

And in the visitor test, we see:
lit.Kind != ast.LiteralNull

This seems inconsistent. However, note that the visitor test is checking that the parser accepts empty and null in this context and treats them the same?

The PR documentation says:
"Uses the existing empty literal as the MDL representation for intentionally unbound Java-action parameters."

And then in the visitor test, it uses both empty and null? Actually, the visitor test input uses empty twice, but the test expects LiteralNull?

Let me read the visitor test again:

    input := `CREATE MICROFLOW Synthetic.Check ()
    RETURNS Boolean AS $Success
    BEGIN
      $Total = CALL JAVA ACTION Synthetic.Recalculate(CompanyId = empty, RecalculateAll = true, ItemList = empty);
      RETURN true;
    END;`

And then it checks that the two empty arguments (CompanyId and ItemList) are of kind ast.LiteralNull.

This suggests that in the parser, both empty and null are being represented as ast.LiteralExpr with kind ast.LiteralNull?

But wait, the builder test uses ast.LiteralEmpty.

This discrepancy might be a bug? Or perhaps the lexer returns the same token for empty and null?

We don't have the lexer in the diff, but note that the PR does not change the lexer.

However, the PR does not need to change the lexer because the empty and null literals already existed.

We must check: in the existing code, how are empty and null represented in the AST?

Without the lexer, we cannot be sure. But note that the PR is not changing the lexer, so we assume that the existing representation is correct.

However, the visitor test expects LiteralNull for the empty literal in the Java-action argument. This might be because the PR author intended to treat empty and null the same in this context?

But note: the PR documentation says it uses the empty literal. It does not mention null.

The visitor test uses empty in the input, but then checks for LiteralNull. This is confusing.

Let's look at the commit that added the empty literal? We don't have that context.

Given that the PR is not changing the lexer, and the existing code already handles empty and null in expressions, we must assume that the AST representation for empty is ast.LiteralEmpty and for null is ast.LiteralNull.

Therefore, the visitor test is incorrect because it expects LiteralNull for an empty literal.

However, note that the PR says:
"This lets describe output preserve empty BasicCodeActionParameterValue bindings"

And the describe output uses the formatter, which we see in the formatter test outputs empty for an empty argument.

So the PR author might have intended that in the Java-action argument context, both empty and null are treated as the same (meaning unbound).

But the PR documentation only mentions empty.

We see that the helper function isEmptyJavaActionArgument checks for both ast.LiteralEmpty and ast.LiteralNull:

    func isEmptyJavaActionArgument(expr ast.Expression) bool {
        lit, ok := expr.(*ast.LiteralExpr)
        return ok && (lit.Kind == ast.LiteralEmpty || lit.Kind == ast.LiteralNull)
    }

So the executor treats both as empty.

Therefore, the visitor test should accept either? But the visitor test input uses empty and then expects LiteralNull? That doesn't match.

The visitor test input uses the word empty, so it should produce an AST node with ast.LiteralEmpty.

The test then checks for ast.LiteralNull and fails.

This is a bug in the test.

However, note that the PR might be relying on the fact that the lexer returns the same token for empty and null? We don't have the lexer.

Since we are not changing the lexer, and the PR does not show lexer changes, we must assume that the lexer produces different tokens for empty and null.

Therefore, the visitor test is wrong.

But wait: the PR also adds a test in the builder that uses ast.LiteralEmpty and expects the BSON to have an empty string. That test passes.

And the formatter test uses an empty string in the BSON and expects the formatter to output empty.

So the core functionality is correct.

The visitor test is the only place that seems to have an issue.

However, note that the visitor test is in the same PR and is testing the parser's acceptance of the empty literal in Java-action arguments. It should be checking for ast.LiteralEmpty (or at least not requiring ast.LiteralNull).

We must flag this as a bug in the test.

Alternatively, if the lexer does return the same token for empty and null (so that the parser cannot distinguish them), then the test is correct. But we don't have evidence of that.

Given the information we have, we note the inconsistency and suggest that the visitor test should be fixed to check for ast.LiteralEmpty (or at least allow both, but the test input uses empty).

However, since we are reviewing the PR and the PR author might have intended to treat empty and null the same in this context, and the helper function does, we can leave it to the author to clarify.

But note: the PR description says: "Uses the existing empty literal", so it is promoting the use of empty. The test should use empty and expect ast.LiteralEmpty.

We see that the builder test uses ast.LiteralEmpty and the formatter test uses the BSON empty string to produce empty in the output.

Therefore, the visitor test should be changed to expect ast.LiteralEmpty.

However, we are not the author and we are reviewing. We must decide if this is a blocker.

Given that the core functionality (executor, formatter, SDK) is tested and works, and the visitor test is only testing the parser's acceptance (which we know works from the builder test), we might consider this a minor issue.

But note: the visitor test is failing as written? We don't know because we don't have the lexer. However, if the lexer produces AST.LiteralEmpty for the token empty, then the test will fail.

We must assume that the lexer does produce AST.LiteralEmpty for empty because the builder test uses that.

Therefore, the visitor test is incorrect and should be fixed.

However, note that the PR also tests null in the same way? We don't see a test for null in the visitor.

The PR could add a separate test for null if desired, but the PR is about empty.

Given the above, we will note the visitor test issue as a minor issue.

Alternatively, we can note that the PR does not change the lexer and the existing representation of empty in the AST is ast.LiteralEmpty, so the test should be fixed.

But since we are not making the change, we will flag it.

Summary of findings:

  • The PR is well-scoped and focused.
  • It adds documentation, tests, and implementation.
  • The core functionality (executor, formatter, SDK) is correct and tested.
  • There is a minor issue in the visitor test: it expects ast.LiteralNull for an empty literal, but it should expect ast.LiteralEmpty (or at least not require ast.LiteralNull when the input is empty).

However, note that the PR might have intended to treat empty and null the same in this context, and the helper function does. So the test could be changed to check for either? But the test input uses empty.

We'll consider the visitor test issue as a minor issue that should be fixed, but it does not block the approval because the core functionality is correct and the test is only testing the parser's acceptance (which we know works from the builder test).

Alternatively, we can note that the PR author might have a reason (maybe the lexer returns the same token for empty and null in the context of Java-action arguments? but we don't see any context-specific lexer changes).

Since we don't have the lexer, we cannot be sure. But note that the PR does not change the lexer, so the lexer behavior is unchanged.

We will assume that the lexer returns different tokens for empty and null and therefore the visitor test is wrong.

However, to be safe, we note that the PR has tested the functionality via the builder and formatter tests, and the doctype test.

We will recommend that the visitor test be fixed, but we can approve the PR with a note to fix the test.

But note: the PR is already written and we are reviewing. We can either request a change to the test or note it as a minor issue and approve.

Given that the PR has extensive test coverage elsewhere, we can approve and note the issue in the review.

However, the checklist says we should flag issues. We will flag the visitor test as a minor issue.

Let's structure the review:

Critical Issues: None
Moderate Issues: None
Minor Issues:
- The visitor test (TestCallJavaActionAcceptsEmptyArguments) incorrectly expects the empty literal to be parsed as ast.LiteralNull when the input uses the token empty. It should expect ast.LiteralEmpty (or at least not require ast.LiteralNull for the empty token). This does not affect the functionality because the executor treats both empty and null as empty, but the test is misleading.

What Looks Good:
- The PR is well-scoped and focused on a single feature.
- It includes comprehensive documentation (skills, quick reference, proposal, doctype test).
- The implementation follows the existing patterns and reuses the existing empty literal appropriately.
- The executor changes correctly handle both basic code action parameters and microflow parameters.
- The SDK changes add the necessary BSON types and parser/writer logic.
- Test coverage is strong: unit tests for the builder, formatter, visitor, and SDK, plus a doctype test.
- The PR preserves the roundtrip symmetry for describe and exec


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@ako
Copy link
Copy Markdown
Collaborator

ako commented May 1, 2026

PR #336 Review — feat: support empty Java action arguments

Good update. The ... placeholder is gone; the existing empty literal is used correctly throughout. Parser, writer, formatter, and builder are all wired up, with test coverage at each layer. The doctype file uses the correct .mdl suffix.

One implementation detail worth documenting, and one missing test.

Minor

isEmptyJavaActionArgument checks both LiteralEmpty and LiteralNull without explanation

In the microflow expression visitor both empty and null keywords produce LiteralNullLiteralEmpty is only produced by the XPath visitor. So in practice only LiteralNull is ever seen here. The dual check in isEmptyJavaActionArgument is harmless but looks like defensive coding without context. A one-line comment — e.g. // the microflow expression visitor maps both EMPTY and NULL tokens to LiteralNull — would make this self-explanatory.

No writer unit test for MicroflowParameterValue serialization

TestParseCodeActionParameterValue_MicroflowParameterValue covers the parse side. serializeCodeActionParameterValue now has a *microflows.MicroflowParameterValue case but there is no corresponding writer test. The implementation is 5 straightforward lines, so the risk is low, but it would complete the test matrix alongside the existing parser test.


Otherwise looks good to merge.

@ako ako merged commit d091837 into mendixlabs:main May 1, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants