Skip to content

docs(wiki): digest the mdl/grammar findings into three bug-pattern pages - #359

Merged
ako merged 5 commits into
docs/bug-patterns-backendfrom
docs/bug-patterns-grammar
Sep 1, 2026
Merged

docs(wiki): digest the mdl/grammar findings into three bug-pattern pages#359
ako merged 5 commits into
docs/bug-patterns-backendfrom
docs/bug-patterns-grammar

Conversation

@ako

@ako ako commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Stacked on #358 — and note that #356, #357 and #358 are all still open (main is at #352). This branch also merges current main in, so the chain stays current.

49 of the 53 mdl/grammar findings (92%) now fall under a named class. Three pages are new; the rest reuse describe-round-trip-gaps and silent-property-drop.

page thesis
capability-gap-as-parse-error an unspelled capability presents as no viable alternative — and the user concludes the feature is impossible
keyword-collisions the distinguishing question is how a collision fails, not whether one happens
scripts-that-cannot-rerun statement-level idempotence, which is not the write-level idempotence people mean

capability-gap-as-parse-error

The distinctive class in this area, and the one with the clearest cost. A parse error is indistinguishable from a mistake the user made, so the reports are not feature requests — they are workarounds:

  • an admin screen hand-rolled as five pages with bespoke SCSS, because a tab container was believed not to exist (two days);
  • "use a Java action" for a binary upload;
  • "go to Studio Pro" as the standing answer to translating an app;
  • a headless pipeline that ends in a manual step, because anonymous access has no statement.

The gap is bidirectional. MDL not being able to write a construct is half of it — DESCRIBE of a document that has one must do something, and dropping it silently is the common outcome. In the mapping findings it did worse: emitted MDL that parsed and rebuilt a different document, which looks like a successful round trip and is not.

Two method points recorded: a narrow/wide statement pair is a whitelist that gets extended one bug report at a time (SET vs REPLACE), so the fix is to reuse the wide rule rather than add another case to the narrow one; and "not in the metamodel" is not a conclusion until the namespace is right — a grep under one prefix ruled out a capability that lives under another, and one 4 KB Studio Pro unit settled in minutes what the grep had "disproved".

keyword-collisions

A parse error is recoverable — the user quotes the name. The same collision resolving to a different valid parse is not: a widget conditional calling trim() was silently dropped, so the page lost its visibility rule and built cleanly.

A grammar alternative and its visitor case are one change. Accepting an unquoted -7 in XPath without the matching AST case made the constraint serialize as [Amount > ] — a dropped operand instead of a loud error, caught only because the visitor has a round-trip helper. And a relaxation is proven with a control binary (stash the .g4, regenerate, sweep every example with both), not by reading: 13 scripts failed with the change, the same 13, all pre-existing.

One correction before committing

The coverage figure in CLAUDE.md said 98%. The measurement says 92%, and I'd written the first number from memory rather than from the script. Corrected in the same commit, and noted in the commit message.

Where the digest stands

area findings covered
mdl/executor 248 83%
cmd/mxcli 95 92%
mdl/backend 83 99%
mdl/grammar 53 92%
mdl/visitor 28

🤖 Generated with Claude Code

claude and others added 5 commits September 1, 2026 06:33
`filter($L, Amount > 0)` was stored as `Microflows$FilterByExpression` with
the authored text verbatim. Mendix evaluates that expression once per item
with the item bound to `$currentObject`, where a bare attribute name is not
valid — so mxbuild reported CE0117 while `mxcli check` passed.

This is the unfinished half of bug #343. That fix rerouted `attr = value` to
`Microflows$Filter` (filter BY ATTRIBUTE), which takes a member name rather
than an expression and so accepts the bare form. Every other predicate still
fell through to the expression shape, which made the split turn on the
OPERATOR and be invisible to the author: `Status = 'x'` built and
`Status != 'x'` did not.

Not a Mendix version change, despite the report: the same 19-microflow probe
produces the identical 7 errors on mxbuild 11.11.0 and 11.13.0.

- A bare name that provably resolves to a member of the list's element entity
  is rewritten to `$currentObject/<member>` (an association keeps its module
  qualifier, an attribute does not). A name that does not resolve is refused
  rather than left to surface as CE0117. When the element entity cannot be
  determined nothing is proven either way, so the predicate is passed through.
