Skip to content

MDL list operations as one statement per Studio Pro activity; set mandatory under mdl 1 (#733) - #737

Merged
ako merged 11 commits into
mainfrom
feature/733-list-operation-statements
Sep 27, 2026
Merged

ako merged 11 commits into
mainfrom
feature/733-list-operation-statements

Conversation

@ako

@ako ako commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #733 (plan item 2.4 of PROPOSAL_mdl_beta_syntax_freeze.md; ADR-0010 R2/R12, ADR-0011).

What changes

List operations and aggregates are statements, one per Studio Pro activity. The operand is always a variable, so nesting cannot be written:

$Open  = filter $Orders by Status = Shop.Status.Open;          -- Filter
$Big   = filter $Orders where $currentObject/Total > 1000;     -- Filter by expression
$Order = find $Orders by Number = $Number;                     -- Find / find … where …
$S     = sort $Orders by OrderDate desc, Number asc;
$F     = head $Orders;   $R = tail $Orders;
$Page  = range $Orders offset 20 limit 10;
$U = union $A with $B;   $I = intersect $A with $B;   $L = subtract $B from $A;
$Has   = contains $Order in $Orders;   $Same = equals $A and $B;
$N = count $Orders;      $T = sum $Orders by Amount;   $T = sum $Orders of $currentObject/Price * 2;
$P = all $Orders where $currentObject/Paid;            $Q = any $Orders where …;
$Csv = reduce $Orders from '' as String using $currentResult + $currentObject/Name;
Construct mdl 0 (no header) mdl 1
statement forms above parse (new syntax, same meaning in both) parse
call form $x = head($L) … (not find/contains) parses, MDL-DEPR003 / MDL-DEPR004 (registered aliases; same AST — tested per operation) same, refused from mdl 2
$x = find(…) / $x = contains(…) today's guess from the arguments, warns MDL-V1-LIST refused; error names both readings
set $x = find(…) / contains(…) today's guess, warns MDL-V1-LIST when it would become a list op always the string function (MfSetStmt)
nested call, or list call after set (count(filter(…)), set $x = head($L)) kept, warns MDL-V1-LIST (nested operand still refused by MDL-LISTOP02) refused ("one statement per activity")
$x = <expr> without set (variable target) kept, warns MDL-V1-SET refused

describe prints the statement forms only under mdl 1: it writes the newest frozen version (langver.Frozen, still V0) or, when it runs inside a script with a newer header, that version. So a plain describe keeps today's call forms while mdl 1 is a preview, and mdl 1; describe microflow X; prints the new forms.

MDL-LISTOP02 is unreachable under mdl 1 (nesting is refused by the parser) and stays for mdl 0 scripts only; documented on the rule.

Vocabulary check against Studio Pro (scope item 1)

Checked against docs.mendix.com (refguide/list-operation, refguide/aggregate-list, raw source from mendix/docs) and the metamodel in modelsdk/gen/microflows. Operation names match the §4 table exactly (Union, Intersect, Subtract, Contains, Equals, Sort, Filter, Filter by expression, Find, Find by expression, Head, Tail, Range; Sum, Average, Count, Minimum, Maximum, All, Any, Reduce). The dialogs show field labels, not linking words, so by/where/of/with/from/in/and are not contradicted by any label. Deviations and notes:

  • Range: the dialog labels are Offset and Amount, not "limit". I kept the proposal's offset … limit … so it reads the same as retrieve … limit … offset … (one word for one concept, R8); amount is not accepted. Flagging it in case the "inputs are the ones the dialog asks for" rule should win.
  • Aggregates: the dialog's choice is "Aggregate with: Attribute / Expression". by <Attr> / of <expr> follow the proposal.
  • Reduce: the dialog order is Return type, Initial value, Expression. The statement is reduce $L from <initial> as <type> using <expr>, as in the proposal. It needed one new keyword, USING (added to the keyword rule, so it is still usable as an identifier).
  • Contains: docs say it "checks whether all elements of the second parameter are present in the first"; the metamodel stores an object variable, so contains $Object in $List.
  • Subtract: "the first parameter with the elements of the second removed", so subtract $B from $A = A − B, ListVariable1 = A.
  • Find/Filter by member: docs say "member (attributes or associations)", so by Member = value accepts an association too.

Design choices the ADRs did not settle

  • by vs where and the call form. ListOperationStmt.ByExpression is set by where. The call form sets it by the shape the flow builder already used (Member = value → by member, anything else → by expression), so filter($L, c) is an exact respelling of filter $L by … or … where … (same AST). where Status = 'x' is now always Filter by expression; ast.IsMemberEquality is the one predicate the visitor and the builder share.
  • by must be Member = value; anything else is an error pointing at where. An unresolvable member after by still falls back to the expression operation (unchanged behaviour of the call form).
  • The version reaches the statement builders through the parse tree (scriptLanguageVersion reads the header off the ProgramContext), because they are free functions without the Builder. Warnings and errors go through Builder.gate in ExitSetStatement / ExitListOperationStatement / ExitAggregateListStatement.
  • Registry Rewrite gains a Structural form, because a call form to statement form rewrite is not a keyword swap. TestRegistryExamplesRecordTheirCode still proves it by AST equality. fmt --upgrade does not exist yet, so nothing applies it.
  • $o/Attr = v without set is not gated: it is the change alias (Phase 3), not a reassignment.
  • One code per family (MDL-DEPR003 list operations, MDL-DEPR004 aggregates; MDL-V1-LIST, MDL-V1-SET) rather than one per operation. Each deprecation carries the operation as its subject.

Test plan (what I ran)

  • make build, make lint: pass.
  • go test ./mdl/... ./cmd/mxcli/...: pass (the orchestrator runs the whole repo).
  • go test -tags integration ./mdl/roundtrip/: pass; allowlist untouched (no entry fixed or added).
  • New tests:
    • mdl/visitor/visitor_list_activity_test.go: every §4 row under both versions; call form = statement form AST for each operation plus the deprecation code; find/contains gated; set $x = find(…) is MfSetStmt under mdl 1 (control: list op + note under mdl 0); nested call refused under mdl 1; set mandatory (with no-warning controls); by shape.
    • mdl/executor/cmd_microflows_format_list_activity_test.go: describe of every operation and aggregate under mdl 1, with a control that mdl 0 / nil context keeps the call form; the output parses back with no warnings.
    • mdl/executor/cmd_microflows_builder_list_activity_test.go: where Status = 'Open' builds FilterOperation, with a control that by builds FilterByAttribute.
    • mdl/roundtrip/list_activity_test.go (integration, PedApp): every §4 row executes, describes back verbatim under mdl 1;, and describe → exec → describe is stable. Control: a plain describe still prints the call form.
  • Revert checks (each one made the named tests fail, then restored):
    • drop s.ByExpression from listAttributeOperation → TestFilterWhereIsAlwaysByExpression fails (built FilterByAttribute);
    • make describeLanguage ignore the script version → 20 TestDescribeListActivityUnderMdl1 cases fail;
    • drop the version check in buildSetStatementNode → TestSetFindIsTheStringFunctionUnderMdl1 fails;
    • disable the three Exit… gating methods → the gating, alias and registry-example tests fail (TestSetIsMandatoryUnderMdl1, TestNestedListOperationRefusedUnderMdl1, TestFindContainsFunctionFormIsVersionGated, TestListFunctionFormIsAnAlias, TestFilterByNeedsMemberEquality, TestRegistryExamplesRecordTheirCode/MDL-DEPR003|004).
  • mxbuild on PedApp (copy): the list-activity microflow with every row adds 0 errors to mxcli docker check: 1002 before and after; the fixture has no theme, so CE6083 appears everywhere. Control: a microflow with an undefined list variable raises the count to 1003.
  • Studio Pro-authored list operations (TestApp copy, 41 microflows in WorkflowCommons and Microflows): for each, describe under mdl 1 → exec under mdl 1 vs describe → exec under mdl 0 on a separate copy. The resulting describe output is identical for 41 of 41, and mxcli docker check gives the same single pre-existing error on both copies.
  • Live Studio Pro over MCP (TestApp): the mdl 1 script created MyFirstModule.MDL733_ListActivities3 with every row except two. The MCP backend does not support range yet ("list operation ListRangeOperation is not yet supported by the MCP backend"), and PED's schema refuses Reduce on create ("Expected one of [Sum, …, Any]"). Both are backend gaps, not grammar problems. Three scratch microflows MDL733_ListActivities{,2,3} are left unsaved in the open Studio Pro; there is no delete or save tool without Concord.

Follow-ups (not in this PR)

  • fmt --upgrade implementation of the MDL-DEPR003/004 structural rewrite, plus adding the header.
  • docs-site / MDL_QUICK_REFERENCE.md and CLAUDE.md idiom 3 still show the call forms.
  • A pre-existing bug found while testing: $a/X * $b/Y is stored as $a/X * $b / Y in any expression, with the call form or the new one (filed as a separate task).
  • MCP backend: support ListRange; Reduce on create is refused by PED.

🤖 Generated with Claude Code

ako and others added 10 commits September 27, 2026 08:33
…t mandatory under mdl 1

Each Studio Pro List operation / Aggregate list activity is one statement
whose operand is a variable, so nesting cannot be written:

  $Open = filter $Orders by Status = M.Status.Open;
  $Big  = filter $Orders where $currentObject/Total > 1000;
  $N    = count $Open;
  $Csv  = reduce $Orders from '' as String using $currentResult + $currentObject/Name;

The statement forms parse under every version. The call forms keep
parsing: a respelling everywhere except find/contains, registered as
MDL-DEPR003 (list operations) and MDL-DEPR004 (aggregates), both building
the same AST. find(...)/contains(...) clash with the string functions, so
they, a list call after `set`, and a nested call are version-gated
(MDL-V1-LIST): kept and warned under mdl 0, refused under mdl 1, where
`set $x = find(...)` is always the string function. A reassignment without
`set` is refused under mdl 1 and warned under mdl 0 (MDL-V1-SET).

`where` is always Find/Filter by expression; `by` and the call form keep
the by-member reading when the condition is `Member = value`
(ast.IsMemberEquality, shared by visitor and flow builder).

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
describe writes the language of the newest frozen version, or of the
script it runs in when that is newer. While mdl 1 is a preview a plain
describe keeps the call form; inside an `mdl 1;` script it prints the
statement form, which executes back to the same microflow (PedApp test).

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…forms

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lementIDs, Writer.UpdateRawUnitPatch)

