You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Monogram's grammar is non-deterministic by design (backtracking recursive-descent +
Pratt: ordered choice, not() lookahead, speculative reparse). That maps to RUNTIME
exploration, which tree-sitter's GLR absorbs directly — the derived tree-sitter
highlighter scores 95.9%, above official. It does NOT map to Lezer's BUILD-TIME LR(1)
automaton: the optional-modifier-heavy rules blow up the LR table combinatorially
(ClassMember 716 choices, buildParser hangs), and the `<` type-vs-expression ambiguity
is a genuine shift/reduce conflict with no mechanical resolution. Getting it to build
would require a hand-authored, Lezer-specific grammar — which contradicts the "one
grammar, derive everything" thesis (the official @lezer/javascript is exactly that:
hand-written, not derived).
Rather than carry a generator that produces nothing usable, drop the whole target:
- delete src/gen-lezer.ts, examples/lezer/, test/lezer-smoke.ts, the CLI emission
- drop the official-Lezer comparison from the bench (engine + README chart row) and the
@lezer/generator + @lezer/javascript + @lezer/highlight devDependencies
- update README / ROADMAP / docs; regenerate the now 3-engine chart
tree-sitter remains the parser-based highlighting target (Neovim, GitHub, Zed, Helix);
TextMate and Monarch are the other derived outputs.
Copy file name to clipboardExpand all lines: README.md
+8-11Lines changed: 8 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Monogram
2
2
3
-
Write a language's grammar **once**. Monogram runs that single definition as a real parser, proves it against the language's official conformance suite, and then **derives a syntax highlighter** — TextMate, tree-sitter, Lezer, Monarch — from the same proven grammar. The highlighter's correctness flows *down* from a parser-verified model instead of *up* from hand-tuned regex.
3
+
Write a language's grammar **once**. Monogram runs that single definition as a real parser, proves it against the language's official conformance suite, and then **derives a syntax highlighter** — TextMate, tree-sitter, Monarch — from the same proven grammar. The highlighter's correctness flows *down* from a parser-verified model instead of *up* from hand-tuned regex.
4
4
5
5
> *mono + grammar — one grammar definition, many derived artifacts.*
6
6
@@ -31,7 +31,7 @@ Take `typeof x < y`. A regex highlighter has to guess whether `<` opens a generi
31
31
32
32
1.**Write the grammar, then prove it.** The grammar is executable. Monogram runs it as a recursive-descent + [Pratt](https://en.wikipedia.org/wiki/Operator-precedence_parser) (operator-precedence) parser over the TypeScript conformance suite and measures *bidirectionally* — it must **accept** every input `tsc` accepts **and reject** every input `tsc` rejects. Today: **100%** of valid single-file cases parse, and **97.8%** bidirectional agreement (see [Results](#results)).
33
33
34
-
2.**Derive the highlighter from the proven grammar.** The TextMate grammar — and the tree-sitter / Lezer / Monarch ones — are generated from that same parser-validated definition, never hand-written. Their correctness is underwritten by the conformance run, not by regex tuning.
34
+
2.**Derive the highlighter from the proven grammar.** The TextMate grammar — and the tree-sitter / Monarch ones — are generated from that same parser-validated definition, never hand-written. Their correctness is underwritten by the conformance run, not by regex tuning.
35
35
36
36
The remaining 2.2% bidirectional gap is **over-acceptance** — the grammar is still too permissive on some *invalid* inputs (valid-code coverage is already 100%; nothing valid is missed). Most of what's left is code `tsc`'s parser rejects via *context-sensitive* rules a context-free grammar can't express — reserved-word placement, modifier combinations like `default abstract class`, `super` type-arguments — the kind of constraint a highlighter grammar was never meant to enforce. We push as close to 100% both ways as a pure grammar can; that's the asymptote, and the highlighter rides down to it for free.
37
37
@@ -56,7 +56,7 @@ The valid-code/bidirectional numbers are the grammar's correctness proof; the hi
56
56
57
57
### Head-to-head: highlighter correctness across ecosystems
58
58
59
-
**Auto-generated** by [`test/highlight-bench.ts`](test/highlight-bench.ts) — Monogram's *derived* TextMate highlighter against the official hand-written grammars of all three ecosystems (TextMate, tree-sitter, Lezer), each graded by the same neutral `tsc` oracle:
59
+
**Auto-generated** by [`test/highlight-bench.ts`](test/highlight-bench.ts) — Monogram's *derived* TextMate highlighter against the official hand-written grammars of two ecosystems (TextMate, tree-sitter), each graded by the same neutral `tsc` oracle:
60
60
61
61
<!-- bench:start -->
62
62
<!-- generated by `npm run bench:readme` — do not edit by hand -->
@@ -69,15 +69,14 @@ official hand-written grammar for its ecosystem; Monogram's is *derived* from it
69
69
```
70
70
TypeScript — token-family accuracy (higher = more correct)
<sub>Graded over 102 ambiguity-rich snippets ([`test/issue-cases.ts`](test/issue-cases.ts)) against tsc's own parse tree. The three parser-based highlighters (Monogram, tree-sitter, Lezer) cluster well above the hand-written TextMate grammar — that gap is the whole point. Per-engine vocabulary→family maps (frozen, auditable): [`test/highlight-engines.ts`](test/highlight-engines.ts). JavaScript: not yet on the bench. Regenerate: `npm run bench:readme`.</sub>
76
+
<sub>Graded over 102 ambiguity-rich snippets ([`test/issue-cases.ts`](test/issue-cases.ts)) against tsc's own parse tree. The parser-derived highlighters (Monogram, official tree-sitter) sit well above the hand-written TextMate grammar — that gap is the whole point. Per-engine vocabulary→family maps (frozen, auditable): [`test/highlight-engines.ts`](test/highlight-engines.ts). JavaScript: not yet on the bench. Regenerate: `npm run bench:readme`.</sub>
78
77
<!-- bench:end -->
79
78
80
-
> **Why the Monogram row is its *TextMate* output.** That's the one CI can rebuild on every commit. Monogram *also* derives a full **tree-sitter** grammar + query from the same definition; built locally to wasm and graded through this *same* `tsc` oracle, it scores **95.9%** — **above the official parsers** (tree-sitter 92.7%, Lezer 89.5%) and both TextMate grammars (its own 87.6%, official 76.2%). The lift is *structural*: every capture is derived from grammar shape — type references by node position (`(type (ident) @type)`), member keys from the member-name rule, type parameters from the `< … >` binder, declaration names keyword-anchored — never a hardcoded vocabulary, so it never paints a value as a type. It's opt-in (needs a local wasm build), hence out of the CI chart: `MONOGRAM_TS_WASM=… node test/highlight-bench.ts --corpus adversarial --engines`. *(There is no Monogram-**Lezer** number: the optional-modifier-heavy rules blow up Lezer's LR table generation combinatorially, and behind that the `<`/type-vs-expression ambiguity is a genuine LR(1) shift/reduce conflict. tree-sitter's GLR absorbs both; Lezer's LR(1) does not.)*
79
+
> **Why the Monogram row is its *TextMate* output.** That's the one CI can rebuild on every commit. Monogram *also* derives a full **tree-sitter** grammar + query from the same definition; built locally to wasm and graded through this *same*`tsc` oracle, it scores **95.9%** — **above official tree-sitter** (92.7%) and both TextMate grammars (its own 87.6%, official 76.2%). The lift is *structural*: every capture is derived from grammar shape — type references by node position (`(type (ident) @type)`), member keys from the member-name rule, type parameters from the `< … >` binder, declaration names keyword-anchored — never a hardcoded vocabulary, so it never paints a value as a type. It's opt-in (needs a local wasm build), hence out of the CI chart: `MONOGRAM_TS_WASM=… node test/highlight-bench.ts --corpus adversarial --engines`.
81
80
82
81
> **The other side of the ledger (honesty check).** On the *broad* TS parser-conformance corpus — not just the documented bugs — the two are now neck-and-neck: token-role accuracy is **tied at ~99%**, Monogram **leads on per-cell coverage** (100% of strict cells vs official's ~97%), and whole-file-perfect snippets are essentially level (~88% vs ~89%). The small residual is niche multiline type-annotation scoping, **not** the ambiguity class above. Clone the TS corpus to `/tmp/ts-repo` and run `node test/highlight-bench.ts` to see both corpora.
83
82
@@ -93,8 +92,7 @@ From one grammar definition (a small TypeScript combinator API), three outputs a
93
92
94
93
And — from the same grammar — generators for the rest of the ecosystem, at varying maturity:
95
94
96
-
-**tree-sitter** — `grammar.js` + a **structural**`queries/highlights.scm` + an external scanner for context-sensitive lexing. Builds end-to-end (tree-sitter's GLR absorbs the grammar; compiles to wasm) and the *derived* query scores **95.9%** through the same oracle as the chart above — **above the official parsers** (tree-sitter 92.7%, Lezer 89.5%). Opt-in (needs a local wasm build), so it stays out of the CI chart.
97
-
-**Lezer** — a CodeMirror 6 grammar + `styleTags` + a JS tokenizer. Does **not** build: the grammar trips Lezer's **LR(1) shift/reduce wall** (TypeScript's `<` type-vs-expression ambiguity, which `tsc` resolves with semantic information LR(1) lacks). That's a ceiling of Lezer's parser class rather than a gap in the grammar — so there is no derived Lezer highlighter number.
95
+
-**tree-sitter** — `grammar.js` + a **structural**`queries/highlights.scm` + an external scanner for context-sensitive lexing. Builds end-to-end (tree-sitter's GLR absorbs the grammar; compiles to wasm) and the *derived* query scores **95.9%** through the same oracle as the chart above — **above official tree-sitter** (92.7%). Opt-in (needs a local wasm build), so it stays out of the CI chart.
98
96
-**Monarch** — a Monaco (web) tokenizer (functional, bounded by JS-regex limits).
99
97
100
98
## The grammar is the source of truth
@@ -161,7 +159,7 @@ A new language is **one grammar file, proven the way TypeScript is** — by its
161
159
2.**Prove it as a parser** against the language's *own* official test suite, measured **bidirectionally** (accept what the reference accepts, reject what it rejects). A grammar is "ready" when valid-code coverage is 100% and bidirectional agreement is high — that run is the correctness proof.
162
160
3.**Bring the reference highlighter as the baseline.** Drop in the language's existing official TextMate grammar so coverage is *measured against the thing you're replacing*, not asserted.
163
161
164
-
The highlighter, lexer, and CST types fall out of step 1 automatically (the tree-sitter / Lezer / Monarch generators give you a scaffold to finish); steps 2–3 are how the result earns trust. A new-language PR is reviewed on exactly two numbers: **parser conformance** and **highlighter coverage vs the official grammar**.
162
+
The highlighter, lexer, and CST types fall out of step 1 automatically (the tree-sitter / Monarch generators give you a scaffold to finish); steps 2–3 are how the result earns trust. A new-language PR is reviewed on exactly two numbers: **parser conformance** and **highlighter coverage vs the official grammar**.
165
163
166
164
> The conformance and highlighter *harnesses* are currently TypeScript-specific — they call `tsc`'s `parseDiagnostics` and read VS Code's bundled TS grammar. A contributor adapts those harness scripts to their reference compiler's diagnostic API; the engine and generators themselves are reused unchanged.
167
165
@@ -222,15 +220,14 @@ examples/typescript.ts one grammar (TypeScript combinator API)
shared src/grammar-utils.ts structural helpers used across stages
230
227
src/api.ts, types.ts the grammar's combinator + type surface
231
228
```
232
229
233
-
Every highlighter target (TextMate, tree-sitter queries, Lezer styleTags, Monarch) is produced by the *same* structural scope-inference, retargeted per format — so highlighting stays consistent across ecosystems.
230
+
Every highlighter target (TextMate, tree-sitter queries, Monarch) is produced by the *same* structural scope-inference, retargeted per format — so highlighting stays consistent across ecosystems.
234
231
235
232
-**One grammar, many derived artifacts.**`gen-lexer` builds a tokenizer from the token definitions; `gen-parser` composes it and interprets the rules into a CST; `gen-tm` reads the same rule *shapes* to derive TextMate patterns; `gen-vscode-config` derives editor config from the same tokens and `scopes`. Shared structural primitives (`grammar-utils.ts`) keep them consistent.
236
233
-**CST, not AST.** Keeping every token (punctuation, keywords) as a node is required for the highlighter and for lossless source reconstruction — roughly 2× the nodes of an AST, by design.
Copy file name to clipboardExpand all lines: docs/upstream-issues.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,7 +45,7 @@ When an issue is **not** solved, it gets exactly one verdict, and **⛔ TM-impos
45
45
1.**🔧 Backlog** — reproduce it; if a parser-derived TM pattern *can* express the fix, it's backlog. Fix it in `examples/typescript.ts`, add a case to `test/test-issues.ts`, move it to Solved.
46
46
2.**📦 Out of scope** — the construct belongs to a dialect/region we don't generate yet (TSX, JSDoc body).
47
47
3.**🧠 Needs semantics** — the correct scope depends on resolving a symbol's *kind* (type/value/namespace) or type relationships. See [Proof A](#proof-a). Out of scope for every grammar, ours included.
48
-
4.**⛔ TM-impossible** — *only* when you can show **(i)** our own generated `tmLanguage.json` cannot express it (a concrete failing pattern) **and (ii)** our tree-sitter or Lezer target (real parsers) *does* get it right. See [Proof B](#proof-b). File the failing-TM + passing-parser pair as the proof. **Currently 0 issues have cleared this bar.**
48
+
4.**⛔ TM-impossible** — *only* when you can show **(i)** our own generated `tmLanguage.json` cannot express it (a concrete failing pattern) **and (ii)** our tree-sitter target (a real parser) *does* get it right. See [Proof B](#proof-b). File the failing-TM + passing-parser pair as the proof. **Currently 0 issues have cleared this bar.**
**A purely syntactic highlighter — TextMate grammar *or* CST parser — cannot resolve a symbol's *kind*.** Both see only the token stream, never the resolved program. Whether `Foo` in `Foo.Bar` is a namespace or a class, whether an `import` binds a type or a value, whether `MAX` is a constant — all require name resolution / type-checking against declarations that may be in another file. Monogram's parser is a *concrete syntax tree* builder; it has no symbol table either. So semantic-coloring issues are unsolvable by **every** layer here — the job belongs to a semantic-token provider (language server). This is *not* a TextMate-specific weakness; it is the boundary of syntax highlighting itself. (Monogram's win is making the *syntactic* layer exact, so only the genuinely-semantic residue is left for the server.)
85
85
86
86
### Proof B
87
-
**A TextMate grammar is a line-oriented pushdown machine whose only inter-line memory is a finite stack of scope contexts**; each step is a single Oniguruma match (bounded even with `\g<>` subexpression recursion, which is per-line). Where correct highlighting requires context *not* captured by the enclosing-scope stack — an unbounded lookback to a token that is not an ancestor context — no TextMate grammar can express it, the official one or ours. Monogram's **tree-sitter and Lezer**targets are real incremental parsers holding the full tree, so they resolve such cases exactly; the generated TM target (like the official) can only approximate.
87
+
**A TextMate grammar is a line-oriented pushdown machine whose only inter-line memory is a finite stack of scope contexts**; each step is a single Oniguruma match (bounded even with `\g<>` subexpression recursion, which is per-line). Where correct highlighting requires context *not* captured by the enclosing-scope stack — an unbounded lookback to a token that is not an ancestor context — no TextMate grammar can express it, the official one or ours. Monogram's **tree-sitter**target is a real incremental parser holding the full tree, so it resolves such cases exactly; the generated TM target (like the official) can only approximate.
88
88
89
89
> **But beware over-claiming this.** The 50 solved cases include heavy multiline / whitespace / nested-generic scenarios long *assumed* "impossible for TextMate" — they are not; they are TM-*expressible* once a parser tells you the right pattern. So this bucket is narrow, and we keep it empty until an issue is *proven* into it (failing generated-TM + passing parser-target). Don't reach for "TM can't" when the honest answer is "we haven't derived the pattern yet."
0 commit comments