Layer 3: languages built on MX
Status: idea. Not on the roadmap. Written down so layers 1 and 2 are built in a way that leaves this reachable at packaging cost, with the parser never opened again for it.
A layer-3 package is a language whose compiler is MX. It keeps the parser states, the IR, the emitters, the projection and the manifests, and changes the surface through the same syntax table and hooks a layer-2 project uses. What distinguishes it is that its user never sees MX:
- its own extension (
.disl), never.<segment>.mx - its own name in every diagnostic, CLI and editor title
- its own manifest key (
package.json#disl) - its own editor language id and grammar
- a pinned
@mxlang/coreas an implementation detail
A user installs disl, writes src/**/*.disl, runs disl build, installs
the disl editor extension. Mesh is the intended first case.
Package anatomy
disl/
package.json dependencies: @mxlang/core (exact), the target package it emits through
dialect.ts default export: LanguageDescriptor
syntax.ts full SyntaxTable (its default row) + lowerTrigger/lowerBlockTag/lowerFilter/afterLower
declarations.ts HostDeclarations: dispositions (which core tags exist, renamed, forbidden)
tags/ the language's own tags
contracts.ts optional
bin/disl.ts CLI wrapping mx-tsc, the Vite plugin and the Bun loader with the descriptor preloaded
editor/vscode/ extension from the MX template: language id, grammar, LS launched with the descriptor
test/oracle/ fixtures through packages/oracle's harness
interface LanguageDescriptor extends TargetDescriptor {
descriptorVersion: 1;
productName: "disl";
extensions: [".disl"];
manifestKey: "disl";
syntax: "./syntax.ts";
declarations: "./declarations.ts";
tags?: "./tags";
contracts?: "./contracts.ts";
target: "html" | "data" | "preact-jsx" | ...; // or its own emitter via load()
allowProjectSyntax?: boolean; // default false
editor?: { languageId: "disl"; grammar: "./editor/disl.tmLanguage.json" };
}
How it embeds MX
The compile path is the existing one with the descriptor supplied once:
compileWithDescriptor(source, file, descriptor, options) resolves the table
(the language’s full default row, overlaid by the consumer’s manifest only if
allowProjectSyntax), loads the syntax module, merges the language’s tags
with the consumer’s, picks the target, compiles. Tools take the descriptor
from package.json#mx.target (an MX project) or from the language’s CLI and
editor extension (a layer-3 project). A file whose extension is in a loaded
descriptor’s extensions routes to it; .<segment>.mx lookup is the
fallback.
Core tags are hidden or renamed through declarations.dispositions; the
language’s own {% if %} or <when> is a block form or a tag lowering to the
same IR. Expressions stay TypeScript until the expression-language axis
exists.
Consumer project
my-app/
package.json "disl": { "target": "html", "tags": "./tags" } (no "mx" key)
src/pages/home.disl
tags/card.disl discovered like tags/*.mx, by the language's extension
manifestKey makes the scan and host-policy readers read package.json#disl
with the shape they read package.json#mx today; the shared manifest reader
is keyed once more by the key.
Build, test, publish
create-dialectscaffold writes the anatomy with the.mxdefault row copied.mx dialect checkvalidates the descriptor, compiles the syntax module, validates the resolved table, loads tags and contracts, runs the oracle fixtures with the descriptor as the host row.mx dialect editorbuilds the VS Code extension from the template; the language server and the TypeScript plugin are MX’s, configured with the descriptor.npm publish;vsce publish. The language pins core exactly, as the tools pin TypeScript’s peer: two core instances breakinstanceofacross the language-server boundary.
What layer 3 does not allow
- A new IR kind: every emitter implements one method per kind; a kind the
hosts do not know is a silent drop. A language composes kinds through
ctx.build; a needed kind is a core decision. - A different tag-open character or attribute grammar.
- A different expression language, until the axis is designed.
- A state-level hook.
- Project-level syntax overlays unless the language opts in.
Effort after layer 2
| Item | Size |
|---|---|
extensions, manifestKey, productName on the descriptor; routing by extension |
S |
compileWithDescriptor |
S |
manifestKey in the scan and host-policy readers |
S |
mx dialect check over the oracle harness |
M |
VS Code extension template and mx dialect editor |
M |
descriptorVersion 1 freeze, decision entry |
S |
create-dialect scaffold |
S |
All packaging over the layer-2 seams.
Open questions
- Does a language’s extension also name its tag files (
tags/card.disl)? Proposed: yes. - May a language expose several targets under one manifest key? Proposed: yes, a list with a default, as
host.defaultis today. - Is the Marko compatibility ADR a constraint on languages? Proposed: no; parity is a property of the
.mxdefault row.