From c00ab83d9b1ecdf3909b17ca8f982b9669ac827a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 14:19:56 +0000 Subject: [PATCH] feat(grammar)!: require semicolons on microflow and nanoflow statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: every statement inside a microflow or nanoflow body must now end with `;`, including block terminators (`end if;`, `end loop;`, `end while;`, `end case;`). Omitting one is a parse error, where it was previously accepted. Semicolons were optional on all 54 body-statement alternatives, which made statement boundaries ambiguous whenever a keyword can both continue a statement and start one. The concrete case that surfaced it: adding a trailing `commit` modifier to create/change for #779 would let $a = create Mod.E commit $b; bind `commit` to the create rather than starting a commit activity. Diagnosing that per keyword does not scale; requiring the terminator removes the class. Scoped deliberately to microflow/nanoflow BODIES. The document terminator (`end;` / `end` then `/`) stays optional, as it is for pages and every other document type — measured, that variant breaks 88 of 234 example files, 75 of them pages, because block-bodied documents have never required a terminator and three styles coexist in the corpus (168 bare `}`, 56 `};`, 62 `}` + `/`). Tightening that is a separate, much larger decision. Impact measured over all 273 files in mdl-examples: 2 needed fixing, both genuine omissions (`end loop` with no `;`, a `log` line with no `;`), plus a `raise error` inside an on-error block. Example corpus is back at baseline parity (234 pass / 39 fail, 0 regressions). Test fixtures embedding MDL needed the same treatment in enginecompare, executor and visitor. Doc and skill code blocks were extracted and parse-checked against both the old and new parser: 0 regressions, so no documentation migration was required. Diagnostics were already precise for this ("line 143:2 missing ';' at 'return'"), so no error-reporting work was needed. Documented in write-microflows.md, write-nanoflows.md and MDL_QUICK_REFERENCE.md, each stating that the definition terminator remains optional. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA --- .claude/skills/mendix/write-microflows.md | 4 +- .claude/skills/mendix/write-nanoflows.md | 4 + docs/01-project/MDL_QUICK_REFERENCE.md | 6 + .../ledger-52-break-in-conditional.mdl | 2 +- .../doctype-tests/02-microflow-examples.mdl | 8 +- mdl/enginecompare/write_alter_test.go | 2 +- mdl/enginecompare/write_microflow_test.go | 14 +-- mdl/executor/validate_microflow_hints_test.go | 16 +-- mdl/grammar/domains/MDLMicroflow.g4 | 108 +++++++++--------- mdl/visitor/visitor_test.go | 4 +- 10 files changed, 90 insertions(+), 78 deletions(-) diff --git a/.claude/skills/mendix/write-microflows.md b/.claude/skills/mendix/write-microflows.md index 5b7185192..61318a030 100644 --- a/.claude/skills/mendix/write-microflows.md +++ b/.claude/skills/mendix/write-microflows.md @@ -95,7 +95,9 @@ end; - Parameters start with `$` prefix - Return variable must be declared or used - Every microflow must end with `return` statement -- Statements end with semicolon `;` +- Every body statement ends with a semicolon `;` — **required**, not optional. This + includes block terminators: `end if;`, `end loop;`, `end while;`, `end case;`. + A missing one is a parse error (`missing ';' at 'return'`), not a warning. - Microflow ends with `/` separator ### Parameter Types diff --git a/.claude/skills/mendix/write-nanoflows.md b/.claude/skills/mendix/write-nanoflows.md index 6cb756bcd..52e1b30fa 100644 --- a/.claude/skills/mendix/write-nanoflows.md +++ b/.claude/skills/mendix/write-nanoflows.md @@ -66,6 +66,10 @@ BEGIN END; ``` +**Every body statement ends with a semicolon `;`** — required, not optional, exactly as +in microflows. That includes block terminators: `end if;`, `end loop;`, `end while;`, +`end case;`. A missing one is a parse error (`missing ';' at 'return'`), not a warning. + ## Naming Convention Nanoflow names use the `NAV_` prefix by convention: diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 95fdad7c2..ecd593ebb 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -258,6 +258,12 @@ return type (`System.ConsumedODataConfiguration` vs ## Microflows - Supported Statements +**Semicolons are mandatory inside a microflow or nanoflow body.** Every statement ends +with `;`, including block terminators (`end if;`, `end loop;`, `end while;`, `end case;`). +Omitting one is a parse error (`missing ';' at 'return'`). The terminator on the +*definition* itself (`end;` / `end` followed by `/`) is unchanged and still optional, as +it is for pages. + | Statement | Syntax | Notes | |-----------|--------|-------| | Variable declaration | `declare $Var type = value;` | Primitives: String, Integer, Boolean, Decimal, DateTime | diff --git a/mdl-examples/bug-tests/ledger-52-break-in-conditional.mdl b/mdl-examples/bug-tests/ledger-52-break-in-conditional.mdl index 94e1265cd..fce56aa77 100644 --- a/mdl-examples/bug-tests/ledger-52-break-in-conditional.mdl +++ b/mdl-examples/bug-tests/ledger-52-break-in-conditional.mdl @@ -34,6 +34,6 @@ begin if $R/Active then break; -- MDL051: crashes mx check (unloadable model) end if; - end loop + end loop; return true; end diff --git a/mdl-examples/doctype-tests/02-microflow-examples.mdl b/mdl-examples/doctype-tests/02-microflow-examples.mdl index b35b58d54..18735093f 100644 --- a/mdl-examples/doctype-tests/02-microflow-examples.mdl +++ b/mdl-examples/doctype-tests/02-microflow-examples.mdl @@ -139,7 +139,7 @@ create or replace microflow MfTest.M001_HelloWorld () returns boolean as $success begin declare $success boolean = true; - log trace node 'TEST' 'List received ' + log trace node 'TEST' 'List received '; return $success; end; / @@ -163,9 +163,9 @@ end; create or replace microflow MfTest.M001_HelloWorld () returns boolean as $success begin - log trace node 'TEST' '> before' + log trace node 'TEST' '> before'; declare $success boolean = true; - log trace node 'TEST' '< After' + log trace node 'TEST' '< After'; return $success; end; / @@ -2010,7 +2010,7 @@ begin log error node 'OrderService' 'Failed to update order status'; change $Order (status = 'ERROR'); commit $Order; - raise error + raise error; }; set $success = true; diff --git a/mdl/enginecompare/write_alter_test.go b/mdl/enginecompare/write_alter_test.go index 035b6f661..d172b062d 100644 --- a/mdl/enginecompare/write_alter_test.go +++ b/mdl/enginecompare/write_alter_test.go @@ -75,7 +75,7 @@ func TestWriteParity_AlterKeepsAccessRule(t *testing.T) { func TestWriteParity_AlterKeepsEventHandler(t *testing.T) { const ent = "MyFirstModule.EvtEnt" setup := []string{ - "CREATE MICROFLOW MyFirstModule.OnEvt () RETURNS BOOLEAN BEGIN RETURN true END", + "CREATE MICROFLOW MyFirstModule.OnEvt () RETURNS BOOLEAN BEGIN RETURN true; END", "CREATE PERSISTENT ENTITY " + ent + " ( Code: string(20), Rank: integer )", "ALTER ENTITY " + ent + " ADD EVENT HANDLER ON BEFORE COMMIT CALL MyFirstModule.OnEvt RAISE ERROR", } diff --git a/mdl/enginecompare/write_microflow_test.go b/mdl/enginecompare/write_microflow_test.go index db46785fe..ea0e1531b 100644 --- a/mdl/enginecompare/write_microflow_test.go +++ b/mdl/enginecompare/write_microflow_test.go @@ -49,9 +49,9 @@ func TestWriteParity_Microflow_ObjectOps(t *testing.T) { // element + parameter mappings (marker-2 list). func TestWriteParity_Microflow_Calls(t *testing.T) { setup := []string{ - "CREATE MICROFLOW MyFirstModule.CTarget () RETURNS BOOLEAN BEGIN RETURN true END", - "CREATE MICROFLOW MyFirstModule.CTargetP (Val: string) RETURNS STRING BEGIN RETURN $Val END", - "CREATE NANOFLOW MyFirstModule.NTarget () RETURNS BOOLEAN BEGIN RETURN true END", + "CREATE MICROFLOW MyFirstModule.CTarget () RETURNS BOOLEAN BEGIN RETURN true; END", + "CREATE MICROFLOW MyFirstModule.CTargetP (Val: string) RETURNS STRING BEGIN RETURN $Val; END", + "CREATE NANOFLOW MyFirstModule.NTarget () RETURNS BOOLEAN BEGIN RETURN true; END", } cases := []struct{ name, stmt, mf string }{ {"MicroflowNoArgs", "CREATE MICROFLOW MyFirstModule.MfCall () BEGIN call microflow MyFirstModule.CTarget(); END", "MfCall"}, @@ -91,10 +91,10 @@ func TestWriteParity_Microflow_Loops(t *testing.T) { cases := []struct{ name, stmt, mf string }{ {"IterateList", "CREATE MICROFLOW MyFirstModule.MfLoop (Items: list of MyFirstModule.LThing) BEGIN " + - "loop $It in $Items begin commit $It; end loop END", "MfLoop"}, + "loop $It in $Items begin commit $It; end loop; END", "MfLoop"}, {"While", "CREATE MICROFLOW MyFirstModule.MfWhile (Item: MyFirstModule.LThing) BEGIN " + - "while $Item/Code != '' begin commit $Item; end while END", "MfWhile"}, + "while $Item/Code != '' begin commit $Item; end while; END", "MfWhile"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -188,8 +188,8 @@ func TestWriteParity_Microflow_Retrieve(t *testing.T) { // against legacy, group by group. Skeleton = start → end, boolean return. func TestWriteParity_Microflow(t *testing.T) { cases := []struct{ name, stmt, mf string }{ - {"Skeleton", "CREATE MICROFLOW MyFirstModule.MfEmpty () RETURNS BOOLEAN BEGIN RETURN true END", "MfEmpty"}, - {"Parameters", "CREATE MICROFLOW MyFirstModule.MfParams (Count: integer, Label: string) RETURNS BOOLEAN BEGIN RETURN true END", "MfParams"}, + {"Skeleton", "CREATE MICROFLOW MyFirstModule.MfEmpty () RETURNS BOOLEAN BEGIN RETURN true; END", "MfEmpty"}, + {"Parameters", "CREATE MICROFLOW MyFirstModule.MfParams (Count: integer, Label: string) RETURNS BOOLEAN BEGIN RETURN true; END", "MfParams"}, {"VoidReturn", "CREATE MICROFLOW MyFirstModule.MfVoid () BEGIN END", "MfVoid"}, } for _, c := range cases { diff --git a/mdl/executor/validate_microflow_hints_test.go b/mdl/executor/validate_microflow_hints_test.go index d482d4f19..2b09e7c27 100644 --- a/mdl/executor/validate_microflow_hints_test.go +++ b/mdl/executor/validate_microflow_hints_test.go @@ -170,10 +170,10 @@ func TestValidateMicroflow_AssociationObjectArg(t *testing.T) { // `continue` form reached users as a corrupt project. func TestValidateMicroflow_ConditionalBreakAccepted(t *testing.T) { bodies := []string{ - "loop $R in $L begin if $R/Active then break; end if; end loop", - "loop $R in $L begin if $R/Active then continue; end if; end loop", - "loop $R in $L begin if $R/Active then if $R/Active then break; end if; end if; end loop", - "loop $R in $L begin break; end loop", + "loop $R in $L begin if $R/Active then break; end if; end loop;", + "loop $R in $L begin if $R/Active then continue; end if; end loop;", + "loop $R in $L begin if $R/Active then if $R/Active then break; end if; end if; end loop;", + "loop $R in $L begin break; end loop;", } for _, body := range bodies { t.Run(body, func(t *testing.T) { @@ -231,10 +231,10 @@ func TestValidateMicroflow_DuplicateLoopVariable(t *testing.T) { body string wantMDL bool }{ - {"two loops same iterator", "loop $R in $L begin set $x = 1; end loop loop $R in $L begin set $y = 1; end loop", true}, - {"nested loop reuses outer iterator", "loop $R in $L begin loop $R in $L begin set $x = 1; end loop end loop", true}, - {"distinct iterators are fine", "loop $R in $L begin set $x = 1; end loop loop $C in $L begin set $y = 1; end loop", false}, - {"single loop is fine", "loop $R in $L begin set $x = 1; end loop", false}, + {"two loops same iterator", "loop $R in $L begin set $x = 1; end loop; loop $R in $L begin set $y = 1; end loop;", true}, + {"nested loop reuses outer iterator", "loop $R in $L begin loop $R in $L begin set $x = 1; end loop; end loop;", true}, + {"distinct iterators are fine", "loop $R in $L begin set $x = 1; end loop; loop $C in $L begin set $y = 1; end loop;", false}, + {"single loop is fine", "loop $R in $L begin set $x = 1; end loop;", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/mdl/grammar/domains/MDLMicroflow.g4 b/mdl/grammar/domains/MDLMicroflow.g4 index 574536993..d47b0832c 100644 --- a/mdl/grammar/domains/MDLMicroflow.g4 +++ b/mdl/grammar/domains/MDLMicroflow.g4 @@ -116,60 +116,60 @@ microflowBody * not at the grammar level. */ microflowStatement - : annotation* declareStatement SEMICOLON? - | annotation* caseStatement SEMICOLON? - | annotation* inheritanceSplitStatement SEMICOLON? - | annotation* castObjectStatement SEMICOLON? - | annotation* setStatement SEMICOLON? - | annotation* createListStatement SEMICOLON? // Must be before createObjectStatement to match "CREATE LIST OF" - | annotation* createObjectStatement SEMICOLON? - | annotation* changeObjectStatement SEMICOLON? - | annotation* commitStatement SEMICOLON? - | annotation* deleteObjectStatement SEMICOLON? - | annotation* rollbackStatement SEMICOLON? - | annotation* retrieveStatement SEMICOLON? - | annotation* ifStatement SEMICOLON? - | annotation* loopStatement SEMICOLON? - | annotation* whileStatement SEMICOLON? - | annotation* continueStatement SEMICOLON? - | annotation* breakStatement SEMICOLON? - | annotation* returnStatement SEMICOLON? - | annotation* raiseErrorStatement SEMICOLON? - | annotation* logStatement SEMICOLON? - | annotation* callMicroflowStatement SEMICOLON? - | annotation* callNanoflowStatement SEMICOLON? - | annotation* callJavaActionStatement SEMICOLON? - | annotation* callJavaScriptActionStatement SEMICOLON? - | annotation* callWebServiceStatement SEMICOLON? - | annotation* executeDatabaseQueryStatement SEMICOLON? - | annotation* callExternalActionStatement SEMICOLON? - | annotation* showPageStatement SEMICOLON? - | annotation* closePageStatement SEMICOLON? - | annotation* showHomePageStatement SEMICOLON? - | annotation* showMessageStatement SEMICOLON? - | annotation* downloadFileStatement SEMICOLON? - | annotation* throwStatement SEMICOLON? - | annotation* listOperationStatement SEMICOLON? - | annotation* aggregateListStatement SEMICOLON? - | annotation* addToListStatement SEMICOLON? - | annotation* removeFromListStatement SEMICOLON? - | annotation* validationFeedbackStatement SEMICOLON? - | annotation* restCallStatement SEMICOLON? - | annotation* sendRestRequestStatement SEMICOLON? - | annotation* importFromMappingStatement SEMICOLON? - | annotation* exportToMappingStatement SEMICOLON? - | annotation* transformJsonStatement SEMICOLON? - | annotation* callWorkflowStatement SEMICOLON? - | annotation* getWorkflowDataStatement SEMICOLON? - | annotation* getWorkflowsStatement SEMICOLON? - | annotation* getWorkflowActivityRecordsStatement SEMICOLON? - | annotation* workflowOperationStatement SEMICOLON? - | annotation* setTaskOutcomeStatement SEMICOLON? - | annotation* openUserTaskStatement SEMICOLON? - | annotation* notifyWorkflowStatement SEMICOLON? - | annotation* openWorkflowStatement SEMICOLON? - | annotation* lockWorkflowStatement SEMICOLON? - | annotation* unlockWorkflowStatement SEMICOLON? + : annotation* declareStatement SEMICOLON + | annotation* caseStatement SEMICOLON + | annotation* inheritanceSplitStatement SEMICOLON + | annotation* castObjectStatement SEMICOLON + | annotation* setStatement SEMICOLON + | annotation* createListStatement SEMICOLON // Must be before createObjectStatement to match "CREATE LIST OF" + | annotation* createObjectStatement SEMICOLON + | annotation* changeObjectStatement SEMICOLON + | annotation* commitStatement SEMICOLON + | annotation* deleteObjectStatement SEMICOLON + | annotation* rollbackStatement SEMICOLON + | annotation* retrieveStatement SEMICOLON + | annotation* ifStatement SEMICOLON + | annotation* loopStatement SEMICOLON + | annotation* whileStatement SEMICOLON + | annotation* continueStatement SEMICOLON + | annotation* breakStatement SEMICOLON + | annotation* returnStatement SEMICOLON + | annotation* raiseErrorStatement SEMICOLON + | annotation* logStatement SEMICOLON + | annotation* callMicroflowStatement SEMICOLON + | annotation* callNanoflowStatement SEMICOLON + | annotation* callJavaActionStatement SEMICOLON + | annotation* callJavaScriptActionStatement SEMICOLON + | annotation* callWebServiceStatement SEMICOLON + | annotation* executeDatabaseQueryStatement SEMICOLON + | annotation* callExternalActionStatement SEMICOLON + | annotation* showPageStatement SEMICOLON + | annotation* closePageStatement SEMICOLON + | annotation* showHomePageStatement SEMICOLON + | annotation* showMessageStatement SEMICOLON + | annotation* downloadFileStatement SEMICOLON + | annotation* throwStatement SEMICOLON + | annotation* listOperationStatement SEMICOLON + | annotation* aggregateListStatement SEMICOLON + | annotation* addToListStatement SEMICOLON + | annotation* removeFromListStatement SEMICOLON + | annotation* validationFeedbackStatement SEMICOLON + | annotation* restCallStatement SEMICOLON + | annotation* sendRestRequestStatement SEMICOLON + | annotation* importFromMappingStatement SEMICOLON + | annotation* exportToMappingStatement SEMICOLON + | annotation* transformJsonStatement SEMICOLON + | annotation* callWorkflowStatement SEMICOLON + | annotation* getWorkflowDataStatement SEMICOLON + | annotation* getWorkflowsStatement SEMICOLON + | annotation* getWorkflowActivityRecordsStatement SEMICOLON + | annotation* workflowOperationStatement SEMICOLON + | annotation* setTaskOutcomeStatement SEMICOLON + | annotation* openUserTaskStatement SEMICOLON + | annotation* notifyWorkflowStatement SEMICOLON + | annotation* openWorkflowStatement SEMICOLON + | annotation* lockWorkflowStatement SEMICOLON + | annotation* unlockWorkflowStatement SEMICOLON ; declareStatement diff --git a/mdl/visitor/visitor_test.go b/mdl/visitor/visitor_test.go index 5868dd1ff..75254310f 100644 --- a/mdl/visitor/visitor_test.go +++ b/mdl/visitor/visitor_test.go @@ -14,8 +14,8 @@ func TestMicroflowParsing(t *testing.T) { input := `CREATE MICROFLOW MyModule.HelloWorld () RETURNS String BEGIN - DECLARE $greeting String = 'Hello, World!' - RETURN $greeting + DECLARE $greeting String = 'Hello, World!'; + RETURN $greeting; END;` prog, errs := Build(input)