TransplantIDs pairs elements by type and position, which re-pairs the
surviving flows of a patched microflow onto their neighbours' $IDs after a
drop. A write that started from the stored bytes skips it; elision and the
storage-GUID guard still apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, replace, drop)

Splices a fragment into the raw stored unit: appends only the fragment's
objects and flows, rewires the flow around the target by its pointers and
the rewired end, moves nodes past the insertion point to make room, and
never rewrites an $ID. Save refuses a unit in which anything still points at
a removed element or two elements share an $ID. Refuses what it cannot do
safely: after a decision, before a join, inside a loop body, drop/replace of
a decision or of an activity with an error handler, and a placement that
would overlap an object. The modelsdk backend writes through
UpdateRawUnitPatch (canon.Reconcile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#736)

alter microflow|nanoflow M.F { insert after|before <target> { … }
replace <target> with { … } drop <target>; }. Targets are the #713 content
addresses, resolved against the stored flow before any operation runs.
Fragments are built with the create-microflow builder, seeded with the
flow's variables, and cut out of their start and end events. A fragment
variable that clashes with one the flow has, or one it reads that is not
declared upstream of the insertion point, is an error; so is dropping an
activity whose output is still read. Acceptance on PedApp VAL_Feedback:
only the new log, the two flows around it and the shifted positions differ;
an empty alter writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… handle

A handle has to parse as an alter target, and a target ends at the { of a
fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs the shared splice on the stored flow and sends the difference as
ped_update_document path operations in one update, after checking the live
document still matches the .mpr (PED addresses entries by index). Drop and
replace are refused: PED does not roll back a removal when an update fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loop body flows are stored in the unit's Flows list, not in the loop, so
removing a Studio Pro loop with two or more body activities left them
pointing at removed objects and Save refused the unit
(TestApp ACT_ConflictedWorkflowHelper_ApplyJumpTo). mx check 11.14 on the
dropped and replaced copies gives the baseline error list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each operation was checked against the flow as stored only. Measured
with mx check 11.14 on TestApp: two fragments declaring the same
variable gave CE0111, and a fragment reading a variable a drop in the
same statement removed gave CE0109, after "Altered microflow". The
context now tracks what earlier operations declared, read and removed.
Also count a fragment loop's iterator as the fragment's own, so loop
fragments are no longer refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Microflow list operations as one statement per Studio Pro activity; set mandatory (mdl 1)

1 participant