Skip to content

mdl: language header mdl <n>; with version-gated semantics (#710) - #726

Merged
ako merged 3 commits into
mainfrom
feature/710-mdl-version-header
Sep 26, 2026
Merged

ako merged 3 commits into
mainfrom
feature/710-mdl-version-header

Conversation

@ako

@ako ako commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Closes #710. Part of the MDL alpha->beta program (#714), plan item 1.5 of PROPOSAL_mdl_beta_syntax_freeze.md; implements ADR-0011 decision 2.

What

  • Grammar: program : languageHeader? statement* EOF, languageHeader : IDENTIFIER NUMBER_LITERAL SEMICOLON. The visitor requires the word to be mdl (case-insensitive). A script with no header is mdl 0.
  • mdl/langver (new): Version, Latest (= 1), Default (= 0) and Frozen, the single preview/frozen switch (currently V0). Everything above Frozen is a preview: mdl 1; parses but warns MDL-LANG01 "mdl 1 is a preview: may still change", and HeaderLine() (what describe/fmt should emit) returns "". At beta, set Frozen = V1: the warning stops and HeaderLine() returns mdl 1;.
  • Gating helper: langver.Change{Code, Since, Old, New} plus Builder.gate(change, ctx) in the visitor. It returns true under the new version. Under an older version the caller keeps the old meaning, and gate records an ast.LanguageNote that ValidateProgram reports as a warning under the change's own rule ID. Handlers in the executor read ExecContext.LanguageVersion and branch on Change.Applies.
  • Executor: ExecuteProgram and ExecuteProgramContinueOnError run their statements under the program's header and then restore the previous version, so a nested EXECUTE SCRIPT uses its own header. A statement run outside a program (REPL, -c) is mdl 0.
  • Edges:
  • Docs: mxcli syntax language-header, docs-site/src/language/basics.md, MDL_QUICK_REFERENCE.md.

describe and fmt do not emit the header. fmt is line-based and keeps an existing header in place without adding one; a test pins this. fmt --upgrade does not exist yet.

Design choices the ADRs did not settle

  • mdl is not a lexer keyword. I first added an MDL token (listed in keyword). That broke describe contract entity X format mdl, because that position accepts only an IDENTIFIER, and TestExamplesParse/odata.show caught it. Matching an IDENTIFIER and checking the word in the visitor leaves every existing use of mdl as a name unchanged. A test covers both a module/attribute named mdl and format mdl.
  • An unknown version is an error, not a warning. Running a script under older rules than it was written for is the silent change of meaning ADR-0011 exists to prevent.
  • Explicit mdl 0; is accepted without comment. The ADR says nobody writes it, but it is harmless.
  • Rule IDs: MDL-LANG01 is the preview warning. Each gated change carries its own Code.
  • No real construct is gated yet. The §5 changes land in their own issues. The "differs by version" construct is a test-only langver.Change attached to show through the real Builder.gate, observed via a listener wrapper (build(input, listen)).
  • Relation to Deprecation registry for MDL aliases (MDL-DEPRnnn), generalising MDL065 #709 (registry). This PR does not depend on it. langver.Change is the declaration for changes of meaning. The Deprecation registry for MDL aliases (MDL-DEPRnnn), generalising MDL065 #709 registry is for respellings/aliases, and alias removal at a version boundary can use Change.Applies.

Test plan

  • go test ./mdl/langver/: preview state; the frozen switch (isPreview/headerLine with frozen = V1); Known; Change.Applies; IsHeaderLine.
  • go test ./mdl/visitor/ -run LanguageHeader:
    • headerless is mdl 0; the header parses (case, a leading comment, header-only)
    • unknown and non-integer versions are refused
    • a header that is not first gets the hint; a misspelled word is refused
    • mdl is still a name
    • gated construct differs by version: old meaning plus one warning per occurrence (with line) under mdl 0; new meaning and no warning under mdl 1.
  • go test ./mdl/executor/ -run 'LanguageVersion|UnderTheHeader':
    • the preview warning, with a headerless control
    • gated notes are reported as warnings
    • end to end, a handler sees mdl 0 for a headerless program, mdl 1 for mdl 1;, and mdl 0 outside the program
  • go test ./mdl/formatter/: fmt keeps the header, is idempotent, and adds none while in preview.
  • go test ./cmd/mxcli/ -run 'EmptyInput|Garbage': a header-only script is not refused as unparsable.
  • Revert checks. Each change was reverted and the matching test watched fail:
    • version not recorded -> Parses/HeaderOnlyScript fail with "got version 0"
    • gate always true -> GatedConstructDiffersByVersion fails with "got [true true]"
    • preview check disabled -> PreviewWarns fails with "got []"
    • enterLanguage removed from ExecuteProgram -> UnderTheHeader fails with "handlers saw [mdl 0 mdl 0 mdl 0 mdl 0]"
    • the executor-context field not propagated -> ExecContextCarriesLanguageVersion fails
  • Manual, with ./bin/mxcli:
    • check on mdl 1; prints the MDL-LANG01 warning and passes
    • mdl 3; gives a syntax error (exit 1)
    • a header on line 2 gives the hint (exit 1)
    • fmt keeps mdl 1;
  • Local suite:
    • make build passed.
    • make lint passed.
    • make test passed except mdl/backend/modelsdk, which hit the 10-minute go test timeout on a heavily loaded shared machine. Run alone with -timeout 40m it passed (392s). This PR does not touch that package.
    • The first make test attempt failed on "no space left on device" in the shared /tmp. It was re-run with TMPDIR/GOTMPDIR on the main disk.

Follow-ups

  • Gate each §5 change through langver.Change in its own issue: limit 1/first, mandatory set, list operations as statements, strict keys, required ;, no \ escapes.
  • fmt --upgrade adds the header and rewrites the gated constructs, once they exist.
  • At beta, flip langver.Frozen to V1 and wire langver.HeaderLine() into describe/fmt output.

🤖 Generated with Claude Code

ako and others added 3 commits September 26, 2026 20:50
An optional first statement declares the MDL language version a script is
written in (ADR-0011 decision 2). No header is mdl 0, the alpha meaning.

- grammar: `program : languageHeader? statement* EOF`; the word is an
  IDENTIFIER checked by the visitor, so `mdl` stays usable as a name
  (e.g. `describe contract entity X format mdl`).
- mdl/langver: Version, Latest, and Frozen, the single preview/frozen
  switch. Before beta mdl 1 is a preview: it warns MDL-LANG01 "preview: may
  still change" and HeaderLine() (what describe/fmt emit) is "".
- langver.Change + Builder.gate: the helper every change of meaning goes
  through; under an older version the old meaning is kept and a warning
  is recorded on the Program, reported by ValidateProgram.
- executor: ExecContext.LanguageVersion carries the program's header to
  handlers; nested EXECUTE SCRIPT runs under its own header.
- unknown versions are refused; a header after the first statement gets a
  hint; a header-only script counts as empty for exec/check/fmt.

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

ExecuteProgramContinueOnError enters the header's version separately from
ExecuteProgram, and nothing covered it: removing its enterLanguage call left
the suite green. The new test fails with handlers seeing mdl 0 throughout.

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

`mdl 99999999999999999999;` said the version "must be a whole number",
which it is. strconv.ErrRange now reports the unknown-version error with the
number as written.

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

ako commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Independent review (automated reviewer).

Verified: both done-when criteria hold (header parses; fmt keeps it and is idempotent; the test-only gated construct takes the old meaning + warning under mdl 0 and the new meaning without a warning under mdl 1). Revert checks reproduced: gate forced true, version not recorded, ValidateLanguageVersion unhooked, enterLanguage removed. Each makes its test fail. CLI edge cases tried: header after a statement (hint shown), missing ;, mdl 2;, mdl -1;, mdl 01;, header after a block comment, header-only file, fmt with a trailing comment on the header.

Pushed two commits:

  • test: ExecuteProgramContinueOnError (exec --continue-on-error) had no coverage for entering the header version. Removing its enterLanguage left the suite green. The new test fails when it is removed.
  • fix: mdl 99999999999999999999; reported "must be a whole number". It now gives the unknown-version error (strconv.ErrRange). Test first, seen failing.

Minor, left for the maintainer: no mdl-examples/doctype-tests example and no skill mention. Reasonable while mdl 1 is a preview. The docs-site link points at the ADR on mendixlabs/mxcli, where it may not exist yet.

make build / make lint pass. make test: the failures in cmd/mxcli, tui, docker and tunnelhub came only from TMPDIR sitting inside the repo, and those packages pass with TMPDIR outside it. marketplace and backend/modelsdk hit the 10m timeout on the loaded machine and were re-run with -timeout 40m.

@ako
ako merged commit bb0f756 into main Sep 26, 2026
17 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.

MDL language header 'mdl <n>;' with version-gated semantics (headerless = mdl 0)

1 participant