Part 02 — What does a DSL need from an IDE? The five-service baseline
Part 01 bounded the supply side: an IDE is a finite bundle of roughly twenty services, thirteen of them riding on LSP, the rest on editor-local contributions or VSCode defaults. This article bounds the demand side. A DSL author, handed that twenty-row menu, is not going to tick every box. Some services are load-bearing — a DSL without them is unusable — and some are refinements that polish a language already fit for purpose. Knowing which is which is what lets the meta-DSL generate the right extension instead of a heavy, half-useful one.
A narrow definition of DSL
The acronym is loose in everyday use. People call YAML a DSL. They call Bash a DSL. They call CSS a DSL. For the purposes of this series, we adopt a deliberately narrow definition:
A DSL — Domain-Specific Language — is a textual notation, sharply scoped to one domain, with an associated compiler, interpreter, or checker that attaches meaning to the text. It has a grammar (implicit or explicit), it has well-formedness rules, and something downstream consumes its output.
That narrowness rules out configuration formats that have no semantic interpreter of their own (plain JSON, plain YAML), general-purpose languages used for a single domain (TypeScript is not a DSL even when you use it only for React), and pure templating systems (Handlebars) whose only "semantics" is string substitution. It keeps in SQL, regular expressions, Catala, Mermaid, JSX, Bicep, Terraform HCL — and the two DSLs this monorepo already produces, @frenchexdev/requirements and @frenchexdev/typed-fsm.
The narrow definition matters because the IDE's job looks very different when the thing in the editor has semantics the editor can surface. A JSON file has keys; a Catala file has articles, rules, scopes — and an IDE that does not know those concepts cannot offer a meaningful go-to-definition.
External versus internal — the first axis of sorting
A DSL lives in one of two places. Either it has its own file extension and its own parser (external DSL — Catala's .catala_fr, Mermaid's .mmd, SQL's .sql), or it is embedded inside a host language using the host's syntax as a carrier (internal DSL — TypeScript decorators, Kotlin builder blocks, Ruby metaprogramming, Rust macros). Internal DSLs inherit the host's IDE support for free: if your internal DSL is expressed in TypeScript decorators, VSCode's TypeScript language service already gives you highlighting, completion, navigation, and rename — for the host-language skeleton. External DSLs start from zero; without a dedicated extension, a .catala_fr file opens as plain text.
The grid below sorts the examples named above on that axis, and adds the third question the meta-DSL actually cares about: does this DSL ship dedicated IDE support today?
| DSL | Domain | External / Internal | Dedicated IDE support today |
|---|---|---|---|
| SQL | Relational queries | External | Via editor plugins (DBeaver, DataGrip, vscode-sqltools) |
| Catala | Legal rules (tax, social benefits) | External | VSCode extension + LSP server (INRIA) |
| Regular expressions | Text patterns | Internal (string literal in a host) | Inline preview extensions (e.g., regex101 integrations) |
@frenchexdev/requirements |
Traceability (Requirement → Feature → AC → Test) | Internal (TypeScript decorators) | Host TS IDE + JSON-Schema binding for sibling .json specs |
@frenchexdev/typed-fsm |
State machines | Internal (TypeScript decorators + classes) | Host TS IDE only |
| Mermaid | Diagrams | External (in markdown fences) | Mermaid Preview extension + live render |
| Bicep | Azure resources | External | First-party VSCode extension with full LSP |
| JSX | UI markup | Internal (TypeScript/JavaScript extension) | Host TS IDE with JSX-aware features |
Three patterns fall out of the grid. First, external DSLs with serious institutional backing (Catala, Bicep) ship dedicated extensions — the effort is amortised over the user base. Second, internal DSLs lean hard on their host language's IDE and rarely get dedicated support; the cost/value ratio of a bespoke extension against an already-capable host IDE is poor. Third, the monorepo's own DSLs (requirements, typed-fsm) sit in the internal-DSL row with no dedicated IDE work done yet — and that is precisely the gap the meta-DSL is designed to close cheaply enough that "cheap dedicated support" becomes the default rather than the exception.
Why most DSLs lack decent IDE support
The honest answer is economics. Writing a VSCode extension by hand for one DSL takes a few hundred lines of plumbing on top of whatever language server you need to author. A solo DSL author — a legal engineer at a ministry writing a tax-rule DSL, a platform team writing an internal config language, a researcher prototyping a rule language for a paper — does not have the time to double the project's scope for tooling. So they ship without. Users of the DSL write the files in plain text mode, lose the first line/column of every error to eyeballing, and the DSL stays niche.
This is not a failure of engineering capability; it is a failure of leverage. The VSCode extension API is not hard to write against individually — each capability is a handful of well-documented hooks. The pain is combinatorial: the extension has to wire a manifest, a grammar, a snippets file, an extension host entry, an LSP client, possibly an LSP server, task providers for "run this file", language configuration for brackets and comments — and do it again, near-identically, for every new DSL. A DSL author who has done it once rarely wants to do it twice. The meta-DSL's leverage argument is that one decorated spec.ts can stand in for all of that plumbing.
The five-service minimum-viable baseline
Of the twenty services Part 01 enumerated, five form the threshold below which a DSL is not really supported in the editor at all. Miss any of them and the user experience collapses to "plain text with a custom file extension".
Syntax highlighting. Without it, the DSL reads as wall-of-text. Highlighting does not need to be semantically rich — a TextMate grammar derived from token regexes is enough for the first paint — but it must exist. A DSL whose keywords, literals, and structural punctuation are not coloured distinguishably is a DSL whose users cannot skim.
Diagnostics. Errors and warnings inline in the editor, rendered as squiggles under the offending range and surfaced in the Problems panel. A DSL whose errors appear only in a build log, minutes later, after the user has moved on, trains its users to ignore it. Diagnostics make the DSL conversational — type something malformed, see the red squiggle, fix it, keep going. Of the five services, diagnostics is the one most tightly coupled to the DSL's semantic core: it requires at least a parser and some form of static checker on the server side.
Snippets. Parametric boilerplate insertion by prefix. A user typing feat and pressing Tab receives the skeleton of a Feature declaration with tab-stops for the id, the title, and the acceptance criteria. Snippets carry the DSL's idiom — the canonical way to write the common constructs — without the user having to remember decorator names or argument order. They cost almost nothing to produce (a JSON file registered in the manifest), and they disproportionately shorten the time from "first open the DSL" to "first useful output".
Go-to-definition. Jump from a reference to the declaration. For a DSL with cross-file linking — the requirements DSL links a @Satisfies(ReqFoo) to the ReqFoo Requirement class, an @FeatureTest(Feature) to the Feature subclass, a @Verifies('acName') literal to the abstract AC method — navigation without go-to-definition means grep-and-hope. For a single-file DSL (a self-contained SQL query, a pure regex) this service is less load-bearing; for any DSL with a notion of scope, it is the difference between a living codebase and a pile of string references.
Executor. A command in the task palette or command palette that runs the DSL — Requirements: run compliance --strict, Mermaid: render this fence, Catala: evaluate this scope. Without it, the user alt-tabs to a shell every time they want to see the effect of their edit. With it, the loop closes: edit, save, Ctrl+Shift+B, read the output, edit again. The executor is not an LSP capability; it is a TaskProvider or CommandContribution in the extension manifest, and it is the one service that takes the DSL from "a notation" to "a notation you can run".
The five-service baseline is deliberately below the twenty-row menu. Part 01 listed completion, hover, rename, signature help, code actions, CodeLens, inlay hints, folding, document symbols, workspace symbols — all of them useful, most of them refinements over the baseline. The meta-DSL will generate them when a spec declares them, but it will not treat their absence as a failure. The five are the floor; the rest are ceiling.
The requirements DSL today — a case study against the baseline
The monorepo's @frenchexdev/requirements package is an internal DSL: a Feature is a class extending Feature, an acceptance criterion is an abstract method returning ACResult, a link to a Requirement is the @Satisfies(ReqFoo) decorator, a test-to-feature binding is @FeatureTest(FeatureFoo) + @Verifies('acName'). The DSL lives in .ts files and is consumed by the requirements CLI (npx requirements compliance, trace, scaffold, feature new, requirement new). It has no dedicated VSCode extension. How does it fare against the five-service baseline?
Read the table below as a stocktake — what the user gets today, by accident of living inside a TypeScript file, versus what a dedicated extension would add.
| Service | What the user gets today | What a dedicated extension would add |
|---|---|---|
| Syntax highlighting | TypeScript's full highlighting, including decorator names | A semantic layer that colours @Satisfies differently from @Component, and that highlights FeatureIds (FEAT-…) as a distinct token class |
| Diagnostics | tsc type errors + eslint stylistic errors; nothing about orphan Features, missing @Satisfies, or ill-formed FeatureIds |
Inline warnings for any Feature subclass without @Satisfies, any FeatureId that does not match the monorepo convention, any @Verifies('…') pointing at a non-existent AC |
| Snippets | Nothing dedicated — the user types @Satisfies( by hand |
feat, ac, ftest, req prefixes that expand into the canonical skeletons (see the RequirementsIde spec: three @Snippet declarations cover the common cases) |
| Go-to-definition | TypeScript's — which resolves @Satisfies(ReqFoo) to the ReqFoo class correctly |
A second resolution for @Verifies('acName') literals, which TypeScript cannot resolve because they are strings |
| Executor | A shell: npx requirements compliance --strict |
A task in the VSCode palette, runnable with Ctrl+Shift+B, with its output piped into the Problems panel and its non-zero exit flagged workspace-wide |
Three of the five services already work tolerably through TypeScript's host IDE. Two do not — dedicated diagnostics (nothing catches orphan Features or string-literal drift in @Verifies) and a one-button executor (users run the CLI from a terminal, losing the task/problems integration). These two are exactly what the RequirementsIde spec artefact declares: an @LspFeature('diagnostics') handler and an @Executor({ command: 'requirements.compliance', taskKind: 'test' }) method. The meta-DSL, applied to this DSL, would close the baseline.
The leverage argument — restated against the baseline
The leverage argument of the series depends on the baseline being small. If the five services each cost a thousand lines of bespoke code to produce, no meta-DSL is going to make them cheap; authors will continue to skip IDE support. The argument works because the five services each reduce, mechanically, to material already present in a decorated spec.
- Syntax highlighting is a mechanical projection of
@Token(name, regex)decorators into a TextMate JSON grammar. The projection is pure text-to-JSON; no parsing of the DSL's content is required. - Diagnostics are an implementation of the
@LspFeature('diagnostics')method on the spec, hosted inside a language-server scaffold the meta-DSL generates. The spec author writes the rules (a Feature without @Satisfies is an error); the meta-DSL writes the transport (LSP over JSON-RPC). - Snippets are a projection of
@Snippet(prefix, body, description)decorators into a VSCode snippets JSON file. Pure text-to-JSON, again. - Go-to-definition is an implementation of the
@LspFeature('definition')method. The spec author writes the resolution (a@Verifiesliteral points at the abstract method with matching name on the feature class); the meta-DSL writes the transport. - The executor is a
@Executor({ command, taskKind })method projected to aTaskProviderin the extension host. The spec author writes the body (spawn a subprocess, return anExecutionResult); the meta-DSL writes the registration.
The ratio matters. In each of the five, the spec author writes the domain-specific part — a regex, a rule, a template, a resolution, a subprocess invocation — and the meta-DSL takes care of the roughly ten-to-one plumbing surround. That ratio is what flips the cost/value calculus: if baseline IDE support costs a hundred lines of spec per DSL rather than a thousand lines of plumbing, every small DSL in the monorepo earns one. A regex DSL earns one. A distributed-task DSL earns one. A finite-state-machine DSL (@frenchexdev/typed-fsm) earns one. The series' bet is that this reversal is the one that makes the zoo of small DSLs — the programme Appareil et compilateur sketches for regulatory complexity — tractable to ship, not just to describe.
The contract with the meta-DSL, stated from the DSL's side
The five-service baseline, restated from the perspective of what a DSL spec must declare to get a usable extension out of the forge, is:
- At least one
@Token— the lexer's base alphabet; without it there is no highlighting to derive. - At least one
@LspFeature('diagnostics')method — even if the body is a single "no errors" return, its presence wires the diagnostics channel. - At least one
@Snippet— zero snippets is a sign the DSL's canonical forms are not yet consolidated; the meta-DSL will accept zero but warn. - At least one
@LspFeature('definition')method — if the DSL has any notion of cross-reference at all; single-file DSLs may skip this. - At least one
@Executor— something the user runs. A DSL you cannot run is a DSL whose IDE extension is decorative.
Read the requirements-ide.spec.ts artefact against this checklist and every row is present: five @Token declarations, four @LspFeature methods (one per LSP capability the DSL needs), three @Snippet declarations (feat, ac, ftest — the three canonical forms of the requirements DSL), and one @Executor (runCompliance wrapping requirements compliance --strict). The spec is not a hypothetical fit against the baseline; it is the baseline, instantiated for one real DSL.
Part 03 takes the next step: now that we know what an IDE offers (Part 01) and what a DSL needs (Part 02), we can ask the shaping question of the series — how do we actually build this meta-DSL? That answer requires engaging the prior art substantively (MPS, Roslyn source generators, the TypeScript compiler infrastructure, Langium) and then stating the guardrails the design will hold to. It is the article where the bet on leverage becomes a bet on a specific architecture.
Build counterpart
The six decorators sketched here are given concrete shapes in the companion build series, Ide.Dsl — Build. Build 01 — Bootstrapping @frenchexdev/ide-dsl defines each decorator as a TC39-standard registration against a module-load registry; Build 02 — The extractor walks the requirements-ide.spec.ts artefact the checklist above references and lifts every @Token, @LspFeature, @Snippet, and @Executor into LanguageIR.