@@ -68,6 +68,103 @@ func convertASTToMicroflowDataType(dt ast.DataType, entityResolver func(ast.Qual
6868 }
6969}
7070
71+ // mendixBuiltinFunctions is the canonical spelling of every built-in Mendix
72+ // expression function. The expression runtime is case-sensitive: it only
73+ // recognises these names as spelt here (lower-case with camelCase for
74+ // compound words). Emitting an alternative spelling causes CE0117
75+ // ("Error(s) in expression.") on Studio Pro validation.
76+ //
77+ // Source: https://docs.mendix.com/refguide/expressions/ and the linked
78+ // function-specific pages (string, math, date arithmetic, parse/format,
79+ // trim-to-date, list operations, aggregates, type conversions).
80+ //
81+ // The map key is the upper-case spelling for case-insensitive lookup; the
82+ // value is the runtime-accepted canonical spelling. Custom user-defined
83+ // java actions, sub-microflows, and unknown function names pass through
84+ // unchanged so user case is preserved.
85+ var mendixBuiltinFunctions = func () map [string ]string {
86+ canonical := []string {
87+ // List operations
88+ "head" , "tail" , "find" , "filter" , "sort" , "union" ,
89+ "intersect" , "subtract" , "contains" , "equals" , "range" ,
90+ // List aggregates
91+ "count" , "sum" , "average" , "minimum" , "maximum" ,
92+ "allTrue" , "anyTrue" ,
93+ // String functions (docs.mendix.com/refguide/string-function-calls)
94+ "toUpperCase" , "toLowerCase" , "trim" , "length" , "substring" ,
95+ "findLast" , "replaceAll" , "replaceFirst" , "startsWith" , "endsWith" ,
96+ "isMatch" , "isInvariantMatch" , "stringFromRegex" , "stringListFromRegex" ,
97+ "urlEncode" , "urlDecode" , "reverse" , "indexOf" ,
98+ // Math functions (docs.mendix.com/refguide/mathematical-function-calls)
99+ "abs" , "ceil" , "floor" , "round" , "max" , "min" , "pow" ,
100+ "sqrt" , "ln" , "log10" , "random" , "rand" ,
101+ // Date creation (docs.mendix.com/refguide/date-creation)
102+ "dateTime" , "dateTimeUTC" ,
103+ // Begin-of-date / end-of-date / trim-to-date
104+ "trimToDays" , "trimToHours" , "trimToMinutes" , "trimToSeconds" ,
105+ "trimToDaysUTC" , "trimToHoursUTC" , "trimToMinutesUTC" , "trimToSecondsUTC" ,
106+ "beginOfDay" , "beginOfWeek" , "beginOfMonth" , "beginOfYear" ,
107+ "beginOfDayUTC" , "beginOfWeekUTC" , "beginOfMonthUTC" , "beginOfYearUTC" ,
108+ "endOfDay" , "endOfWeek" , "endOfMonth" , "endOfYear" ,
109+ "endOfDayUTC" , "endOfWeekUTC" , "endOfMonthUTC" , "endOfYearUTC" ,
110+ // Between-date functions
111+ "millisecondsBetween" , "secondsBetween" , "minutesBetween" ,
112+ "hoursBetween" , "daysBetween" , "weeksBetween" , "monthsBetween" ,
113+ "yearsBetween" , "calendarDaysBetween" , "calendarMonthsBetween" ,
114+ "calendarYearsBetween" ,
115+ // Add-date functions
116+ "addMilliseconds" , "addSeconds" , "addMinutes" , "addHours" ,
117+ "addDays" , "addWeeks" , "addMonths" , "addYears" ,
118+ "addDaysUTC" , "addWeeksUTC" , "addMonthsUTC" , "addYearsUTC" ,
119+ // Subtract-date functions
120+ "subtractMilliseconds" , "subtractSeconds" , "subtractMinutes" ,
121+ "subtractHours" , "subtractDays" , "subtractWeeks" , "subtractMonths" ,
122+ "subtractYears" , "subtractDaysUTC" , "subtractWeeksUTC" ,
123+ "subtractMonthsUTC" , "subtractYearsUTC" ,
124+ // Day-of / timestamp conversion helpers
125+ "dayOfWeek" , "dayOfWeekFromDateTime" , "weekOfYearFromDateTime" ,
126+ "dayOfYearFromDateTime" , "daysInMonth" , "daysInYear" ,
127+ "dateTimeToEpoch" , "epochToDateTime" ,
128+ // Parse / format (parse-and-format-date, parse-and-format-decimal)
129+ "formatDateTime" , "formatDateTimeUTC" , "parseDateTime" , "parseDateTimeUTC" ,
130+ "parseInteger" , "parseLong" , "parseDecimal" , "formatDecimal" ,
131+ // To-string / length (to-string, length refguide pages)
132+ "toString" , "toBoolean" , "toFloat" ,
133+ // Enumeration helpers
134+ "getCaption" , "getKey" ,
135+ // Miscellaneous
136+ "if" , "empty" , "isNew" , "isAnonymous" ,
137+ // Boolean operators expressed as functions (true(), false())
138+ "true" , "false" ,
139+ // Not / and / or appear as operators, not function calls — omitted.
140+ }
141+ m := make (map [string ]string , len (canonical ))
142+ for _ , c := range canonical {
143+ m [strings .ToUpper (c )] = c
144+ }
145+ return m
146+ }()
147+
148+ // mendixFunctionName normalises the case of built-in Mendix expression
149+ // functions. The visitor canonicalises list / aggregate operations in
150+ // UPPERCASE for AST dispatch; the expression runtime only recognises the
151+ // documented camelCase spelling. For every built-in Mendix function we
152+ // always emit the canonical spelling so that:
153+ //
154+ // - round-tripping a pristine microflow never mutates `find(...)` into
155+ // `FIND(...)` (which Studio Pro rejects with CE0117).
156+ // - LLM-generated MDL with accidental capitalisation (`LENGTH(...)`,
157+ // `ToString(...)`) still validates when executed.
158+ //
159+ // Custom (user-defined) java actions, sub-microflows and entity member
160+ // references pass through unchanged so user case is preserved.
161+ func mendixFunctionName (name string ) string {
162+ if canonical , ok := mendixBuiltinFunctions [strings .ToUpper (name )]; ok {
163+ return canonical
164+ }
165+ return name
166+ }
167+
71168// expressionToString converts an AST Expression to a Mendix expression string.
72169func expressionToString (expr ast.Expression ) string {
73170 // Check for nil interface
@@ -119,7 +216,7 @@ func expressionToString(expr ast.Expression) string {
119216 for _ , arg := range e .Arguments {
120217 args = append (args , expressionToString (arg ))
121218 }
122- return e .Name + "(" + strings .Join (args , ", " ) + ")"
219+ return mendixFunctionName ( e .Name ) + "(" + strings .Join (args , ", " ) + ")"
123220 case * ast.TokenExpr :
124221 return "[%" + e .Token + "%]"
125222 case * ast.ParenExpr :
@@ -181,7 +278,7 @@ func expressionToXPath(expr ast.Expression) string {
181278 for _ , arg := range e .Arguments {
182279 args = append (args , expressionToXPath (arg ))
183280 }
184- return e .Name + "(" + strings .Join (args , ", " ) + ")"
281+ return mendixFunctionName ( e .Name ) + "(" + strings .Join (args , ", " ) + ")"
185282 case * ast.LiteralExpr :
186283 if e .Kind == ast .LiteralEmpty {
187284 return "empty"
0 commit comments