Compiler and DSL standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to building a language processor: your own DSL, an interpreter, a compiler, a
transpiler, a linter with its own parser, or an input format with enough syntax to need a grammar.
It covers the prior decision (do you need a language?), the frontend, the intermediate
representation, the type system, the backend, diagnostic quality, testing and the tooling that makes
a language usable.
Triggers: ANTLR .g4, Tree-sitter grammar.js and node-types.json, flex/bison .l/.y,
.pest, .lark, .ebnf, combinators (nom, chumsky, megaparsec, FParsec, parsec),
llvm-sys/inkwell/llvmlite/libLLVM, .ll and .bc, Cranelift cranelift-codegen,
wasm32 as the target of your own compiler, "AST", "IR", "SSA", "name resolution",
"type inference", "symbol table", "parser error recovery", "tree versus bytecode",
"should I add a JIT?", textDocument/publishDiagnostics and other LSP methods,
ariadne/codespan-reporting/miette, insta/expect-test/lit/FileCheck, "ambiguous
grammar", "shift/reduce conflict", "left recursion".
Governing principle, and it is a prohibition before it is advice: your own DSL is the most
expensive over-engineering there is. The parser is not the cost — the parser is a weekend —; the
cost is the documentation, the highlighting, the formatter, the LSP, the debugger, the error
messages, the training of every new person and the migration when the syntax changes, for the
whole life of the system. A language without that budget committed does not get started. The
ladder is climbed from the bottom up and you only climb a rung when the previous one has failed
with a concrete, demonstrated case:
- Data: JSON/YAML/TOML with a validated schema (JSON Schema). It covers 80% of what people call
a DSL.
- Library / API: functions and types of the host language. Zero new tooling, the IDE already
works, the type checker already exists.
- Internal (embedded) DSL: a builder, operators, macros or host metaprogramming. It inherits
the editor, the debugger, types and ecosystem. It is the right answer almost always.
- External DSL with its own grammar: only if the user is not a programmer, or if the syntax
has to be portable across several hosts, or if the language must be analysable/verifiable in a
way the host prevents (e.g. guaranteed termination, sandbox execution).
- General-purpose language: almost never, and not in a product project.
Second thesis, which orders §3.5 and §4: the quality of the error messages is a product
feature, not later polish. It is what decides whether a language is adopted or hated; and it is
the technical reason why almost every serious compiler ends up with a hand-written parser.
Not applicable: see rust-standards, c-standards, cpp-standards, zig-standards,
haskell-fp-standards, ocaml-fsharp-standards, typescript-standards, python-standards,
go-standards (the language you write the compiler in is theirs: build, lint, unit tests,
dependency management, idioms. Here the architecture of the language processor, not how the code
that implements it is written. OCaml/Haskell/F# appear here because their sum types and pattern
matching are the natural tool for an AST, but that recommendation belongs to §2, it is not a scope
handover), webassembly-standards (Wasm as a target and as a runtime is theirs: the wasm32-*
target, the Component Model and WIT, sandbox limits, fuel/epoch and the module size budget. Here
only the decision to emit Wasm from your backend and what you lose by doing so),
lowcode-governance-standards (the low-code platform as a third-party product and its
governance — life cycle, ownership, catalogue, licences, shadow IT — is theirs. Clean boundary:
building a language belongs here; governing the use of one you bought, to them. A visual flow
editor that serialises to JSON is a platform, not a DSL in the sense of this document),
sql-standards (SQL is already the DSL almost nobody needs to reinvent; if the problem is
querying data, the answer is usually SQL, not a new grammar), api-design-standards
(if the user is a program, the answer is an API, not a syntax: contract, versioning and
compatibility are theirs), testing-qa-standards (general quality strategy; here the specific
techniques: snapshot, parser fuzzing, conformance suites), appsec-standards (threat modelling
and agnostic vulnerability classes; here only the sinks particular to executing third-party code),
performance-engineering-standards (measurement and profiling methodology; here only what to
measure in a compiler and when a JIT does not pay off), opensource-licensing-standards (the
licence as a legal constraint; here only the fact that LLVM is Apache-2.0 WITH
LLVM-exception), ai-agents-standards (an agent that generates code in your DSL; here the DSL).
2. Default decisions / Toolchain
Verify the latest version on the web before fixing it in a real project (§8).
| Decision |
Default |
Justifiable alternative |
Reason |
| A DSL? |
No. Data with a schema or a host API |
Internal DSL |
The ladder in §1; rung 4 requires a written justification |
| Internal vs. external DSL |
Internal |
External if the user does not program |
The internal one inherits IDE, types, debugger and ecosystem for free |
| Production parser |
Hand-written recursive descent (+ Pratt for expressions) |
A generator for prototyping or for someone else's already specified language |
Total control of messages and error recovery; no opaque conflicts |
| Generator, if you use one |
ANTLR 4 (.g4, BSD-3-Clause) |
bison/flex in legacy C |
ALL(*) accepts left recursion; mature ecosystem. Last release 4.13.2, August 2024 (verify §8): a stable project but with a very slow cadence — a risk datum, not a disqualifier |
| Incremental / editor parser |
Tree-sitter (MIT, 0.26.x as of Jul 2026) |
LSP with a full reparse if the file is small |
Incremental, error-tolerant reparse; it is what an editor wants. Still on 0.x: the API breaks between minors |
| PEG |
Only with a documented alternative order |
— |
The ordered / operator removes ambiguity without telling you: a rule that is never reached gives no error, it gives silence. See §7 |
| AST |
Immutable sum types + a Span on every node |
A CST/concrete tree if the original has to be reprinted |
Without a Span there is no diagnostic, no fix-it and no LSP |
| Diagnostics |
A rendering library with spans and labels (ariadne, codespan-reporting, miette) |
Your own file:line:col: format, editor-compatible |
A message with no highlighted code fragment is a useless message |
| Execution |
Bytecode interpreter (stack VM) |
A tree if the language is configuration and runs once |
The tree is 5–20× slower; bytecode is not as complicated as it looks |
| JIT |
No, until you have the profile that demands it |
When the same code runs millions of times in a long-lived process |
See §6.2 |
| Native backend |
LLVM if you need quality machine code |
Cranelift if compilation time dominates (JIT, Wasm) |
LLVM optimises better; Cranelift compiles much faster and is pure Rust |
| "Cheap" backend |
Transpile to C, Rust, Go or TypeScript |
— |
You inherit its optimiser, its portability and its debugger. The most underrated option |
| Portable target |
WebAssembly |
— |
One backend, many platforms, sandbox included → webassembly-standards |
| Output tests |
Snapshot (insta, expect-test, lit+FileCheck) |
— |
A compiler is a pure function from text to text: the snapshot is its natural test |
| Parser fuzzing |
Mandatory (cargo-fuzz/libFuzzer/AFL++) |
— |
The parser is the attack surface; see §5 |
| Minimum tooling to publish |
LSP + formatter + highlighting |
— |
Without the three, the language is not usable outside its author |
About LLVM, and this is the datum that breaks projects: LLVM's IR is not a stable format. The
official policy says, textually, that "The textual format is not backwards compatible. We don't
change it too often, but there are no specific promises." For the bitcode there is a backward
reading guarantee ("The current LLVM version supports loading any bitcode since version 3.0"), but
with caveats that matter: "Newer releases can ignore features from older releases, but they cannot
miscompile them" and "Debug metadata is special in that it is currently dropped during upgrades".
Operational, not theoretical consequences:
- Pin LLVM's major version in the build and treat it as a platform dependency, not as an
interchangeable library. Moving up a major is a project, with its window and its tests.
- Do not keep
.ll or .bc as a long-term artifact or as an interchange format between
components that are updated separately. That is the classic mistake.
- The binding (
inkwell, llvmlite, llvm-sys) is tied to LLVM's major: your project's
update pace is set by the slower of the two.
- LLVM publishes two majors a year and frequent patches (as of Jun 2026, 22.1.8; verify §8).
Licence Apache-2.0 WITH LLVM-exception.
3. Structure and conventions
3.1 Phases, and the rule that each one produces a datum
text → [lexing] → tokens (+span) → [parsing] → CST/AST (+span)
→ [name resolution] → resolved AST → [types] → typed AST
→ [lowering] → IR → [optimisation] → IR → [backend] → output
- Each phase is a pure function from one structure to another. No mutating the AST in place from
five different phases: it destroys traceability and makes per-phase testing impossible.
- Each node carries its
Span (start offset, end offset, file id). The Span survives every
phase; if it is lost in the lowering, the type error cannot point at code.
- Centralised
SourceMap: offsets are bytes, conversion to line/column happens once at render
time. Careful with UTF-8: the column an editor expects is usually UTF-16 (the LSP uses UTF-16
by default); this is decided and documented once, not per function.
- Errors as data, not as exceptions: the compiler accumulates diagnostics and carries on.
Aborting on the first error is the worst possible experience and makes the LSP unviable.
3.2 Parser: why by hand
The generator is faster to write and worse to use. A generated parser produces "syntax error at
line 42"; a hand-written one produces "missing ) — the opening parenthesis is on line 39". What
tips the balance is not performance, it is this:
- Error recovery: the LSP needs a reasonable tree from a file the user is typing and which is by
definition invalid. Recovery via synchronisation points (
;, }, the start of a declaration) and
Error nodes in the tree, not an exception.
- Messages with context: only you know what the parser was expecting and why.
- Expression precedence: Pratt parsing / precedence climbing, an explicit table of
precedence and associativity. It is the part pure recursive descent does badly.
- If you use a generator: prototype with it and switch to hand-written once the grammar
stabilises. Migrating with the grammar already written as a specification is cheap; starting by
hand without knowing the grammar is not.
3.3 Name resolution and types
- Name resolution is a phase of its own, separate from the parser and from the type checker.
Explicit scopes, not a mutable global table.
- Inference: Hindley-Milner (Algorithm W / level-based generalization) if the language is
functional and you want full inference; bidirectional checking if there is subtyping, generics or
overloading. Pick one, and know which, because mixing them by eye produces a type system that
is impossible to explain and to give messages for.
- Practical and unintuitive corollary: global inference makes error messages worse. The error
appears far from its cause. Requiring annotations at the boundaries (public functions, fields) is
a usability decision, not a technical limitation.
3.4 Backend: choose the cheapest that does the job
By increasing cost: tree interpreter → bytecode → transpile → Wasm → Cranelift → LLVM → your own
code generation. You never climb a rung without a number to justify it. Transpiling to a language
with a good ecosystem is the most underrated route: you inherit the optimiser, the platforms, the
debugger and the profiler; the price is that the target language's error messages leak to the
user — and that has to be actively covered up with source maps or with a translation layer.
3.5 Diagnostics, as a specification
A complete diagnostic has: a stable code (E0308, TS2345) documented and searchable; a
severity; a primary span with the rendered fragment; labelled secondary spans ("it was
declared as Int here"); a note explaining the rule; and, when a mechanical correction exists, a
structured fix-it (range + replacement text) the LSP can apply. The error code is a contract:
once published it is not reused for something else.
4. Quality and testing
Gates in order of increasing cost; the first three break the build.
- Unit tests per phase: lexer, parser, resolution, types. Each with its minimal input.
- Snapshot of the output, and of the errors: for each test program the serialised AST, the
IR and the exact text of the diagnostic are frozen. Having the error message under version
control is what stops it degrading without anyone noticing.
insta, expect-test, or lit +
FileCheck LLVM-style.
- A suite of invalid programs, as large as the valid one. A compiler is judged by what it
rejects and by how it explains it. Every defined error has at least one case.
- Parser fuzzing (libFuzzer/AFL++, with a seed corpus from the suite): the target is zero
panics, zero stack overflows, zero infinite loops on any input. A recursive parser overflows
the stack with nested parentheses: an explicit depth limit, not trust.
- Round-trip / properties:
parse(print(parse(x))) == parse(x) for the formatter; random
generation of valid ASTs and checking that they print and reparse.
- Differential, if you are reimplementing something existing: compare against the reference
implementation over a real corpus.
- Conformance: if the language has a specification (yours or someone else's), the conformance
suite is a separately versioned artifact, and the conformance percentage is published. Without a
suite, the "specification" is the implementation, and then there is no specification.
- Performance regression: compilation time over a fixed corpus, measured in CI with a
threshold. A compiler that becomes slow loses users exactly as one that gives bad errors does.
5. Stack security
- Running someone else's language is running someone else's code. If the DSL is written by
users, customers or an LLM, the interpreter is a trust boundary: an instruction limit, a memory
limit, a wall-clock limit, a recursion depth limit and a data nesting limit, all mandatory and
all configurable. Without them, a three-line loop is a denial of service.
- Decide and document whether the DSL is Turing-complete. If it does not need to be, do not
make it so: a total language (with no unbounded loops) can be analysed, bounded and executed
without fear. That is the real advantage of a DSL over "let them write Python".
- Host surface: the list of native functions exposed is the complete attack surface. No
exposing the filesystem, the network or
exec "for convenience". An explicit allowlist, reviewed
the way a public API is reviewed.
- Denial of service in the parser: exponential backtracking in PEG and in regular expressions
(ReDoS), nesting depth, unbounded input sizes. All with a limit and with a test.
- No host
eval to implement the DSL. A "DSL" that compiles to Python/JS eval() is not a
DSL: it is code injection with pretty syntax.
- The compilation cache is a supply chain vector: if you keep compiled artifacts (bytecode,
.bc, objects) or download third-party grammars/plugins, they come with integrity verification. A
Tree-sitter grammar file compiles to native C that gets loaded into your process.
- Error messages do not leak absolute paths or the environment when the compiler runs as a
service. This error is as classic as it is boring.
6. Performance and operability
6.1 What to measure
Time per phase (lexing, parsing, types, backend) over a fixed corpus; the split is almost never
what you think — in mature compilers the backend and resolution dominate, not the parser. In an
LSP, what matters is perceived latency: completion and hover below ~100 ms, and diagnostics
below ~500 ms from the last keystroke. That is achieved with incremental parsing and lazy querying
(a query-based architecture with memoisation), not by optimising the lexer.
6.2 When a JIT does not pay off
A JIT only wins when the accumulated execution time of the hot code comfortably exceeds the cost
of compiling it, and that cost includes what almost nobody counts: debugging complexity, the
impossibility of W^X on platforms that forbid it (iOS, consoles, many environments with strict
seccomp/SELinux), the attack surface of writable executable pages, and the work of maintaining it
per architecture. It does not pay off in: short processes, code that runs once (configuration,
templates, rules), I/O-dominated workloads, and anywhere portability matters more than the peak.
Before a JIT: compact bytecode, inline caching, superinstructions, and — first of all —
checking that the interpreter is not wasting its time on memory allocations.
6.3 Tooling without which the language does not exist
- LSP (spec 3.18 in development as of August 2026; 3.17 is the last one published as stable —
verify §8). Minimum viable: diagnostics, hover, go to definition, autocompletion, document
symbols.
- A canonical formatter with no options. The
gofmt lesson: zero configuration removes the
debate forever. It ships from the first version, because a retroactive formatter rewrites all
the existing code at once.
- Highlighting: a Tree-sitter grammar (modern editors) and/or TextMate
.tmLanguage.json (VS
Code, and still the lowest common denominator).
- Debugging: if the language is transpiled or compiled, either you emit real debug information
(DWARF, source maps) or you assume nobody will be able to debug. A conscious, documented
decision.
- Language versioning: SemVer over the syntax and the semantics, not over the binary. A
change that makes a previously valid program invalid is breaking, always.
7. Long-term sustainability
A DSL with no owner is debt that cannot be refactored. A language has no reliable find usages
outside its own tooling, so the code written in it becomes untouchable as soon as the person who
designed it leaves. Requirements of existence, not recommendations: a named owner, a written
specification (even if it is a short document), a conformance suite, a compatibility
policy and an automated migration plan (fix/codemod) for every syntax change. If any of
them is missing, the DSL is not approved.
- ❌ FORBIDDEN to create an external DSL without having exhausted — and documented why they fail
— rungs 1 to 3 of the ladder in §1.
- ❌ FORBIDDEN to implement the DSL on top of the host language's
eval()/exec().
- ❌ FORBIDDEN to run user code without time, memory, instruction and depth limits.
- ❌ FORBIDDEN to expose to the DSL native file, network or process functions without a reviewed
allowlist.
- ❌ FORBIDDEN an AST with no
Span. A node with no position is a diagnostic that cannot be
given.
- ❌ FORBIDDEN to abort compilation on the first syntax or type error.
- ❌ FORBIDDEN to treat LLVM's IR as a stable interchange or storage format (§2), and forbidden
to leave LLVM's major version unpinned in the build.
- ❌ FORBIDDEN to publish a language with no formatter and no highlighting. "It will come later"
means never.
- ❌ FORBIDDEN to change a diagnostic's text without a snapshot recording it.
- ❌ FORBIDDEN to reuse a retired error code for another meaning.
- ❌ FORBIDDEN a PEG grammar with alternatives whose order is not documented: an unreachable rule
gives no error, it gives silence (§2).
- ❌ FORBIDDEN a recursive parser with no depth limit and no fuzzing test verifying it.
- ❌ FORBIDDEN "we will optimise it with a JIT" with no prior profile (§6.2).
- ❌ FORBIDDEN to break the syntax with no codemod and no notice period with both forms valid.
- ❌ FORBIDDEN to leave "the specification" as a synonym for "whatever the current implementation
does".
8. Mandatory web verification
Before fixing anything in a real project, check on the web:
- LLVM: the latest major and patch version, the release calendar, and re-read the IR
Backwards Compatibility section of
llvm.org/docs/DeveloperPolicy.html — that is the source,
not this summary. Compatibility of the binding you use (inkwell, llvmlite, llvm-sys) with
that major.
- ANTLR: the latest release and the project's activity (as of August 2026 the last one published
was 4.13.2, from August 2024; if it is still the same, weigh it as a maintenance risk).
Licence BSD-3-Clause, read in the repository's
LICENSE.txt.
- Tree-sitter: version (still on
0.x, with breakage between minors), the state of the
bindings API and the MIT licence read in LICENSE.
- Cranelift / Wasmtime: version, supported architectures and production status.
- LSP: whether 3.18 has been published as final or is still "under development"; changes in the
negotiation of
positionEncoding (UTF-8 vs. UTF-16).
- Raw licences (
LICENSE, COPYING, LICENSE.txt, careful with master versus main) of
every parsing, codegen or runtime library you link — especially if you link LLVM: it is
Apache-2.0 WITH LLVM-exception and the exception matters.
- CVEs of the runtime you embed (LLVM, regular expression engines, native grammar loaders).
- The state of the Wasm target if you compile to it: WASI 0.2/0.3, the Component Model, and which
runtime supports it today.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: compilers-dsl-standards3description: Building a language or a language processor, and deciding first whether you need one at all. Use when designing an internal (embedded) or external DSL, writing a lexer and parser by hand or with a generator (ANTLR .g4 grammars, Tree-sitter grammar.js and node-types.json, lex/flex .l and yacc/bison .y files, PEG parsers with pest .pest / peg.js / Lark, parser combinators like nom, chumsky, megaparsec or FParsec), building an AST and its source spans, name resolution and scoping, a type checker or Hindley-Milner inference, choosing a backend (LLVM IR and llvm-sys/inkwell/llvmlite, Cranelift, WebAssembly as a target, transpiling to another language, or a tree-walking versus bytecode interpreter), deciding whether a JIT is worth it, designing compiler diagnostics with spans, labels and fix-its (ariadne, codespan-reporting, miette), snapshot-testing compiler output, fuzzing a parser, building a conformance suite, or shipping the tooling a language needs to be usable - a Language Server Protocol server, formatter, 4---56# Compiler and DSL standards78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to **building a language processor**: your own DSL, an interpreter, a compiler, a13transpiler, a *linter* with its own parser, or an input format with enough syntax to need a grammar.14It covers the prior decision (do you need a language?), the frontend, the intermediate15representation, the type system, the backend, diagnostic quality, testing and the tooling that makes16a language usable.1718Triggers: ANTLR `.g4`, Tree-sitter `grammar.js` and `node-types.json`, flex/bison `.l`/`.y`,19`.pest`, `.lark`, `.ebnf`, combinators (`nom`, `chumsky`, `megaparsec`, `FParsec`, `parsec`),20`llvm-sys`/`inkwell`/`llvmlite`/`libLLVM`, `.ll` and `.bc`, Cranelift `cranelift-codegen`,21`wasm32` as the target of your own compiler, "AST", "IR", "SSA", "name resolution",22"type inference", "symbol table", "parser error recovery", "tree versus bytecode",23"should I add a JIT?", `textDocument/publishDiagnostics` and other LSP methods,24`ariadne`/`codespan-reporting`/`miette`, `insta`/`expect-test`/`lit`/`FileCheck`, "ambiguous25grammar", "shift/reduce conflict", "left recursion".2627**Governing principle, and it is a prohibition before it is advice: your own DSL is the most28expensive over-engineering there is.** The parser is not the cost — the parser is a weekend —; the29cost is the documentation, the highlighting, the formatter, the LSP, the debugger, the error30messages, the training of every new person and the migration when the syntax changes, **for the31whole life of the system**. A language without that budget committed does not get started. The32ladder is climbed from the bottom up and you only climb a rung when the previous one has failed33**with a concrete, demonstrated case**:34351. **Data**: JSON/YAML/TOML with a validated schema (JSON Schema). It covers 80% of what people call36 a DSL.372. **Library / API**: functions and types of the host language. Zero new tooling, the IDE already38 works, the *type checker* already exists.393. **Internal (embedded) DSL**: a *builder*, operators, macros or host metaprogramming. It inherits40 the editor, the debugger, types and ecosystem. **It is the right answer almost always.**414. **External DSL with its own grammar**: only if the user **is not a programmer**, or if the syntax42 has to be portable across several hosts, or if the language must be analysable/verifiable in a43 way the host prevents (e.g. guaranteed termination, *sandbox* execution).445. **General-purpose language**: almost never, and not in a product project.4546**Second thesis, which orders §3.5 and §4**: **the quality of the error messages is a product47feature, not later polish.** It is what decides whether a language is adopted or hated; and it is48the technical reason why almost every serious compiler ends up with a hand-written parser.4950**Not applicable**: see `rust-standards`, `c-standards`, `cpp-standards`, `zig-standards`,51`haskell-fp-standards`, `ocaml-fsharp-standards`, `typescript-standards`, `python-standards`,52`go-standards` (**the language you write the compiler in is theirs**: build, lint, unit tests,53dependency management, idioms. Here the architecture of the language processor, not how the code54that implements it is written. OCaml/Haskell/F# appear here because their sum types and *pattern55matching* are the natural tool for an AST, but that recommendation belongs to §2, it is not a scope56handover), `webassembly-standards` (**Wasm as a target and as a runtime is theirs**: the `wasm32-*`57target, the Component Model and WIT, *sandbox* limits, *fuel*/epoch and the module size budget. Here58only the decision to **emit** Wasm from your backend and what you lose by doing so),59`lowcode-governance-standards` (**the low-code platform as a third-party product and its60governance** — life cycle, ownership, catalogue, licences, *shadow IT* — is theirs. Clean boundary:61**building a language belongs here; governing the use of one you bought, to them**. A visual flow62editor that serialises to JSON is a platform, not a DSL in the sense of this document),63`sql-standards` (**SQL is already the DSL almost nobody needs to reinvent**; if the problem is64querying data, the answer is usually SQL, not a new grammar), `api-design-standards`65(**if the user is a program, the answer is an API, not a syntax**: contract, versioning and66compatibility are theirs), `testing-qa-standards` (general quality strategy; here the specific67techniques: *snapshot*, parser *fuzzing*, conformance suites), `appsec-standards` (threat modelling68and agnostic vulnerability classes; here only the *sinks* particular to executing third-party code),69`performance-engineering-standards` (measurement and profiling methodology; here only what to70measure in a compiler and when a JIT does not pay off), `opensource-licensing-standards` (the71licence as a legal constraint; here only the fact that LLVM is **Apache-2.0 WITH72LLVM-exception**), `ai-agents-standards` (an agent that *generates* code in your DSL; here the DSL).7374## 2. Default decisions / Toolchain7576> Verify the latest version on the web before fixing it in a real project (§8).7778| Decision | Default | Justifiable alternative | Reason |79|---|---|---|---|80| A DSL? | **No.** Data with a schema or a host API | Internal DSL | The ladder in §1; rung 4 requires a written justification |81| Internal vs. external DSL | **Internal** | External if the user does not program | The internal one inherits IDE, types, debugger and ecosystem for free |82| Production parser | **Hand-written recursive descent** (+ *Pratt* for expressions) | A generator for prototyping or for someone else's already specified language | Total control of messages and **error recovery**; no opaque conflicts |83| Generator, if you use one | **ANTLR 4** (`.g4`, BSD-3-Clause) | bison/flex in legacy C | ALL(*) accepts left recursion; mature ecosystem. **Last release 4.13.2, August 2024** (verify §8): a stable project but with a very slow cadence — a risk datum, not a disqualifier |84| Incremental / editor parser | **Tree-sitter** (MIT, `0.26.x` as of Jul 2026) | LSP with a full reparse if the file is small | Incremental, error-tolerant reparse; it is what an editor wants. **Still on `0.x`: the API breaks between minors** |85| PEG | **Only with a documented alternative order** | — | The ordered `/` operator **removes ambiguity without telling you**: a rule that is never reached gives no error, it gives silence. See §7 |86| AST | Immutable sum types + **a `Span` on every node** | A CST/concrete tree if the original has to be reprinted | Without a `Span` there is no diagnostic, no *fix-it* and no LSP |87| Diagnostics | A rendering library with spans and labels (`ariadne`, `codespan-reporting`, `miette`) | Your own `file:line:col:` format, editor-compatible | A message with no highlighted code fragment is a useless message |88| Execution | **Bytecode interpreter** (stack VM) | A tree if the language is configuration and runs once | The tree is 5–20× slower; bytecode is not as complicated as it looks |89| JIT | **No, until you have the profile that demands it** | When the same code runs millions of times in a long-lived process | See §6.2 |90| Native backend | **LLVM** if you need quality machine code | **Cranelift** if compilation time dominates (JIT, Wasm) | LLVM optimises better; Cranelift compiles much faster and is pure Rust |91| "Cheap" backend | **Transpile** to C, Rust, Go or TypeScript | — | You inherit its optimiser, its portability and its debugger. The most underrated option |92| Portable target | **WebAssembly** | — | One backend, many platforms, *sandbox* included → `webassembly-standards` |93| Output tests | **Snapshot** (`insta`, `expect-test`, `lit`+`FileCheck`) | — | A compiler is a pure function from text to text: the snapshot is its natural test |94| Parser fuzzing | **Mandatory** (`cargo-fuzz`/libFuzzer/AFL++) | — | The parser is the attack surface; see §5 |95| Minimum tooling to publish | **LSP + formatter + highlighting** | — | Without the three, the language is not usable outside its author |9697**About LLVM, and this is the datum that breaks projects: LLVM's IR is not a stable format.** The98official policy says, textually, that *"The textual format is not backwards compatible. We don't99change it too often, but there are no specific promises."* For the *bitcode* there is a backward100reading guarantee (*"The current LLVM version supports loading any bitcode since version 3.0"*), but101with caveats that matter: *"Newer releases can ignore features from older releases, but they cannot102miscompile them"* and *"Debug metadata is special in that it is currently dropped during upgrades"*.103Operational, not theoretical consequences:104105- **Pin LLVM's major version in the build and treat it as a platform dependency**, not as an106 interchangeable library. Moving up a major is a project, with its window and its tests.107- **Do not keep `.ll` or `.bc` as a long-term artifact** or as an interchange format between108 components that are updated separately. That is the classic mistake.109- The *binding* (`inkwell`, `llvmlite`, `llvm-sys`) is **tied to LLVM's major**: your project's110 update pace is set by the slower of the two.111- LLVM publishes two majors a year and frequent patches (as of Jun 2026, **22.1.8**; verify §8).112 Licence **Apache-2.0 WITH LLVM-exception**.113114## 3. Structure and conventions115116### 3.1 Phases, and the rule that each one produces a datum117118```119text → [lexing] → tokens (+span) → [parsing] → CST/AST (+span)120 → [name resolution] → resolved AST → [types] → typed AST121 → [lowering] → IR → [optimisation] → IR → [backend] → output122```123124- **Each phase is a pure function from one structure to another.** No mutating the AST in place from125 five different phases: it destroys traceability and makes per-phase testing impossible.126- **Each node carries its `Span`** (start offset, end offset, file id). The `Span` survives every127 phase; if it is lost in the *lowering*, the type error cannot point at code.128- **Centralised `SourceMap`**: offsets are bytes, conversion to line/column happens once at render129 time. Careful with UTF-8: the column an editor expects is usually **UTF-16** (the LSP uses UTF-16130 by default); this is decided and documented once, not per function.131- **Errors as data, not as exceptions**: the compiler accumulates diagnostics and carries on.132 Aborting on the first error is the worst possible experience and makes the LSP unviable.133134### 3.2 Parser: why by hand135136The generator is faster to write and worse to use. A generated parser produces *"syntax error at137line 42"*; a hand-written one produces *"missing `)` — the opening parenthesis is on line 39"*. What138tips the balance is not performance, it is this:139140- **Error recovery**: the LSP needs a reasonable tree from a file the user is typing and which is by141 definition invalid. Recovery via synchronisation points (`;`, `}`, the start of a declaration) and142 **`Error` nodes in the tree**, not an exception.143- **Messages with context**: only you know what the parser was expecting and why.144- **Expression precedence**: *Pratt parsing* / *precedence climbing*, an explicit table of145 precedence and associativity. It is the part pure recursive descent does badly.146- If you use a generator: **prototype with it and switch to hand-written once the grammar147 stabilises**. Migrating with the grammar already written as a specification is cheap; starting by148 hand without knowing the grammar is not.149150### 3.3 Name resolution and types151152- **Name resolution is a phase of its own**, separate from the parser and from the *type checker*.153 Explicit scopes, not a mutable global table.154- **Inference**: Hindley-Milner (Algorithm W / level-based generalization) if the language is155 functional and you want full inference; bidirectional checking if there is subtyping, generics or156 overloading. **Pick one, and know which**, because mixing them by eye produces a type system that157 is impossible to explain and to give messages for.158- **Practical and unintuitive corollary: global inference makes error messages worse.** The error159 appears far from its cause. Requiring annotations at the boundaries (public functions, fields) is160 a usability decision, not a technical limitation.161162### 3.4 Backend: choose the cheapest that does the job163164By increasing cost: **tree interpreter → bytecode → transpile → Wasm → Cranelift → LLVM → your own165code generation**. You never climb a rung without a number to justify it. Transpiling to a language166with a good ecosystem is the most underrated route: you inherit the optimiser, the platforms, the167debugger and the profiler; the price is that **the target language's error messages leak to the168user** — and that has to be actively covered up with *source maps* or with a translation layer.169170### 3.5 Diagnostics, as a specification171172A complete diagnostic has: a **stable code** (`E0308`, `TS2345`) documented and searchable; a173**severity**; a **primary span** with the rendered fragment; **labelled secondary spans** ("it was174declared as `Int` here"); a **note** explaining the rule; and, when a mechanical correction exists, a175structured **`fix-it`** (range + replacement text) the LSP can apply. The error code is a contract:176**once published it is not reused for something else**.177178## 4. Quality and testing179180Gates in order of increasing cost; the first three break the build.1811821. **Unit tests per phase**: lexer, parser, resolution, types. Each with its minimal input.1832. **Snapshot of the output**, and of the **errors**: for each test program the serialised AST, the184 IR and **the exact text of the diagnostic** are frozen. Having the error message under version185 control is what stops it degrading without anyone noticing. `insta`, `expect-test`, or `lit` +186 `FileCheck` LLVM-style.1873. **A suite of invalid programs, as large as the valid one.** A compiler is judged by what it188 rejects and by how it explains it. Every defined error has at least one case.1894. **Parser fuzzing** (libFuzzer/AFL++, with a seed corpus from the suite): the target is **zero190 *panics*, zero stack overflows, zero infinite loops** on any input. A recursive parser overflows191 the stack with nested parentheses: **an explicit depth limit**, not trust.1925. ***Round-trip* / properties**: `parse(print(parse(x))) == parse(x)` for the formatter; random193 generation of valid ASTs and checking that they print and reparse.1946. **Differential**, if you are reimplementing something existing: compare against the reference195 implementation over a real corpus.1967. **Conformance**: if the language has a specification (yours or someone else's), the conformance197 suite is a separately versioned artifact, and the conformance percentage is published. **Without a198 suite, the "specification" is the implementation**, and then there is no specification.1998. **Performance regression**: compilation time over a fixed corpus, measured in CI with a200 threshold. A compiler that becomes slow loses users exactly as one that gives bad errors does.201202## 5. Stack security203204- **Running someone else's language is running someone else's code.** If the DSL is written by205 users, customers or an LLM, the interpreter is a trust boundary: **an instruction limit, a memory206 limit, a wall-clock limit, a recursion depth limit and a data nesting limit**, all mandatory and207 all configurable. Without them, a three-line loop is a denial of service.208- **Decide and document whether the DSL is Turing-complete.** If it does not need to be, **do not209 make it so**: a total language (with no unbounded loops) can be analysed, bounded and executed210 without fear. That is the real advantage of a DSL over "let them write Python".211- ***Host* surface: the list of native functions exposed is the complete attack surface.** No212 exposing the filesystem, the network or `exec` "for convenience". An explicit allowlist, reviewed213 the way a public API is reviewed.214- **Denial of service in the parser**: exponential backtracking in PEG and in regular expressions215 (ReDoS), nesting depth, unbounded input sizes. All with a limit and with a test.216- **No host `eval`** to implement the DSL. A "DSL" that compiles to Python/JS `eval()` **is not a217 DSL: it is code injection with pretty syntax**.218- **The compilation cache is a supply chain vector**: if you keep compiled artifacts (bytecode,219 `.bc`, objects) or download third-party grammars/plugins, they come with integrity verification. A220 Tree-sitter grammar file compiles to native C that gets loaded into your process.221- **Error messages do not leak absolute paths or the environment** when the compiler runs as a222 service. This error is as classic as it is boring.223224## 6. Performance and operability225226### 6.1 What to measure227228Time per phase (lexing, parsing, types, backend) over a fixed corpus; **the split is almost never229what you think** — in mature compilers the backend and resolution dominate, not the parser. In an230LSP, what matters is **perceived latency**: *completion* and *hover* below ~100 ms, and diagnostics231below ~500 ms from the last keystroke. That is achieved with incremental parsing and lazy querying232(a *query-based* architecture with memoisation), not by optimising the lexer.233234### 6.2 When a JIT does not pay off235236A JIT only wins when the **accumulated execution time of the hot code comfortably exceeds the cost237of compiling it**, and that cost includes what almost nobody counts: debugging complexity, the238impossibility of W^X on platforms that forbid it (iOS, consoles, many environments with strict239`seccomp`/SELinux), the attack surface of writable executable pages, and the work of maintaining it240per architecture. **It does not pay off** in: short processes, code that runs once (configuration,241templates, rules), I/O-dominated workloads, and anywhere portability matters more than the peak.242**Before a JIT**: compact bytecode, *inline caching*, *superinstructions*, and — first of all —243checking that the interpreter is not wasting its time on memory allocations.244245### 6.3 Tooling without which the language does not exist246247- **LSP** (spec 3.18 in development as of August 2026; 3.17 is the last one published as stable —248 verify §8). Minimum viable: diagnostics, *hover*, go to definition, autocompletion, document249 symbols.250- **A canonical formatter with no options.** The `gofmt` lesson: zero configuration removes the251 debate forever. It ships **from the first version**, because a retroactive formatter rewrites all252 the existing code at once.253- **Highlighting**: a Tree-sitter grammar (modern editors) and/or TextMate `.tmLanguage.json` (VS254 Code, and still the lowest common denominator).255- **Debugging**: if the language is transpiled or compiled, either you emit real debug information256 (DWARF, *source maps*) or you assume **nobody will be able to debug**. A conscious, documented257 decision.258- **Language versioning**: SemVer over the **syntax and the semantics**, not over the binary. A259 change that makes a previously valid program invalid is *breaking*, always.260261## 7. Long-term sustainability262263**A DSL with no owner is debt that cannot be refactored.** A language has no reliable *find usages*264outside its own tooling, so the code written in it becomes untouchable as soon as the person who265designed it leaves. Requirements of existence, not recommendations: **a named owner**, a **written266specification** (even if it is a short document), a **conformance suite**, a **compatibility267policy** and an **automated migration plan** (`fix`/*codemod*) for every syntax change. If any of268them is missing, the DSL is not approved.269270- ❌ **FORBIDDEN** to create an external DSL without having exhausted — and documented why they fail271 — rungs 1 to 3 of the ladder in §1.272- ❌ **FORBIDDEN** to implement the DSL on top of the host language's `eval()`/`exec()`.273- ❌ **FORBIDDEN** to run user code without time, memory, instruction and depth limits.274- ❌ **FORBIDDEN** to expose to the DSL native file, network or process functions without a reviewed275 allowlist.276- ❌ **FORBIDDEN** an AST with no `Span`. A node with no position is a diagnostic that cannot be277 given.278- ❌ **FORBIDDEN** to abort compilation on the first syntax or type error.279- ❌ **FORBIDDEN** to treat LLVM's IR as a stable interchange or storage format (§2), and forbidden280 to leave LLVM's major version unpinned in the build.281- ❌ **FORBIDDEN** to publish a language with no formatter and no highlighting. "It will come later"282 means never.283- ❌ **FORBIDDEN** to change a diagnostic's text without a snapshot recording it.284- ❌ **FORBIDDEN** to reuse a retired error code for another meaning.285- ❌ **FORBIDDEN** a PEG grammar with alternatives whose order is not documented: an unreachable rule286 **gives no error, it gives silence** (§2).287- ❌ **FORBIDDEN** a recursive parser with no depth limit and no *fuzzing* test verifying it.288- ❌ **FORBIDDEN** "we will optimise it with a JIT" with no prior profile (§6.2).289- ❌ **FORBIDDEN** to break the syntax with no *codemod* and no notice period with both forms valid.290- ❌ **FORBIDDEN** to leave "the specification" as a synonym for "whatever the current implementation291 does".292293## 8. Mandatory web verification294295Before fixing anything in a real project, check on the web:296297- **LLVM**: the latest major and patch version, the release calendar, and **re-read the *IR298 Backwards Compatibility* section of `llvm.org/docs/DeveloperPolicy.html`** — that is the source,299 not this summary. Compatibility of the *binding* you use (`inkwell`, `llvmlite`, `llvm-sys`) with300 that major.301- **ANTLR**: the latest release and the project's activity (as of August 2026 the last one published302 was **4.13.2, from August 2024**; if it is still the same, weigh it as a maintenance risk).303 Licence **BSD-3-Clause**, read in the repository's `LICENSE.txt`.304- **Tree-sitter**: version (still on `0.x`, with breakage between *minors*), the state of the305 *bindings* API and the **MIT** licence read in `LICENSE`.306- **Cranelift / Wasmtime**: version, supported architectures and production status.307- **LSP**: whether 3.18 has been published as final or is still "under development"; changes in the308 negotiation of `positionEncoding` (UTF-8 vs. UTF-16).309- **Raw licences** (`LICENSE`, `COPYING`, `LICENSE.txt`, careful with `master` versus `main`) of310 every parsing, *codegen* or *runtime* library you link — especially if you link LLVM: it is311 **Apache-2.0 WITH LLVM-exception** and the exception matters.312- **CVEs** of the *runtime* you embed (LLVM, regular expression engines, native grammar loaders).313- The state of the Wasm *target* if you compile to it: WASI 0.2/0.3, the Component Model, and which314 runtime supports it today.315316If the web contradicts this document, **the web wins** — flag the discrepancy.