- The predicate can reach the builder as a frozen `SourceExpr`, so the rewrite
  patches the source text too, skipping single-quoted literals — the `'Amount'`
  in `filter($L, Qty > 0 and Status != 'Amount')` must survive untouched.
- MDL-LISTOP01 refuses an iterator variable that is not in scope, pre-empting
  CE0109. It keys on scope rather than on the name, so `$item` stays valid as
  an enclosing loop's iterator — the shape of CLAUDE.md's O(N) `find` idiom.
- The `syntax microflow.list-operations` example no longer teaches a form that
  only compiles through the `=` reroute, and notes that SORT is not an
  expression, so a bare attribute is the only spelling there.

Control: stubbing the qualifier takes the repro from 0 to 7 × CE0117 with the
two `=` cases staying green, which is also the #343 regression guard.

Repros: mdl-examples/bug-tests/1002-filter-find-bare-attribute.mdl (0 errors
on mxbuild 11.13.0) and 1002-filter-bad-iterator.fail.mdl.

Closes mendixlabs#1002

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Follow-up to the mendixlabs#1002 fix, covering the surfaces a user actually hits the
new messages from.

- docs-site error-messages appendix: MDL-LISTOP01 under "mxcli Check Errors"
  (alongside the MDL-WIDGET rules), and the unresolvable-bare-name refusal
  under "mxcli Execution Errors". The MDL-LISTOP01 entry spells out that the
  rule keys on scope rather than on the name, so `$item` as an enclosing
  loop's iterator is not flagged.
- `mxcli syntax errors.execution`: both errors with their cause and fix, and
  CE0117/CE0109/MDL-LISTOP01 as keywords on that topic and on
  microflow.list-operations — keywords are emitted in the `--json` index that
  agents cache, so a CE number now leads back to the right topic (the CE0133
  precedent).
- CLAUDE.md: the always-in-context O(N) `find` idiom uses `$item`, which reads
  like something MDL-LISTOP01 forbids. One clause records that the iterator
  there is in scope and stays valid, so the documented idiom is not "fixed"
  into something worse later.

No list-operations page exists under docs-site/src/language/, and the check
rules do not appear in `mxcli lint --list-rules` (verified), so neither needed
a change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
fix(mdl): a bare attribute in FILTER/FIND built an expression Mendix rejects (mendixlabs#1002)
49 of the 53 mdl/grammar findings (92%) now fall under a named class.
Three pages are new; the rest reuse describe-round-trip-gaps and
silent-property-drop.

- capability-gap-as-parse-error (36 touched, ~20 primary). The
  distinctive one. Someone tries to express something Mendix supports,
  the parser says `no viable alternative`, and they conclude the FEATURE
  is impossible — a parse error is indistinguishable from a mistake they
  made. So the reports in this class are not feature requests, they are
  workarounds: an admin screen hand-rolled as five pages because a tab
  container was believed not to exist, "use a Java action" for a binary
  upload, "go to Studio Pro" as the standing answer to translation, a
  headless pipeline ending in a manual step.

  The gap is bidirectional — DESCRIBE of a document that HAS the
  construct must do something, and in the mapping findings it emitted
  MDL that parsed and rebuilt a different document, which is worse than
  a parse error. Also records that a narrow/wide statement pair (SET vs
  REPLACE) is a whitelist extended one bug report at a time, and that
  "not in the metamodel" is not a conclusion until the namespace is
  right.

- keyword-collisions (22 touched, ~8 primary). The distinguishing
  question is how a collision fails, not whether one happens: a parse
  error is recoverable, a different valid parse is not — a widget
  conditional calling trim() was silently dropped. Records that a
  grammar alternative and its visitor case are ONE change (accepting -7
  without the AST case serialized `[Amount > ]`), and the control-binary
  sweep for proving a relaxation causes no regressions.

- scripts-that-cannot-rerun (5). Statement-level idempotence, kept
  deliberately apart from ADR-0008's write-level idempotence, which is
  what people usually mean when they say mxcli is idempotent. `exec`
  halting on the first error turns "90% already applied" into "none of
  the remaining 10% applied", and the silent variant — a duplicated
  index — is worse than the error.

Coverage stated in CLAUDE.md was corrected to the measured 92% before
committing; the first draft claimed 98% from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ako
ako merged commit 40d779c into docs/bug-patterns-backend Sep 1, 2026
7 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.

2 participants